1.阻塞IO模型
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
普通的socket通信就是阻塞IO,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
2.非阻塞IO
如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error,就代表NO data 。从用户进程角度讲 ,它发起一个call后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有
#服务端 from socket import * import time s=socket(AF_INET,SOCK_STREAM) s.bind(('127.0.0.1',8080)) s.listen(5) s.setblocking(False) #设置socket的接口为非阻塞 conn_l=[] del_l=[] while True: try: conn,addr=s.accept() #因为是非阻塞的没有连接会报错 conn_l.append(conn) except BlockingIOError: for conn in conn_l: #没有连接的时候就是循环已经请求过来的conn try: data=conn.recv(1024) #因为非阻塞,如果也没有conn也会报错,如有有就继续一直循环连接池里是否有人发信息 if not data: #有人断开连接 del_l.append(conn) #因为发送的数据为空了,说明这个连接已经断了,下次要删除这个连接否则会报错 continue conn.send(data.upper()) except BlockingIOError: pass except ConnectionResetError: del_l.append(conn) for conn in del_l: #循环需要删除conn的列表 conn_l.remove(conn) #从连接池里去除 conn.close() #close del_l=[] #清空这个需要删除conn的列表,否则下去for 还会循环到 #客户端 from socket import * c=socket(AF_INET,SOCK_STREAM) c.connect(('127.0.0.1',8080)) while True: msg=input('>>: ') if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8'))
3.IO多路复用
判断非阻塞调用是否就绪如果 OS 能做,是不是应用程序就可以不用自己去等待和判断了,就可以利用这个空闲去做其他事情以提高效率。
select poll epoll的好处就在于单个process就可以同时处理多个网络连接的IO
所以OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。并且提供了专门的系统模块让应用程序可以接收事件通知。这个模块就是select。让应用程序可以通过select注册文件描述符和回调函数。当文件描述符的状态发生变化时,select 就调用事先注册的回调函数。
select因其算法效率比较低,后来改进成了poll,再后来又有进一步改进,BSD内核改进成了kqueue模块,而Linux内核改进成了epoll模块。这四个模块的作用都相同,暴露给程序员使用的API也几乎一致,区别在于kqueue 和 epoll 在处理大量文件描述符时效率更高。
鉴于 Linux 服务器的普遍性,以及为了追求更高效率,所以我们常常听闻被探讨的模块都是 epoll 。
它的基本原理就是会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
1.如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
结论: select的优势在于可以处理多个连接,不适用于单个连接
#服务端 from socket import * import select s=socket(AF_INET,SOCK_STREAM) s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) s.bind(('127.0.0.1',8081)) s.listen(5) s.setblocking(False) #设置socket的接口为非阻塞 read_l=[s,] #把服务端放入 一个列表 一会传给select while True: #select()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息 r_l,w_l,x_l=select.select(read_l,[],[]) #等待有人连接 for ready_obj in r_l: if ready_obj == s: conn,addr=ready_obj.accept() #此时的ready_obj等于s read_l.append(conn) else: try: data=ready_obj.recv(1024) #此时的ready_obj等于conn if not data: ready_obj.close() read_l.remove(ready_obj) continue ready_obj.send(data.upper()) except ConnectionResetError: #当接受的数据没有完成时,客服端断开了触发error ready_obj.close() read_l.remove(ready_obj) #客户端 from socket import * c=socket(AF_INET,SOCK_STREAM) c.connect(('127.0.0.1',8081)) while True: msg=input('>>: ') if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8'))
epoll适用于linux,好处在于能接受的文件描述符要更多,epoll采用的是事件通知机制,而不再是以轮询的方式挨个询问每个文件描述符的状态,节省cpu时间。
mac os 使用 kqueue 同epoll
selectors,它的功能与linux的epoll,还是select模块,poll等类似;实现高效的I/O multiplexing, 常用于非阻塞的socket的编程中,根据平台选出最佳的IO多路机制,比如在win的系统上他默认的是select模式而在linux上它默认的epoll。
模块定义了一个 BaseSelector的抽象基类, 以及它的子类,包括:SelectSelector, PollSelector, EpollSelector, DevpollSelector, KqueueSelector.
另外还有一个DefaultSelector类,它其实是以上其中一个子类的别名而已,它自动选择为当前环境中最有效的Selector,所以平时用 DefaultSelector类就可以了,其它用不着。
import selectors import socket sel = selectors.DefaultSelector() def accept(sock, mask): conn, addr = sock.accept() # Should be ready print('accepted', conn, 'from', addr) conn.setblocking(False) sel.register(conn, selectors.EVENT_READ, read) def read(conn, mask): data = conn.recv(1024) # Should be ready if data: print('echoing', repr(data), 'to', conn) conn.send(data) # Hope it won't block else: print('closing', conn) sel.unregister(conn) #取消某一个注册对象比如 conn conn.close() sock = socket.socket() sock.bind(('localhost', 10000)) sock.listen(100) sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, accept) while True: events = sel.select() #m默认阻塞,有活动连接就返回活动连接列表 for key, mask in events: print(key) #SelectorKey,包含了fileobj注册的对象也就是socket对象,fd文件描述符,events 1 或者0 可读还是可写,data就是注册对象的的回调函数 accept print(key.data) #accept 函数 print(mask) # 1 2 代表的读 写 print(key.fileobj) #及时绑定的sock对象 callback = key.data #accept callback(key.fileobj, mask)
如果不清楚以后用在什么操作系统下,尽量选择selectors,会自动选择是匹配最合适的epoll 还是select