在SocketServer模塊的學習中,咱們瞭解了多線程和多進程簡單Server的實現,使用多線程、多進程技術的服務端爲每個新的client鏈接建立一個新的進/線程,當client數量較多時,這種技術也將帶來巨大的開銷,服務器的內存畢竟是有限的,而客戶的量級可能很是龐大,所以爲每一個客戶端鏈接建立單獨的進/線程可能並不實際。python
另外一種提高服務器性能的網絡編程模式是事件驅動的(異步)編程,這裏所說的「事件」一般是指:客戶端鏈接到來、套接字有可讀數據、套接字可寫等。服務器時刻處在一個時刻等待這些事件的循環中,一旦這些常見的某類事件發生,服務端分別進行必定的響應,而後繼續等待下一個事件的發生。編程
Python 對事件驅動的網絡編程提供的支持包括:服務器
select 模塊 —— 低層次 select, epoll 等異步編程機制;網絡
asyncore, asynchat 模塊 —— 高層次的異步編程機制,在Python 3中合併爲 asyncio 模塊;多線程
Twisted 框架 —— Twisted 是一個功能強大的Python事件驅動編程框架。app
本文將會學習 select 模塊提供的 select、epoll 機制。框架
Select 編程異步
select 的中文含義是」選擇「,select機制也如其名,監聽一些 server 關心的套接字、文件等對象,關注他們是否可讀、可寫、發生異常等事件。一旦出現某個 select 關注的事件,select 會對相應的套接字或文件進行特定的處理,這就是 select 機制最主要的功能。socket
select 機制能夠只使用一個進程/線程來處理多個 socket 或其餘對象,所以又被稱爲I/O複用。async
關於 select 機制的進程阻塞形式,與普通的套接字略有不一樣。socket 對象可能阻塞在 accept(), recvfrom()等方法上,以 recvfrom() 方法爲例,當執行到 socket.recvfrom() 這一句時,就會調用一個系統調用詢問內核:client / server 發來的數據包準備好了沒?此時從進程空間切換到內核地址空間,內核可能須要等數據包徹底到達,而後將數據複製到程序的地址空間後,recvfrom() 纔會返回,接下來進程繼續執行,對讀取到的數據進行必要的處理。
而使用 select 函數編程時,一樣針對上面的 recvfrom() 方法,進程會阻塞在 select() 調用上,等待出現一個或多個套接字對象知足可讀事件,當內核將數據準備好後,select() 返回某個套接字對象可讀這一條件,隨後再調用 recvfrom() 將數據包從內核複製到進程地址空間。
因此可見,若是僅僅從單個套接字的處理來看,select() 反倒性能更低,由於 select 機制使用兩個系統調用。但 select 機制的優點就在於它能夠同時等待多個 fd 就緒,而當某個 fd 發生知足咱們關心的事件時,就對它執行特定的操做。
調用 select 模塊提供的 select 函數能夠實現 Select 編程:
select(inputs, outputs, excepts, timeout=None)
參數說明:
inputs, outputs, excepts 是分別由等待輸入事件、輸出事件和異常條件的 socket 對象組成的列表;
timeout——數字或者None,上一篇文章中已經介紹過 Python套接字對象的超時行爲,這裏select與套接字的超時行爲相似:若是 timeout 是 None,則 select 在阻塞調用上會一直等待直到事件發生;若是 timeout 是一個非 0 數字,則 select 最多等待 timeout 秒;若是 timeout 是0,則 select 遇到阻塞的調用時會當即返回,一刻也不等待。
返回值:
select() 返回一個 (i, o, e) 形式的三元組,其中i, o, e 分別都是列表,列表 i 中的每一個元素都收到了 input 事件,列表 o 中的每一個元素都收到了 output 事件,列表 e 中的每一個元素則發生了異常。
Select 編程的特色是 select() 參數中的 inputs, outputs, excepts 能夠不止 socket 對象,而只要是支持無參數調用 fileno() 方法,返回相應 fd 的對象都可,好比 SocketServer 模塊中提供的幾種 server 類型就支持這一方法,因此在 select() 參數的某個列表中,就能夠包含這些 Server 類型的實例。在 Unix 平臺上,select 機制也支持並不對應於 socket 對象的文件描述字。
例:
使用 select 網絡編程模型實現一個簡單的回顯服務器
#!-*-encoding:utf-8-*- import socket import select sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 4424)) sock.listen(10) ins = [sock, ] ous = [] # socket -> data to send data = {} # socket -> client address, which is (host, port) tuple adrs = {} try: print("Select Server Start Working!") while True: i, o, e = select.select(ins, ous, []) for x in i: # 處理髮生 input 事件的套接字 if x is sock: # 監聽套接字上發生input事件,說明有新的鏈接 newSock, addr = sock.accept() print("Connected from: ", addr) ins.append(newSock) adrs[newSock] = addr else: # 鏈接套接字上發生 input 事件說明有數據可讀,或者是client端斷開鏈接 newData = x.recv(1024) if newData: # 有新的數據到來,此時將發給該 client 的響應入隊 print("%d bytes from %s" %(len(newData), adrs[x])) data[x] = data.get(x, "") + newData if x not in ous: ous.append(x) else: # 鏈接套接字上發生 input 事件,若是不是有新數據可讀,說明是client斷開鏈接 print("Disconnected from: ", adrs[x]) del adrs[x] try: ous.remove(x) except ValueError: pass x.close() ins.remove(x) for x in o: # 處理髮生 output 事件的套接字 # 有 output 事件,說明此時可寫 tosend = data.get(x) if tosend: nsent = x.send(tosend) print("%d bytes to %s" %(nsent, adrs[x])) tosend = tosend[nsent:] if tosend: print("%d bytes remain for %s" %(len(tosend), adrs[x])) data[x] = tosend else: try: del data[x] except KeyError: pass ous.remove(x) print("No data currently remain for:", adrs[x]) finally: sock.close()