在簡明網絡I/O模型文章能夠知道經常使用的IO
模型。其中同步模型中,使用多路複用I/O
能夠提升服務器的性能。node
在多路複用的模型中,比較經常使用的有select
模型和poll
模型。這兩個都是系統接口,由操做系統提供。固然,Python
的select
模塊進行了更高級的封裝。select
與poll
的底層原理都差很少。下面就介紹select
。python
網絡通訊被Unix
系統抽象爲文件的讀寫,一般是一個設備,由設備驅動程序提供,驅動能夠知道自身的數據是否可用。支持阻塞操做的設備驅動一般會實現一組自身的等待隊列,如讀/寫等待隊列用於支持上層(用戶層)所需的block
或non-block
操做。設備的文件的資源若是可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。nginx
這些設備的文件描述符被放在一個數組中,而後select
調用的時候遍歷這個數組,若是對於的文件描述符可讀則會返回改文件描述符。當遍歷結束以後,若是仍然沒有一個可用設備文件描述符,select
讓用戶進程則會睡眠,直到等待資源可用的時候在喚醒,遍歷以前那個監視的數組。每次遍歷都是線性的。數組
select
涉及系統調用和操做系統相關的知識,所以單從字面上理解其原理仍是比較乏味。用代碼來演示最好不過了。使用python
的select
模塊很容易寫出下面一個回顯服務器:服務器
1網絡 2app 3curl 4異步 5socket 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import select import socket import sys
HOST = 'localhost' PORT = 5000 BUFFER_SIZE = 1024
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen(5)
inputs = [server, sys.stdin] running = True
while True: try: # 調用 select 函數,阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) except select.error, e: break
# 數據抵達,循環 for sock in readable: # 創建鏈接 if sock == server: conn, addr = server.accept() # select 監聽的socket inputs.append(conn) elif sock == sys.stdin: junk = sys.stdin.readlines() running = False else: try: # 讀取客戶端鏈接發送的數據 data = sock.recv(BUFFER_SIZE) if data: sock.send(data) if data.endswith('\r\n\r\n'): # 移除select監聽的socket inputs.remove(sock) sock.close() else: # 移除select監聽的socket inputs.remove(sock) sock.close() except socket.error, e: inputs.remove(sock)
server.close() |
運行上述代碼,使用curl
訪問http://localhost:5000
,便可看命令行返回請求的HTTP request
信息。
下面詳細解析上述代碼的原理。
1 2 3 |
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen(5) |
上述代碼使用socket
初始化一個TCP
套接字,並綁定主機地址和端口,而後設置服務器監聽。
1 |
inputs = [server, sys.stdin] |
這裏定義了一個須要select
監聽的列表,列表裏面是須要監聽的對象(等於系統監聽的文件描述符)。這裏監聽socket
套接字和用戶的輸入。
而後代碼進行一個服務器無線循環。
1 2 3 4 5 |
try: # 調用 select 函數,阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) except select.error, e: break |
調用了select
函數,開始循環遍歷監聽傳入的列表inputs
。若是沒有curl
服務器,此時沒有創建tcp
客戶端鏈接,所以改列表內的對象都是數據資源不可用。所以select
阻塞不返回。
客戶端輸入curl http://localhost:5000
以後,一個套接字通訊開始,此時input
中的第一個對象server
由不可用變成可用。所以select
函數調用返回,此時的readable
有一個套接字對象(文件描述符可讀)。
1 2 3 4 5 6 |
for sock in readable: # 創建鏈接 if sock == server: conn, addr = server.accept() # select 監聽的socket inputs.append(conn) |
select
返回以後,接下來遍歷可讀的文件對象,此時的可讀中只有一個套接字鏈接,調用套接字的accept()
方法創建TCP
三次握手的鏈接,而後把該鏈接對象追加到inputs
監視列表中,表示咱們要監視該鏈接是否有數據IO
操做。
因爲此時readable
只有一個可用的對象,所以遍歷結束。再回到主循環,再次調用select
,此時調用的時候,不只會遍歷監視是否有新的鏈接須要創建,仍是監視剛纔追加的鏈接。若是curl
的數據到了,select
再返回到readable
,此時在進行for
循環。若是沒有新的套接字,將會執行下面的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
try: # 讀取客戶端鏈接發送的數據 data = sock.recv(BUFFER_SIZE) if data: sock.send(data) if data.endswith('rnrn'): # 移除select監聽的socket inputs.remove(sock) sock.close() else: # 移除select監聽的socket inputs.remove(sock) sock.close() except socket.error, e: inputs.remove(sock) |
經過套接字鏈接調用recv
函數,獲取客戶端發送的數據,當數據傳輸完畢,再把監視的inputs
列表中除去該鏈接。而後關閉鏈接。
整個網絡交互過程就是如此,固然這裏若是用戶在命令行中輸入中斷,inputs
列表中監視的sys.stdin
也會讓select
返回,最後也會執行下面的代碼:
1 2 3 |
elif sock == sys.stdin: junk = sys.stdin.readlines() running = False |
有人可能有疑問,在程序處理sock
鏈接的是時候,假設又輸入了curl
對服務器請求,將會怎麼辦?此時毫無疑問,inputs
裏面的server
套接字會變成可用。等如今的for
循環處理完畢,此時select
調用就會返回server
。若是inputs
裏面還有上一個過程的conn
鏈接,那麼也會循環遍歷inputs
的時候,再一次針對新的套接字accept
到inputs
列表進行監視,而後繼續循環處理以前的conn
鏈接。如此有條不紊的進行,直到for
循環結束,進入主循環調用select
。
任什麼時候候,inputs
監聽的對象有數據,下一次調用select
的時候,就會繁返回readable
,只要返回,就會對readable
進行for
循環,直到for
循環結束在進行下一次select
。
主要注意,套接字創建鏈接是一次IO
,鏈接的數據抵達也是一次IO
。
儘管select
用起來挺爽,跨平臺的特性。可是select
仍是存在一些問題。select
須要遍歷監視的文件描述符,而且這個描述符的數組還有最大的限制。隨着文件描述符數量的增加,用戶態和內核的地址空間的複製所引起的開銷也會線性增加。即便監視的文件描述符長時間不活躍了,select
仍是會線性掃描。
爲了解決這些問題,操做系統又提供了poll
方案,可是poll
的模型和select
大體至關,只是改變了一些限制。目前Linux
最早進的方式是epoll
模型。
許多高性能的軟件如nginx
, nodejs
都是基於epoll
進行的異步。