Python知識點-IO模型
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) #因為傳送的資料為空了,說明這個連線已經斷了,下次要刪除這個連線否則會報錯 continueconn.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