selector 簡介
selector 是一個實現了IO複用模型的python包,實現了IO多路複用模型的 select、poll 和 epoll 等函數。
它允許程序同時監聽多個文件描述符(例如套接字),並在其中任何一個就緒時進行相應的操作。這樣可以有效地管理併發 I/O 操作,提高程序的性能和資源利用率。
本篇主要講解selector編程示例,以socket編程爲主題,首先分析阻塞IO模型的網絡編程,然後對比selector實現的IO多路複用模型的網絡編程。
阻塞IO模型下的 socket 網絡編程
通過socket實現最簡單的客戶端和服務端通信的功能,阻塞IO模型的特點就是在文件IO或網絡IO時獲取數據的函數會一直阻塞,直到數據到來。
服務端:server.py
import socket
# 創建TCP套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定IP地址和端口號
server_address = ('0.0.0.0', 12346)
server_socket.bind(server_address)
# 監聽連接
server_socket.listen()
# 接受客戶端連接請求
print("服務器已啓動,等待客戶端連接...")
client_socket, client_address = server_socket.accept()
print(f"與客戶端 {client_address} 建立連接")
# 向客戶端發送消息
message = "歡迎連接到服務器!"
client_socket.sendall(message.encode())
while True:
# 從客戶端接收消息
data = client_socket.recv(1024).decode()
print(f"客戶端消息:{data}")
client_socket.sendall(f"服務器收到消息:{data}".encode())
if data == "close":
# 關閉客戶端套接字
client_socket.close()
客戶端:client.py
import socket
# 創建TCP套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 服務器地址和端口號
server_address = ('localhost', 12346)
# 連接服務器
client_socket.connect(server_address)
# 接收服務器的消息
data = client_socket.recv(1024).decode()
print(f"已連接到服務器, 服務器消息:{data}")
while True:
# 向服務器發送消息
message = input()
if message == "end":
break
client_socket.sendall(message.encode())
print(client_socket.recv(1024).decode())
# 關閉客戶端套接字
client_socket.close()
啓動服務端,未啓動客戶端
啓動服務端,代碼執行到client_socket, client_address = server_socket.accept()
暫停,accept 是一個阻塞函數。
函數說明:
client_socket, client_address = server_socket.accept()
阻塞接口,等待客戶端的連接,沒有客戶端連接時阻塞等待,有客戶端連接時返回新的聊天socket,用於後續發送和接收消息
繼續啓動客戶端
啓動客戶端之後,客戶端連接服務端,服務端代碼執行到data = client_socket.recv(1024).decode()
, recv 是一個阻塞函數。
函數說明:
data = client_socket.recv(1024).decode()
阻塞接口,等待緩衝區有消息到來。沒有消息時阻塞等待,有消息到來返回消息內容
客戶端發送消息
客戶端發送消息,data = client_socket.recv(1024).decode()
收到消息,從網絡協議棧中獲取消息,並返回。繼續whie True 循環的下一輪循環,阻塞在相同地方。
IO多路複用模型下的socket 網絡編程 selector
selector是實現IO多路複用模型的模塊,首先回憶一下IO多路複用。
IO多路複用是通過select
、poll
、epoll
監聽文件句柄,當有文件句柄處於就緒狀態,就通知對應的應用程序處理。
服務端:server.py
import selectors
import socket
# 選擇一個當前平臺最優的IO多路複用模型
sel = selectors.DefaultSelector()
def accept(server_socket):
conn, addr = server_socket.accept()
print(f"與客戶端 {addr} 建立連接")
conn.setblocking(False) # 設定非阻塞
# 註冊conn對象到selector中,當conn可讀時,返回conn和回調函數read
sel.register(conn, selectors.EVENT_READ, read)
def read(conn):
data = conn.recv(1024).decode('utf-8')
print(f"客戶端消息:{data}")
if data == "close":
sel.unregister(conn)
conn.close()
else:
conn.sendall(f"服務器收到消息:{data}".encode())
if __name__ == "__main__":
sock = socket.socket()
sock.bind(("0.0.0.0", 9999))
sock.listen()
sock.setblocking(False) # 設置sock非阻塞
# 將sock註冊到selector中,當sock可讀時,返回sock和回調函數accept
sel.register(sock, selectors.EVENT_READ, accept)
print("創建事件循環")
while True:
events = sel.select() # 阻塞運行,有就緒的事件返回就緒事件列表
for key, _ in events:
print(key)
# key.data: 註冊的回調函數 key.fileobj: 註冊的文件句柄
callback = key.data # 註冊的回調函數
callback(key.fileobj)
主要的函數:
一、自動選擇文件IO模型selectors.DefaultSelector()
選擇一個當前平臺最優的IO模型,一般來說是epoll或kqueue。存在的可選項包括:
- SelectSelector
- PollSelector
- EpollSelector
- DevpollSelector
- KqueueSelector
DefaultSelector 是一個指向當前平臺上可用的最高效實現的別名,當選擇epoll時,可以認爲 sel = EpollSelector
。
返回:一個select對象
二、文件註冊 sel.register(sock, selectors.EVENT_READ, accept)
函數原型:
register(fileobj, events, data=None)
註冊一個用於選擇的文件對象,在其上監視 I/O 事件。
fileobj 是要監視的文件對象。 它可以是整數形式的文件描述符或者具有 fileno() 方法的對象。
events 是要監視的事件的位掩碼。
data 是一個任意對象或變量。
返回:
這將返回一個新的 SelectorKey 實例,實例具體內容見下一個函數的key
三、獲取就緒文件events = sel.select()
函數原型:
select(timeout=None)
可用於遍歷獲取狀態變爲就緒註冊的文件,如果設置超時時間則可能會拋出超時異常。
返回:一個(key, events)的元組,
-
key: 一個SelectorKey類的實例,包括
fileobj: 已註冊的文件對象。 fd: 下層的文件描述符 events: 必須在此文件對象上被等待的事件。
-
events:文件句柄可讀還是可寫的標識。爲EVENT_READ或EVENT_WRITE,或者二者的組合
client.py
import socket
client = socket.socket(family=socket. AF_INET, type=socket.SOCK_STREAM)
host = socket.gethostname()
client.connect((host, 9999))
while True:
data = input("客戶端發送數據:").strip()
client.send(data.encode())
if data == "end" or data == "":
client.close()
break
print(client.recv(1024).decode("utf-8"))
服務端啓動,客戶端未啓動
if __name__ == "__main__":
sock = socket.socket()
sock.bind(("0.0.0.0", 9999))
sock.listen()
sock.setblocking(False) # 設置sock非阻塞
# 將sock註冊到selector中,當sock可讀時,返回sock和回調函數accept
sel.register(sock, selectors.EVENT_READ, accept)
print("創建事件循環")
while True:
events = sel.select() # 阻塞運行,有就緒的事件返回就緒事件列表
服務端啓動,代碼完成的功能包括:
- 創建一個socket,並綁定IP,監聽端口
- 設置socket爲非阻塞,否則超時會報錯
- 將socket註冊到 selector 中,等待socket就緒,綁定就緒之後的回調函數accept
- 進入while True循環,訪問select返回的就緒列表。這個阻塞函數,沒有文件讀寫就緒就會阻塞。
繼續啓動客戶端
啓動一個客戶端,客戶端連接到服務端,socket文件句柄有連接請求,select返回可讀狀態的socket。返回的events是一個列表,當中只有一個就緒的文件句柄。
key.data拿到註冊的回調函數也就是accept函數,key.fileobj拿到文件句柄的socket對象。調用accept函數,傳入socket對象。
def accept(server_socket):
conn, addr = server_socket.accept()
print(f"與客戶端 {addr} 建立連接")
conn.setblocking(False) # 設定非阻塞
# 註冊conn對象到selector中,當conn可讀時,返回conn和回調函數read
sel.register(conn, selectors.EVENT_READ, read)
accept中先通過accept接收連接,返回通信使用的文件句柄conn,然後設置conn爲非阻塞,最後將conn阻塞到selector中,傳入回調函數read。等conn文件句柄可讀時,就表示有數據發送過來,就可以調用read函數讀取內容了。
客戶端發送消息
客戶端發送消息時,selector會返回可讀狀態的conn文件句柄,從返回對象中獲取回調函數,調用回調函數read,傳入文件句柄。
def read(conn):
data = conn.recv(1024).decode('utf-8')
print(f"客戶端消息:{data}")
if data == "close":
sel.unregister(conn)
conn.close()
else:
conn.sendall(f"服務器收到消息:{data}".encode())
在read函數中,首先獲取了網絡協議棧中的消息內容,然後判斷消息是否爲關閉連接。如果不是則發送一條消息給對方。
整個基於IO多路複用模型的網絡編程流程就是這樣。
selector 原理分析
selector是操作系統的IO多路複用模型的一種實現。通過select
、poll
、epoll
監聽文件句柄,在文件句柄可讀的狀態下,會返回就緒的文件句柄。
返回就緒狀態文件句柄
sel = selectors.DefaultSelector()
while True:
events = sel.select()
循環中訪問sel.select()
就是監聽文件句柄狀態的函數,一個阻塞函數。應用程序調用該函數後會等待,直到有數據到來,數據從設備發送到內核空間,在socket編程中就是數據流從網卡到內核空間中。當數據到達內核空間中,該函數返回文件句柄相關的內容。
數據拷貝
當文件句柄就緒之後,就可以從文件句柄裏讀取數據了。
在selector中相關的函數是
conn, addr = server_socket.accept()
data = conn.recv(1024).decode('utf-8')
總結
一個完整的IO多路複用模型就是由兩個部分組成,分別是
- 返回就緒狀態文件句柄
- 數據拷貝
asyncio 和 selector 的關係
selectors 則是 asyncio 的底層實現之一。asyncio實現的協程是由事件循環
+ 任務
組成的,而selector就是事件循環的重要依賴模塊。
asyncio 使用了 selectors 模塊來實現底層的併發 I/O 操作。通過將 selectors 的功能封裝爲 asyncio 提供的事件循環(Event Loop)和其他協程相關的工具。
回顧一下事件循環的機制
任務列表 = [ 任務1, 任務2, 任務3,... ]
while True:
可執行的任務列表,已完成的任務列表 = 去任務列表中檢查所有的任務,將'可執行'和'已完成'的任務返回
for 就緒任務 in 已準備就緒的任務列表:
執行已就緒的任務
for 已完成的任務 in 已完成的任務列表:
在任務列表中移除 已完成的任務
如果 任務列表 中的任務都已完成,則終止循環
事件循環就是一個while True的循環,循環中做的事情有三個:
- 獲取就緒狀態的任務和已完成的任務
- 執行就緒狀態的任務
- 移除已完成的任務
那麼selector的功能在事件循環中的功能就非常明顯了,就是負責返回IO相關的就緒任務。
asyncio 庫使用了底層的 selectors 模塊來監聽和管理文件描述符的狀態變化,並在合適的時候將控制權交給其他的協程。這樣可以實現非阻塞的 I/O 操作,並支持高併發和並行執行。
selectors 提供了底層的 I/O 多路複用機制,而 asyncio 在其之上提供了更高級的異步編程框架。
附錄asyncio模塊事件循環核心模塊
def run_forever(self):
"""Run until stop() is called."""
self._check_closed()
self._check_running()
self._set_coroutine_origin_tracking(self._debug)
old_agen_hooks = sys.get_asyncgen_hooks()
try:
self._thread_id = threading.get_ident()
sys.set_asyncgen_hooks(firstiter=self._asyncgen_firstiter_hook,
finalizer=self._asyncgen_finalizer_hook)
events._set_running_loop(self)
while True:
self._run_once()
if self._stopping:
break
finally:
self._stopping = False
self._thread_id = None
events._set_running_loop(None)
self._set_coroutine_origin_tracking(False)
sys.set_asyncgen_hooks(*old_agen_hooks)
連載一系列關於python異步編程的文章。包括同異步框架性能對比、異步事情驅動原理等。歡迎關注微信公衆號第一時間接收推送的文章。