http://python.jobbole.com/84058/
多路複用I/O
在簡明網絡I/O模型文章可以知道常用的IO
模型。其中同步模型中,使用多路複用I/O
可以提高服務器的性能。
在多路複用的模型中,比較常用的有select
模型和poll
模型。這兩個都是系統接口,由操作系統提供。當然,Python
的select
模塊進行了更高級的封裝。select
與poll
的底層原理都差不多。下面就介紹select
。
select 原理
網絡通信被Unix
系統抽象爲文件的讀寫,通常是一個設備,由設備驅動程序提供,驅動可以知道自身的數據是否可用。支持阻塞操作的設備驅動通常會實現一組自身的等待隊列,如讀/寫等待隊列用於支持上層(用戶層)所需的block
或non-block
操作。設備的文件的資源如果可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。
這些設備的文件描述符被放在一個數組中,然後select
調用的時候遍歷這個數組,如果對於的文件描述符可讀則會返回改文件描述符。當遍歷結束之後,如果仍然沒有一個可用設備文件描述符,select
讓用戶進程則會睡眠,直到等待資源可用的時候在喚醒,遍歷之前那個監視的數組。每次遍歷都是線性的。
select 回顯服務器
select
涉及系統調用和操作系統相關的知識,因此單從字面上理解其原理還是比較乏味。用代碼來演示最好不過了。使用python
的select
模塊很容易寫出下面一個回顯服務器:
1 2 3 4 5 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
需要遍歷監視的文件描述符,並且這個描述符的數組還有最大的限制。隨着文件描述符數量的增長,用戶態和內核的地址空間的複製所引發的開銷也會線性增長。即使監視的文件描述符長時間不活躍了,select
還是會線性掃描。
爲了解決這些問題,操作系統又提供了poll
方案,但是poll
的模型和select
大致相當,只是改變了一些限制。目前Linux
最先進的方式是epoll
模型。
許多高性能的軟件如nginx
, nodejs
都是基於epoll
進行的異步。