Python的select()方法直接調用操做系統的IO接口,它監控sockets,open files, and pipes(全部帶fileno()方法的文件句柄)什麼時候變成readable 和writeable, 或者通訊錯誤,select()使得同時監控多個鏈接變的簡單,而且這比寫一個長循環來等待和監控多客戶端鏈接要高效,由於select直接經過操做系統提供的C的網絡接口進行操做,而不是經過Python的解釋器。python
注意:Using Python’s file objects with select() works for Unix, but is not supported under Windows.緩存
接下來經過socket server例子要以瞭解select 是如何經過單進程實現同時處理多個非阻塞的socket鏈接的 服務器
2.1 socket server 開始監聽網絡
import select import socket import queue import sys # Create a TCP/IP socket server = socket.socket() # set noblocking server.setblocking(False) # Bind the socket to the port server_address = ('localhost', 9999) print(sys.stderr, 'starting up on %s port %s' % server_address) server.bind(server_address) # Listen for incoming connections server.listen()
2.2 3個通訊列表app
select()方法接收並監控3個通訊列表, 第一個是全部的輸入的data,就是指外部發過來的數據,第2個是監控和接收全部要發出去的data(outgoing data),第3個監控錯誤信息,接下來咱們須要建立2個列表來包含輸入和輸出信息來傳給select()。 socket
import select import socket import queue import sys # Create a TCP/IP socket server = socket.socket() # set noblocking server.setblocking(False) # Bind the socket to the port server_address = ('localhost', 9999) print(sys.stderr, 'starting up on %s port %s' % server_address) server.bind(server_address) # Listen for incoming connections server.listen() # 全部鏈接進來的對象都放在inputs inputs = [server, ] # 本身也要監控,由於server自己也是個對象 # 須要發送數據的對象 outputs = []
2.3 添加一個隊列oop
全部客戶端的進來的鏈接和數據將會被server的主循環程序放在上面的list中處理,咱們如今的server端須要等待鏈接可寫(writable)以後才能過來,而後接收數據並返回(所以不是在接收到數據以後就馬上返回),由於每一個鏈接要把輸入或輸出的數據先緩存到queue裏,而後再由select取出來再發出去。this
Connections are added to and removed from these lists by the server main loop. Since this version of the server is going to wait for a socket to become writable before sending any data (instead of immediately sending the reply), each output connection needs a queue to act as a buffer for the data to be sent through it. spa
# 對外發送數據的隊列,記錄到字典中 message_queues = {}
2.4 主循環 操作系統
while True: readable, writable, exceptional = select.select(inputs, outputs, inputs) # 若是沒有任何fd就緒,那程序就會一直阻塞在這裏
當你把inputs,outputs,exceptional(這裏跟inputs共用)傳給select()後,它返回3個新的list,咱們上面將他們分別賦值爲readable,writable,exceptional, 全部在readable list中的socket鏈接表明有數據可接收(recv),全部在writable list中的存放着你能夠對其進行發送(send)操做的socket鏈接,當鏈接通訊出現error時會把error寫到exceptional列表中。
2.5 Readable list
Readable list 中的socket 能夠有3種可能狀態,第一種是若是這個socket是main "server" socket,它負責監聽客戶端的鏈接,若是這個main server socket出如今readable裏,那表明這是server端已經ready來接收一個新的鏈接進來了,爲了讓這個main server能同時處理多個鏈接,在下面的代碼裏,咱們把這個main server的socket設置爲非阻塞模式。
for s in readable: # 每個s就是有個socket if s is server: # 別忘記,上面咱們server本身也當作一個fd放在了inputs列表裏,傳給了select,若是這個s是server,表明server這個fd就緒了, # 就是有活動了, 什麼狀況下它纔有活動? 固然 是有新鏈接進來的時候 # 新鏈接進來了,接受這個鏈接 conn, client_addr = s.accept() print("new connection from", client_addr) conn.setblocking(0) inputs.append(conn) # 爲了避免阻塞整個程序,咱們不會馬上在這裏開始接收客戶端發來的數據, 把它放到inputs裏, 下一次loop時,這個新鏈接 # 就會被交給select去監聽,若是這個鏈接的客戶端發來了數據 ,那這個鏈接的fd在server端就會變成就續的,select就會把這個鏈接返回, # 返回到readable 列表裏,而後你就能夠loop readable列表,取出這個鏈接,開始接收數據了, 下面就是這麼幹的 message_queues[conn] = queue.Queue() # 接收到客戶端的數據後,不馬上返回 ,暫存在隊列裏,之後發送
第二種狀況是這個socket是已經創建了的鏈接,它把數據發了過來,這個時候你就能夠經過recv()來接收它發過來的數據,而後把接收到的數據放到queue裏,這樣你就能夠把接收到的數據再傳回給客戶端了。
else: # s不是server的話,那就只能是一個 與客戶端創建的鏈接的fd了 # 客戶端的數據過來了,在這接收 data = s.recv(1024) if data: print('received [%s] from %s' % (data, s.getpeername()[0])) message_queues[s].put(data) # 收到的數據先放到queue裏,一會返回給客戶端 if s not in outputs: outputs.append(s) # 爲了避免影響處理與其它客戶端的鏈接 , 這裏不馬上返回數據給客戶端
第三種狀況就是這個客戶端已經斷開了,因此你再經過recv()接收到的數據就爲空了,因此這個時候你就能夠把這個跟客戶端的鏈接關閉了。
else: # 若是收不到data表明什麼呢? 表明客戶端斷開了 print("client [%s] closed", s) if s in outputs: # 既然客戶端都斷開了,我就不用再給它返回數據了, # 因此這時候若是這個客戶端的鏈接對象還在outputs列表中,就把它刪掉 outputs.remove(s) inputs.remove(s) # 這個鏈接必然在inputs中,也刪掉 s.close() # 關閉的鏈接在隊列中也刪除 del message_queues[s]
2.6 writable list
對於writable list中的socket,也有幾種狀態,若是這個客戶端鏈接在跟它對應的queue裏有數據,就把這個數據取出來再發回給這個客戶端,不然就把這個鏈接從output list中移除,這樣下一次循環select()調用時檢測到outputs list中沒有這個鏈接,那就會認爲這個鏈接還處於非活動狀態
for s in writable: try: next_msg = message_queues[s].get_nowait() except queue.Empty: # 沒有數據了,該鏈接對象隊列爲空,中止檢測 print('output queue for [%s] is empty' % s.getpeername()[0]) outputs.remove(s) else: print('send %s to %s' % (next_msg, s.getpeername()[0])) s.send(next_msg)
2.7 exceptional condition
最後,若是在跟某個socket鏈接通訊過程當中出了錯誤,就把這個鏈接對象在inputs\outputs\message_queue中都刪除,再把鏈接關閉掉
for s in exceptional: print('handling exceptional condition for', s.getpeername()[0]) # 從inputs中刪除 inputs.remove(s) if s in outputs: outputs.remove(s) s.close() # 刪除隊列 del message_queues[s]
注: getpeername() / getsocketname
getpeername能夠得到服務器的地址信息和端口號,正好和getsockname得到本機地址信息和端口號徹底相反
select server
# -*- coding: UTF-8 -*- import select import socket import queue import sys # Create a TCP/IP socket server = socket.socket() # set noblocking server.setblocking(False) # Bind the socket to the port server_address = ('localhost', 9999) print(sys.stderr, 'starting up on %s port %s' % server_address) server.bind(server_address) # Listen for incoming connections server.listen() # 全部鏈接進來的對象都放在inputs inputs = [server, ] # 本身也要監控,由於server自己也是個對象 # 須要發送數據的對象 outputs = [] # 對外發送數據的隊列,記錄到字典中 message_queues = {} while True: readable, writable, exceptional = select.select(inputs, outputs, inputs) # 若是沒有任何fd就緒,那程序就會一直阻塞在這裏 for s in readable: # 每個s就是有個socket if s is server: # 別忘記,上面咱們server本身也當作一個fd放在了inputs列表裏,傳給了select,若是這個s是server,表明server這個fd就緒了, # 就是有活動了, 什麼狀況下它纔有活動? 固然 是有新鏈接進來的時候 # 新鏈接進來了,接受這個鏈接 conn, client_addr = s.accept() print("new connection from", client_addr) conn.setblocking(0) inputs.append(conn) # 爲了避免阻塞整個程序,咱們不會馬上在這裏開始接收客戶端發來的數據, 把它放到inputs裏, 下一次loop時,這個新鏈接 # 就會被交給select去監聽,若是這個鏈接的客戶端發來了數據 ,那這個鏈接的fd在server端就會變成就續的,select就會把這個鏈接返回, # 返回到readable 列表裏,而後你就能夠loop readable列表,取出這個鏈接,開始接收數據了, 下面就是這麼幹的 message_queues[conn] = queue.Queue() # 接收到客戶端的數據後,不馬上返回 ,暫存在隊列裏,之後發送 else: # s不是server的話,那就只能是一個 與客戶端創建的鏈接的fd了 # 客戶端的數據過來了,在這接收 data = s.recv(1024) if data: print('received [%s] from %s' % (data, s.getpeername()[0])) message_queues[s].put(data) # 收到的數據先放到queue裏,一會返回給客戶端 if s not in outputs: outputs.append(s) # 爲了避免影響處理與其它客戶端的鏈接 , 這裏不馬上返回數據給客戶端 else: # 若是收不到data表明什麼呢? 表明客戶端斷開了 print("client [%s] closed", s) if s in outputs: # 既然客戶端都斷開了,我就不用再給它返回數據了, # 因此這時候若是這個客戶端的鏈接對象還在outputs列表中,就把它刪掉 outputs.remove(s) inputs.remove(s) # 這個鏈接必然在inputs中,也刪掉 s.close() # 關閉的鏈接在隊列中也刪除 del message_queues[s] for s in writable: try: next_msg = message_queues[s].get_nowait() except queue.Empty: # 沒有數據了,該鏈接對象隊列爲空,中止檢測 print('output queue for [%s] is empty' % s.getpeername()[0]) outputs.remove(s) else: print('send %s to %s' % (next_msg, s.getpeername()[0])) s.send(next_msg) for s in exceptional: print('handling exceptional condition for', s.getpeername()[0]) # 從inputs中刪除 inputs.remove(s) if s in outputs: outputs.remove(s) s.close() # 刪除隊列 del message_queues[s]
client
# -*- coding: UTF-8 -*- import socket HOST = 'localhost' # The remote host PORT = 9999 # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) while True: msg = bytes(input(">>:"), encoding="utf8") s.sendall(msg) data = s.recv(1024) # print(data) print('Received', repr(data))