1. 程式人生 > >Python知識點-IO模型

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