11 非阻塞套接字与IO多路复用(进阶)
第一部分 基本IO模型
1.普通套接字实现的服务端的缺陷
一次只能服务一个客户端!
2.普通套接字实现的服务端的瓶颈!!!
accept阻塞!
在没有新的套接字来之前,不能处理已经建立连接的套接字的请求。
recv 阻塞!
在没有接受到客户端请求数据之前,
不能与其他客户端建立连接!
3.普通服务器的IO模型
第二部分 非阻塞套接字
1.非阻塞套接字与普通套接字的区别
>>> import socket >>> server = socket.socket() # 讲socket设置成非阻塞 >>> server.setblocking(False) # 注意!这必须要在其他操作之前! >>> server.bind(('',8080)) >>> server.listen(5) >>> server.accept() # 没有连接就引发BlockingIOError Traceback (most recent call last): File "<pyshell#5>", line 1, in <module> server.accept() File "E:\python\lib\socket.py", line 205, in accept fd, addr = self._accept() BlockingIOError: [WinError 10035] 无法立即完成一个非阻止性套接字操作。 # 使用一个客户端(普通的就行,不需要非阻塞)连接过来 >>> conn,addr = server.accept() # 有连接则正确返回 >>> conn.recv(1024) # 没有连接数据就引发BlockingIOError Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> conn.recv(1024) BlockingIOError: [WinError 10035] 无法立即完成一个非阻止性套接字操作。
2.使用非阻塞套机字实现阻塞的服务端
# 原来的recv while True: try: recv_data = conn.recv(1024) break except BlockingIOError: pass # 原来的accept while True: try: conn,addr = server.accept() break except:BlockingIOError: pass
3。非阻塞客户端套接的注意点
connect操作一定会引发BlockingIOError异常
如果连接没有建立,那么send操作引发OSError异常
第三部分 非阻塞IO模型
第四部分 使用非阻塞套接字实现并发
1.整体思路
吃满 CPU !宁可用 whileTrue ,也不要阻塞发呆!
只要资源没到,就先做别的事!将代码顺序重排,避开阻塞!
2.实现了什么?
并发服务多个客户端!
3.编程范式
import socket server = socket.socket() # 生成套接字 server.setblocking(False) # 非阻塞 server.bind(('',7788)) server.listen(1000) # 我们现在生成的非阻塞套接字,非阻塞套接字在执行accept跟recv的时候不会阻塞,但是会报错, # 所以我们写非阻塞的并发服务器需要用到异常处理 all_conn = [] # 用来保存我们所有的已经生成的套接字(这个客户端还在连接着) while True: # 处理连接,生成对等连接套接字 try: conn,addr = server.accept() conn.setblocking(False) # conn是新生成的,需要给它设置一下非阻塞 all_conn.append(conn) # 这一行代码的前提是,上面一行代码正常返回 except BlockingIOError: pass for conn in all_conn: try: # 只负责接受数据 recv_data = conn.recv(1024) if recv_data: res = recv_data.decode() print(res) conn.send(recv_data) else: conn.close() all_conn.remove(conn) # 客户端关闭连接,就把它移除 except BlockingIOError: pass
2、IO多路复用
第一部分 不完美的CPU利用率
关键一: 任何Python操作都是需要花费CPU资源的 !
关键二: 如果资源还没有到达,那么
accept、recv以及
send(在connect没有完成时)
操作都是无效的CPU花费 !
关键三: 对应BlockingIOError的异常处理
也是无效的CPU花费 !
第二部分 epoll是真正的答案!
IO多路复用技术
我们把socket交给操作系统去监控
2.epoll是惰性的事件回调
惰性事件回调是由用户进程自己调用的。
操作系统只起到通知的作用。
3.为什么是epoll ?
目前Linux上效率最高的IO多路复用 技术 !
第三部分 IO多路复用选择器
1.注册惰性事件回调
>>> import socket >>> import selectors >>> server = socket.socket() >>> server.bind(('',9000)) >>> server.listen(1000) >>> selector = selectors.EpollSelector() # 实例化一个 epoll 选择器 >>> def create_conn(server): ... conn,addr = server.accept() ... return conn ... # 套接字、事件、回调函数 >>> selector.register(server,selectors.EVENT_READ,create_conn) SelectorKey(fileobj=<socket.socket fd=3, # 生成一个打包对象 family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 9000)>, # fileobj是对应套接字 fd=3, events=1, # 事件(1 表示EVENT_READ) data=<function create_conn at 0xb70b7b24>) # data是对应的回掉函数
2.事件回调
events = selector.select() # 查询,返回所有已经准备好的资源打包对象 print(enents) # 是一个 ‘二元组’ 的列表 [(SelectorKey(fileobj=<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 8888)>, fd=4, events=1, data=<function accept at 0xb71f292c>), 1)] # 我们只需要关心,每个元祖的第一项(即打包对象,其中包含了对应的套接字与回掉函数) # 接下来并不需要关心是什么套接字,什么事件,只需要调用对应的对调函数即可 callbeck = events[0][0].data sock = events[0][0].fileojb callbeck(sock)
3.编程范式
# 使用EpollSelector,实现一个并发的服务器 import socket import selectors # IO多路复用选择器的模块,接口,调用epoll epoll_selector = selectors.EpollSelector() # 创建一个用来和epoll通信的选择器 server = socket.socket() server.bind(('',8888)) server.listen(1000) def read(conn): recv_data = conn.recv(1024) if recv_data: print(recv_data.decode()) conn.send(recv_data) else: epoll_selector.unregister(conn) # 现在数据已经传输完了,那我现在就不用再去监控它了,所以# 关闭监控 conn.close() # 关闭连接 def accept(server): conn, addr = server.accept() # 生成一个对等连接套接字 # 要准备接受数据 epoll_selector.register(conn, selectors.EVENT_READ, read) epoll_selector.register(server,selectors.EVENT_READ, accept) # 注册事件在可以读的时候的回调函数 # 事件循环(主动去问epoll,哪些socket可以回调了,如果有了,那我就回调) while True: events = epoll_selector.select() # 查询所有的已经准备好的事件,返回一个列表(二元组列表) # a, b = events for key, mask in events: # 第一项是我们需要用的 callback = key.data # 从key里面把回掉函数拿出来 sock = key.fileobj # 从key里面把我们注册的那个socket拿出来 callback(sock)