day10-select IO多路复用代码实例
概述
Python的select()方法直接调用操作系统的IO接口,它监控sockets,open files, and pipes(所有带fileno()方法的文件句柄)何时变成readable 和writeable, 或者通信错误,select()使得同时监控多个连接变的简单,并且这比写一个长循环来等待和监控多客户端连接要高效,因为select直接通过操作系统提供的C的网络接口进行操作,而不是通过Python的解释器。
注意:Using Python’s file objects with select() works for Unix, but is not supported under Windows.
接下来通过echo server例子要以了解select 是如何通过单进程实现同时处理多个非阻塞的socket连接的
代码实现
非阻塞
说明:在设置为非阻塞的条件下,accept和recive都不阻塞了,没有值就报错。也就是说没有连接就不要让它走server.accept()这一步。让select帮助去检测这个100个连接。所以说只要有连接,有活动了,有数据(新连接进来)才accept。
import select import socket import sys import queue #创建一个socket服务器对象 server = socket.socket() server.setblocking(False) #设置不阻塞模式 #绑定端口到socket server.bind(('localhost', 8003)) #监听传入连接 server.listen(1000) inputs = [server,] #将自己加入列表(server本身也是一个连接) outputs= [] readable,writeable,exceptional = select.select(inputs,outputs,inputs) #1select函数阻塞进程,直到inputs中的套接字被触发(在此例中,套接字接收到客户端发来的握手信号,从而变得可读,满足select函数的“可读”条件),readable返回被触发的socket(服务器socket)#3 select再次阻塞进程,同时监听服务器套接字和获得的客户端套接字 print(readable,writeable,exceptional) for r in readable: #2 如果是服务器套接字被触发(监听到有客户端连接服务器) conn,addr = server.accept() print(conn,addr) data = conn.recv(1024) print("recv:",data) #运行输出 [<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8003)>] [] [] Traceback (most recent call last): File "/Users/huwei/PycharmProjects/s14/module_4/select_socket_server.py", line 22, in <module> <socket.socket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8003), raddr=('127.0.0.1', 61376)> ('127.0.0.1', 61376) data = conn.recv(1024) BlockingIOError: [Errno 35] Resource temporarily unavailable Process finished with exit code 1
程序运行解析:我们通过单线程下的I/O多路复用实现非阻塞的socket连接,这里的inputs为空,首先需要加入server本身,也就是说如果有客户端连接我了,就代表我活动了,所以在这里一开始是监测本身server,当加入server到inputs后,就程序就被阻塞了,这时候,有客户端的连接连入进来才不会阻塞,同时当连接进来后select会返回3个数据(readable,writeable,exceptional),而这里inputs并没有来自客户端的连接,而服务器端这时候又在接收数据,因为是非阻塞模式,所以服务器端直接返回一个错误,出现阻塞I/O错误。
select()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息,接下来我们需要创建2个列表来包含输入和输出信息来传给select().
因此,我们需要实现在客户端发送数据的时候,内核通知服务器端接收客户端发来的数据,那么就需要将客户端的连接append到inputs监测列表中,下一次select循环时,客户端发送数据到服务器端,通过服务器端判断这时候有新的客户端连接进来了,服务器端就可以接收数据了。
import select import socket import sys import queue #创建一个socket服务器对象 server = socket.socket() server.setblocking(False) #设置不阻塞模式 #绑定端口到socket server.bind(('localhost', 8002)) #监听传入连接 server.listen(1000) inputs = [server,] #将自己加入列表(本身也是一个连接) outputs= [] #inputs = [server,conn] #[conn,] 第一个客户端连接加入到服务器端 #inputs = [server,conn.conn2] #[conn2,] 第二个客户端连接加入到服务器端 while True: readable,writeable,exceptional = select.select(inputs,outputs,inputs) #1,#3 有连接进来会返回三个数据 print(readable,writeable,exceptional) for r in readable: if r is server: #2 如果是服务器套接字被触发(监听到有客户端连接服务器),代表来了一个新连接 conn,addr = server.accept() print("来了个新连接:",addr) inputs.append(conn) #是因为这个新建立的连接还没有发送数据过来,现在就接收的话,程序就报错,所以要想实现这个客户端发送数据来时,server端能够知道,就需要让select再监测这个conn else: #可能是第二个客户端的conn2 data = conn.recv(1024) print("recv:",data) conn.send(data.upper())
运行结果
程序运行解析:在启动服务器端后,启动客户端1并send数据,再启动客户端2并send数据,然后再切换到客户端1后send数据,此时服务器端出现报错,没有接收到客户端1send的数据,这是为什么呢?因为第一次连接是server本身,然后第二次client 1连接到服务器端后,该连接被append到inputs列表中,服务器端程序执行通过conn接收和发送数据,这时候又有客户端连接进来了,加入到inputs列表中,此时活动的连接可能就是conn2,但是程序使用的还是第二次client 1的conn接收和发送数据,而不是第三次的client 2的conn2接收和发送数据,所以出现了报错。
解决方案:将接收和发送数据的实例由conn改为r即可。
data = r.recv(1024) r.send(data.upper())
到这里程序就实现了服务器端和客户端的收发数据,但是我们想实现在服务器端接收到客户端的数据后,不立刻发回给客户端,而是先将它缓存到一个queue中,然后再由select取出来再发出去,接下来我们就对代码进行优化:
import select import socket import sys import queue # 创建一个socket服务器对象 server = socket.socket() server.setblocking(False) # 设置不阻塞模式 # 绑定端口到socket server.bind(('localhost', 8013)) # 监听传入连接 server.listen(1000) inputs = [server, ] # 将自己加入列表(本身也是一个连接) outputs = [] # inputs = [server,conn] #[conn,] msg_dict = {} while True: readable, writeable, exceptional = select.select(inputs, outputs, inputs) # 1,#3 有连接进来会返回三个数据 print("next:",readable, writeable, exceptional) for r in readable: # 2 如果是服务器套接字被触发(监听到有客户端连接服务器) if r is server: conn, addr = server.accept() print("来了个新连接:", addr) inputs.append(conn) # 是因为这个新建立的连接还没有发送数据过来,现在就接收的话,程序就报错,所以要想实现这个客户端发送数据来时,server端能够知道,就需要让select再监测这个conn msg_dict[conn] = queue.Queue() #初始化一个队列,后面存 要返回给这个客户端的数据 else: data = r.recv(1024) print("recv:", data) if data: msg_dict[r].put(data) outputs.append(r) #放入返回的连接队列里 else: print("客户端已经断开了",r) if r in outputs: outputs.remove(r) inputs.remove(r) del msg_dict[r] for w in writeable: #要返回给客户端的连接列表 data_to_client = msg_dict[w].get() w.send(data_to_client) #返回给客户端源数据 outputs.remove(w) #确保下次循环的时候writeable,不返回这个已经处理完的连接 for e in exceptional: if e in outputs: outputs.remove(e) inputs.remove(e) del msg_dict[e] server.close()
程序详解
inputs = [server, ]
#这里定义了一个需要select
监听的列表,列表里面是需要监听的对象(等于系统监听的文件描述符)。这里监听socket
套接字和用户的输入。
然后进行一个无限循环:
while True: #调用select函数,阻塞等待 readable, writeable, exceptional = select.select(inputs, outputs, inputs)
#调用了select
函数,开始循环遍历监听传入的列表inputs
。如果没有连接服务器,此时没有建立tcp
客户端连接,因此改列表内的对象都是数据资源不可用。因此select
阻塞不返回
在启动客户端1后,1个socket连接通信开始,此时inputs中的第一个对象server由不可用变成可用。因此select函数调用返回,此时readable有一个socket对象(文件描述符可读)。
for r in readable: # 建立连接 if r is server: conn, addr = server.accept() print("来了个新连接:", addr) # select监听的socket inputs.append(conn)
#select
返回之后,接下来遍历可读的文件对象,此时的可读中只有一个socket连接(r),调用socket的accept()
方法建立TCP
三次握手的连接,然后把该连接对象append到inputs
监视列表中,表示我们要监视该连接是否有数据IO
操作。由于此时readable
只有一个可用的对象,因此遍历结束。再回到主循环,再次调用select
,此时调用的时候,不仅会遍历监视是否有新的连接需要建立,还要监视刚才追加的客户端的连接。如果该客户端连接的数据到了,select
再返回到readable
,此时再进行for
循环。
如果没有新的socket连接,将会执行下面的代码:
else: #读取客户端连接发送的数据 data = r.recv(1024) print("recv:", data) if data: #把从客户端send来的数据放入queue中 msg_dict[r].put(data) #同时把该客户端的socket连接加入到output中 outputs.append(r) #放入返回的连接队列里 else: #没有客户端没有发送数据? print("客户端已经断开了",r) if r in outputs: outputs.remove(r) # inputs.remove(r) del msg_dict[r] #如果客户端连接在跟它对应的queue里有数据,就把这个数据取出来再发回给这个客户端 for w in writeable: data_to_client = msg_dict[w].get() w.send(data_to_client) outputs.remove(w) #如果在跟某个socket连接通信过程中出了错误,那么就要将此这个连接对象从inputs和outputs及queue中删除,再把连接关闭 for e in exceptional: if e in outputs: outputs.remove(e) inputs.remove(e) del msg_dict[e] server.close()
#通过socket连接调用recv
函数,获取客户端发送的数据并把数据放入定义好的queue中且将该客户端连接放入output列表中(监控和接收所有要发出去的数据),如果服务器端没有接收到客户端发送的数据,说明客户端已经断开连接,此时删除inputs和outputs中的socket连接及数据队列(全部删除);如果服务器端接收到客户端发送的数据,在下一次循环时将队列中的数据发送出去并将outputs中的客户端的socket连接删除,这样下一次循环select时监测outputs列表中就没有这个客户端的连接,那就会认为这个连接还处于非活动状态。最后在出现异常情况时,将服务器端监视的inputs/outputs/queue
中都除去该连接。然后关闭连接。