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"))