單程序伺服器-select版-TCP伺服器
1. select 原理
在多路複用的模型中,比較常見的有select模型和epoll模型。這兩個都是系統介面,由作業系統提供。當然,Python的select模組進行了更高階的封裝。
網路通訊被Unix系統抽象為檔案的讀寫,通常是一個裝置,由裝置驅動程式提供,驅動可以知道自身的資料是否可用。支援阻塞操作的裝置驅動通常會實現一組自身的等待佇列,如讀/寫等待佇列用於支援上層(使用者層)所需的block或non-block操作。裝置的檔案的資源如果可用(可讀或者可寫)則會通知程序,反之則會讓程序睡眠,等到資料到來可用的時候,再喚醒程序。
這些裝置的檔案描述符被放在一個數組中,然後select呼叫的時候遍歷這個陣列,如果對於的檔案描述符可讀則會返回改檔案描述符。當遍歷結束之後,如果仍然沒有一個可用裝置檔案描述符,select讓使用者程序則會睡眠,直到等待資源可用的時候在喚醒,遍歷之前那個監視的陣列。每次遍歷都是依次進行判斷的。
2. select 回顯伺服器
使用python的select模組很容易寫出下面一個echo(回顯)伺服器:
import select import socket import sys server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('', 7788)) server.listen(5) inputs = [server, sys.stdin] running = True while True: # 呼叫select函式,阻塞等待 readable, writeable, exceptional = select.select(inputs, [], []) # 資料抵達,迴圈 for sock in readable: # 監聽到有新的連線 if sock == server: conn, addr = server.accept() # select 監聽的socket inputs.append(conn) # 監聽到鍵盤有輸入 elif sock == sys.stdin: cmd = sys.stdin.readline() running = False break # 有資料到達 else: # 讀取客戶端連線傳送的資料 data = sock.recv(1024) if data: sock.send(data) else: # 移除select監聽的socket inputs.remove(sock) sock.close() # 如果檢測到使用者輸入敲擊鍵盤,那麼就退出 if not running: break server.close()
在windows中,使用‘網路除錯助手’,進行連線伺服器即可測試
另外一個伺服器(包含writeList):
#coding=utf-8 import socket import Queue from select import select SERVER_IP = ('', 9999) # 儲存客戶端傳送過來的訊息,將訊息放⼊佇列中 message_queue = {} input_list = [] output_list = [] if __name__ == "__main__": server = socket.socket() server.bind(SERVER_IP) server.listen(10) # 設定為非阻塞 server.setblocking(False) # 初始化將服務端加入監聽列表 input_list.append(server) while True: # 開始 select 監聽,對input_list中的服務端server進行監聽 stdinput, stdoutput, stderr = select(input_list, output_list, input_list) # 迴圈判斷是否有客戶端連線進來,當有客戶端連線進來時select將觸發 for obj in stdinput: # 判斷當前觸發的是不是服務端物件, 當觸發的物件是服務端物件時,說明有新客戶端連線進來 if obj == server: # 接收客戶端的連線, 獲取客戶端物件和客戶端地址資訊 conn, addr = server.accept() print("Client %s connected! "%str(addr)) # 將客戶端物件也加入到監聽的列表中, 當客戶端傳送訊息時 select將觸發 input_list.append(conn) # 為連線的客戶端單獨建立一個訊息佇列,用來儲存客戶端傳送的訊息 message_queue[conn] = Queue.Queue() else: # 由於客戶端連線進來時服務端接收客戶端連線請求,將客戶端加入到了監聽列表中 #(input_list),客戶端傳送訊息將觸發 # 所以判斷是否是客戶端物件觸發 try: recv_data = obj.recv(1024) # 客戶端未斷開 if recv_data: print("received %s from client %s"%(recv_data, str(addr))) # 將收到的訊息放入到各客戶端的訊息佇列中 message_queue[obj].put(recv_data) # 將回復操作放到output列表中,讓select監聽 if obj not in output_list: output_list.append(obj) except ConnectionResetError: # 客戶端斷開連線了,將客戶端的監聽從input列表中移除 input_list.remove(obj) # 移除客戶端物件的訊息佇列 del message_queue[obj] print("\n[input] Client %s disconnected"%str(addr)) # 如果現在沒有客戶端請求,也沒有客戶端傳送訊息時,開始對傳送訊息列表進行處理,是否需要傳送訊息 for sendobj in output_list: try: # 如果訊息佇列中有訊息,從訊息佇列中獲取要傳送的訊息 if not message_queue[sendobj].empty(): # 從該客戶端物件的訊息佇列中獲取要傳送的訊息 send_data = message_queue[sendobj].get() sendobj.send(send_data) else: # 將監聽移除等待下一次客戶端傳送訊息 except ConnectionResetError: # 客戶端連線斷開了 del message_queue[sendobj] output_list.remove(sendobj) print("\n[output] Client %s disconnected"%str(addr))
3. 總結
優點
select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個優點。
缺點
select的一個缺點在於單個程序能夠監視的檔案描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改巨集定義甚至重新編譯核心的方式提升這一限制,但是這樣也會造成效率的降低。
一般來說這個數目和系統記憶體關係很大,具體數目可以cat /proc/sys/fs/filemax察看。32位機預設是1024個。64位機預設是2048.
對socket進行掃描時是依次掃描的,即採用輪詢的方法,效率較低。
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成排程,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。