前言
問題:普通套接字實現的服務端的缺陷
一次只能服務一個客戶端!
在沒有新的套接字來之前,不能處理已經建立連接的套接字的請求
recv 阻塞!
在沒有接受到客戶端請求數據之前,不能與其他客戶端建立連接
可以用非阻塞接口來嘗試解決這個問題
IO阻塞與非阻塞
阻塞IO模型
阻塞IO(blocking IO)的特點:就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了。
什么是阻塞呢?想象這種情形,比如你等快遞,但快遞一直沒來,你會怎么做?有兩種方式:
- 快遞沒來,我可以先去睡覺,然后快遞來了給我打電話叫我去取就行了。
- 快遞沒來,我就不停的給快遞打電話說:擦,怎么還沒來,給老子快點,直到快遞來。
很顯然,你無法忍受第二種方式,不僅耽擱自己的時間,也會讓快遞很想打你。
而在計算機世界,這兩種情形就對應阻塞和非阻塞忙輪詢。
- 非阻塞忙輪詢:數據沒來,進程就不停的去檢測數據,直到數據來。
- 阻塞:數據沒來,啥都不做,直到數據來了,才進行下一步的處理。
非阻塞IO模型
非阻塞式IO中,用戶進程其實是需要不斷的主動詢問kernel數據准備好了沒有
非阻塞如何利用
- 寧可用 while True ,也不要阻塞發呆!
- 只要資源沒到,就先做別的事!
服務器端
import socket CONN_ADDR = ('127.0.0.1', 9999) conn_list = [] # 連接列表 sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 開啟socket sock.setblocking(False) # 設置為非阻塞 sock.bind(CONN_ADDR) # 綁定IP和端口到套接字 sock.listen(5) # 監聽,5表示客戶端最大連接數 print('start listen') while True: try: conn, addr = sock.accept() # 被動接受TCP客戶的連接,等待連接的到來,收不到時會報異常 print('connect by ', addr) conn_list.append(conn) conn.setblocking(False) # 設置非阻塞 except BlockingIOError as e: pass tmp_list = [conn for conn in conn_list] for conn in tmp_list: try: data = conn.recv(1024) # 接收數據1024字節 if data: print('收到的數據是{}'.format(data.decode())) conn.send(data) else: print('close conn',conn) conn.close() conn_list.remove(conn) print('還有客戶端=>',len(conn_list)) except IOError: pass
客戶端
import socket client = socket.socket() client.connect(('127.0.0.1', 9999)) while True: msg = input(">>>") if msg != 'q': client.send(msg.encode()) data = client.recv(1024) print('收到的數據{}'.format(data.decode())) else: client.close() print('close client socket') break
輸出結果
非阻塞IO模型優點:實現了同時服務多個客戶端,能夠在等待任務完成的時間里干其他活了(包括提交其他任務,也就是 “后台” 可以有多個任務在“”同時“”執行)。
但是非阻塞IO模型絕不被推薦
非阻塞IO模型缺點:不停地輪詢recv,占用較多的CPU資源。
對應BlockingIOError的異常處理也是無效的CPU花費 !
如何解決:多路復用IO
多路復用IO
把socket交給操作系統去監控,相當於找個代理人(select), 去收快遞。快遞到了,就通知用戶,用戶自己去取。
阻塞I/O只能阻塞一個I/O操作,而I/O復用模型能夠阻塞多個I/O操作,所以才叫做多路復用
使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,感覺效率更差。
但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,
即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。
epoll是目前Linux上效率最高的IO多路復用技術。
epoll是惰性的事件回調,惰性事件回調是由用戶進程自己調用的,操作系統只起到通知的作用。
epoll實現並發服務器,處理多個客戶端
import socket import selectors # 注冊一個epllo事件 # 1. socket # 2.事件可讀 # 3.回調函數 把一個函數當成變量傳到函數里 def recv_data(conn): data = conn.recv(1024) if data: print('接收的數據是:%s' % data.decode()) conn.send(data) else: e_poll.unregister(conn) conn.close() def acc_conn(p_server): conn, addr = p_server.accept() print('Connected by', addr) # 也有注冊一個epoll e_poll.register(conn,selectors.EVENT_READ,recv_data) CONN_ADDR = ('127.0.0.1', 9999) server = socket.socket() server.bind(CONN_ADDR) server.listen(6) # 表示一個客戶端最大的連接數 # 生成一個epllo選擇器實例 I/O多路復用,監控多個socket連接 e_poll = selectors.EpollSelector() # window沒有epoll使用selectors.DefaultSelector()實現多路復用 e_poll.register(server, selectors.EVENT_READ, acc_conn) # 事件循環 while True: # 事件循環不斷地調用select獲取被激活的socket events = e_poll.select() #print(events) """[(SelectorKey(fileobj= < socket.socket laddr = ('127.0.0.1',9999) >,……data = < function acc_conn at 0xb71b96ec >), 1)] """ for key, mask in events: call_back = key.data #print(key.data) call_back(key.fileobj)
輸出結果
多路復用模型,使用select() 的事件驅動模型只用單線程(進程)執行,占用資源少,不消耗太多 CPU,