IO多路模型--select

  1.IO多路复用模型的触发

  首先介绍一个有关IO多路模型中的“触发”的概念:

 1 # 在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别如下:
 2 #
 3 # 水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态,
 4 # 没有必要每次描述符就绪后尽可能多的执行IO.select,poll就属于水平触发.
 5 #
 6 # 边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能
 7 # 多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述
 8 # 符.信号驱动式IO就属于边缘触发.
 9 #
10 # epoll既可以采用水平触发,也可以采用边缘触发.
11 #
12 # 大家可能还不能完全了解这两种模式的区别,我们可以举例说明:一个管道收到了1kb的数据,epoll会立即返回,此时
13 # 读了512字节数据,然后再次调用epoll.这时如果是水平触发的,epoll会立即返回,因为有数据准备好了.如果是边
14 # 缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,
15 # 直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).
16 
17 
18 # 下面我们还从电子的角度来解释一下:
19 # 
20 #     水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.上面提到的只要
21 # 有数据可读(描述符就绪)那么水平触发的epoll就立即返回.
22 # 
23 #     边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据
24 # 可读,但是没有新的IO活动到来,epoll也不会立即返回.
25 
26 水平触发和边缘触发

  2。详细代码分析

 #服务端
1
import socket 2 import select 3 4 sk1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 5 sk1.bind(("127.0.0.1", 6667)) 6 sk1.listen(5) 7 8 sk2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 9 sk2.bind(("127.0.0.1", 6668)) 10 sk2.listen(5) 11 while True: 12 r, w, e = select.select([sk1, sk2], [], []) 13 for i in r: 14 conn, addr = i.accept() 15 print(conn) 16 print("hello") 17 print(">>>", r)
 1 #客户端
 2 import socket
 3 import time
 4 
 5 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 6 while True:
 7     sk.connect(("127.0.0.1", 6667))
 8     print("hello")
 9     sk.sendall(bytes("byebye", encoding="utf-8"))
10     time.sleep(2)
11     break

上面这些代码很简单,一眼就可以看出来输出的结果:

<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6667), raddr=('127.0.0.1', 49322)>
hello
>>> [<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6667)>]

但是我想知道的是,如果将服务端代码改成如下(即将其中for循环下面的两行代码注释掉):

 1 import socket
 2 import select
 3 
 4 sk1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 5 sk1.bind(("127.0.0.1", 6667))
 6 sk1.listen(5)
 7 
 8 sk2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 9 sk2.bind(("127.0.0.1", 6668))
10 sk2.listen(5)
11 while True:
12     r, w, e = select.select([sk1, sk2], [], [])
13     for i in r:
14         # conn, addr = i.accept()
15         # print(conn)
16         print("hello")
17     print(">>>", r)

那么再次运行之后的输出结果会是什么呢?

服务端会一直打印如下信息:

hello
>>> [<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6667)>]

至于其中的原因,我想大家结合上面第一部分的解释,应该可以得出解释了

由于select是“水平触发”的形式来工作的,所以在没有注释掉那两行代码之前,

conn, addr = i.accept()
print(conn)

这两行代码(只要是第一行)相当于是用掉了r里面的元素(r,w,e为三个列表),所以在用完之后,“水平触发”不会再提醒(返回)新的元素,在用完之后如果没有新的连接到来,那么会在此处阻塞

r, w, e = select.select([sk1, sk2], [], [])

所以只会打印一遍,如果我此时将客户端重新运行一遍,相当于是新的连接到来,服务端会有如下新的信息打印出来

<socket.socket fd=5, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6667), raddr=('127.0.0.1', 49372)>
hello
>>> [<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6667)>]

下面来说一下注释那两行代码之后的情况:

由于那两行代码被注释掉,所以列表r里面的元素没有被使用,在“水平触发”模式工作下,会一直提醒,所以每次

r, w, e = select.select([sk1, sk2], [], [])

这行代码都会有返回值,即没有被阻塞,所以在for循环里面会打印“hello”,在

print(">>>", r)

这儿也会一直打印服务端socket对象。

 

话外篇:epoll支持“水平触发”和“边缘触发”两种模式

3.关于select的顺序问题

首先上代码

服务端:

 1 import socket
 2 import select
 3 
 4 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 5 sk.bind(("127.0.0.1", 8801))
 6 sk.listen(5)
 7 input_listen = [sk]
 8 while True:
 9     inputs, outputs, errors = select.select(input_listen, [], [])
10     for each in inputs:
11         if each == sk:
12             conn, addr = each.accept()
13             print(conn)
14             input_listen.append(conn)
15         else:
16             data_from_client = each.recv(1024)
17             print(str(data_from_client, encoding="utf-8"))
18             data_to_client = input("回复%d" % input_listen.index(each))
19             each.sendall(bytes(data_to_client, encoding="utf-8"))
20             # each.sendall(bytes("hello", encoding="utf-8"))
21     

客户端:

1 import socket
2 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3 sk.connect(("127.0.0.1", 8801))
4 while True:
5     data_client = input(">>>")
6     sk.send(bytes(data_client, encoding="utf-8"))
7     data_from_server = sk.recv(1024)
8     print(str(data_from_server, encoding="utf-8"))

可以看出上面的程序实现了:IO多路复用下的并发,现在的问题是,我启动了五个客户端,假设我们编号为1,2,3,4,5,向服务端发送消息的顺序是3,2,1,4,5,那么服务端接受消息并回应的顺序是什么呢?

首先我给出结果:3,1,2,4,5

现在给出我自己的解释如下:

首先先回应3是明显的,然后现在的问题是,为什么是3,1,2,4,5,而不是3,2,1,4,5呢?因为当服务端得到客户端3发来的消息时,因为要等待我们输入答复消息,所以服务端阻塞了,此时

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

收到了客户端2,1,4,5依次到来的消息,这里要讲明一点的是:select知道有消息到来,但是要确定消息是从谁发过来的需要遍历监控列表,此处为input_listen,而在这个程序中,input_listen是由客户端启动的顺序相关的,此处了[sk,1,2,3,4,5],数字代表启动的客户端socket对象,即conn,所以select按照这个列表的顺序依次遍历来判断到来的消息所属的客户端,所以input为[1,2,4,5],不考虑sk和3,sk和3的消息已经处理过了,所以最终的顺序是:3,1,2,4,5

当然需要说明一点的是,如果服务端在收到消息后直接将“hello”返回给客户端作为答复,即不存在阻塞,那么顺序当然是3,2,1,4,5了,即发送消息的顺序(不阻塞的代码如下):

 1 import socket
 2 import select
 3 
 4 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 5 sk.bind(("127.0.0.1", 8801))
 6 sk.listen(5)
 7 input_listen = [sk]
 8 while True:
 9     inputs, outputs, errors = select.select(input_listen, [], [])
10     for each in inputs:
11         if each == sk:
12             conn, addr = each.accept()
13             print(conn)
14             input_listen.append(conn)
15         else:
16             data_from_client = each.recv(1024)
17             print(str(data_from_client, encoding="utf-8"))
18             each.sendall(bytes("hello", encoding="utf-8"))

 

posted @ 2016-10-18 15:05  玖色堇  阅读(623)  评论(0编辑  收藏  举报