概述
本文透過使用select改寫之前的伺服器程式透過監控多個套接字描述符來實現併發連線並加入了一些機制讓程式更加健壯,不過我們所有的實驗都是建立在單詞傳送資料不會超過1024位元組,如果超過你需要做特殊處理。
程式碼例項
描述符就緒條件
套接字準備好讀
以下條件滿足之一則套接字準備好讀
- 套接字接收緩衝區中的資料長度大於0
- 該連線讀半部關閉,也就是本端的套接字收到FIN,也就是對方已經傳送完資料並執行了四次斷開的第一次傳送FIN,這時候本端如果繼續嘗試讀取將會得到一個EOF也就是得到空。
- 套接字是一個監聽套接字且已經完成的連線數量大於0,也就是如果監聽套接字可讀正面有新連線進來那麼在連線套接字上條用accept將不會阻塞
- 套接字產生錯誤需要進行處理,讀取這樣的套接字將返回一個錯誤
套接字準備好寫
以下條件滿足之一則套接字準備好寫
- 套接字傳送緩衝區可以空間大於等於套接字傳送緩衝區最低水位,也就是傳送緩衝區沒有空餘空間或者空餘空間不足以容納一個TCP分組(1460-40=1420)。如果不夠它就會等。當可以容納了就表示套接字可寫,這個可寫是程式把資料傳送到套接字傳送緩衝區。
- 該連線寫半部關閉,
- 使用非阻塞式connect的套接字已建立連線或者connect已經失敗
- 有一個錯誤套接字待處理
伺服器端程式碼
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Author: rex.cheny 4 # E-mail: rex.cheny@outlook.com 5 6 import socket 7 import select 8 9 10 def echoStr(readAbledSockFD, rList): 11 try: 12 bytesData = readAbledSockFD.recv(1024) 13 data = bytesData.decode(encoding="utf-8") 14 if data: 15 print("收到客戶端 ", readAbledSockFD.getpeername(), " 訊息:", data) 16 if data.upper() == "BYE": 17 print("客戶端 ", readAbledSockFD.getpeername(), " 主動斷開連線。") 18 rList.remove(readAbledSockFD) 19 readAbledSockFD.close() 20 else: 21 readAbledSockFD.send(data.encode(encoding="utf-8")) 22 else: 23 """ 24 如果客戶端程式意外終止,那麼select將返回,因為該連線套接字收到FIN,所以readAbledSockFD讀取的內容是''就是空,資料長度是0 25 也就是你試圖讀取一個收到FIN的套接字會出現這種情況,通常的錯誤資訊是 "server terminated prematurely" 26 """ 27 print("客戶端 ", readAbledSockFD.getsockname(), " 意外中斷連線。") 28 rList.remove(readAbledSockFD) 29 readAbledSockFD.close() 30 except Exception as err: 31 """ 32 這裡如果丟擲異常通常是因為當連線套接字收到RST之後呼叫 recv()函式產生的 "Connection reset by peer" 錯誤, 33 為什麼套接字會收到RST,通常是向一個收到FIN的套接字執行寫入操作導致的。 34 """ 35 print("客戶端 ", readAbledSockFD.getsockname(), " 意外中斷連線。") 36 rList.remove(readAbledSockFD) 37 readAbledSockFD.close() 38 39 40 def main(): 41 sockFd = socket.socket() 42 sockFd.bind(("", 5556)) 43 sockFd.listen(5) 44 45 # 這裡為什麼要把這個監聽套接字放入可讀列表中呢?伺服器監聽套接字描述符如果有新連線進來那麼該描述符可讀 46 rList = [sockFd] 47 wList = [] 48 eList = [] 49 50 print("等待客戶端連線......") 51 while True: 52 """ 53 select(),有4個引數,前三個必須也就是感興趣的描述符,第四個是超時時間 54 第一個引數:可讀描述符列表 55 第二個引數:可寫描述符列表 56 第三個引數:錯誤資訊描述符列表 57 對於自己的套接字來說,輸入表示可以讀取,輸出表示可以寫入,套接字就相當於一個管道,對方的寫入代表你的讀取,你的寫入代表對方的讀取 58 59 select函式返回什麼呢?你把感興趣的描述符加入到列表中並交給select後,當有可讀或者有可寫或者錯誤這些描述符就緒後,select就會返回 60 哪些就緒的描述符,你需要做的就是遍歷這些描述符逐一進行處理。 61 """ 62 readSet, writeSet, errorSet = select.select(rList, wList, eList) 63 64 # 處理描述符可讀 65 for readAbledSockFD in readSet: 66 if readAbledSockFD is sockFd: 67 try: 68 connFd, remAddr = sockFd.accept() 69 except Exception as err: 70 """ 71 這裡處理當三次握手完成後,客戶端意外傳送了一個RST,這將導致一個伺服器錯誤 72 """ 73 print("") 74 continue 75 print("新連線:", connFd.getpeername()) 76 # 把新連線加入可讀列表中 77 rList.append(connFd) 78 else: 79 echoStr(readAbledSockFD, rList) 80 81 # 處理描述符可寫 82 for writeAbledSockFd in writeSet: 83 pass 84 85 # 處理錯誤描述符 86 for errAbled in errorSet: 87 pass 88 89 90 if __name__ == '__main__': 91 main()
客戶端程式碼
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Author: rex.cheny 4 # E-mail: rex.cheny@outlook.com 5 6 import socket 7 import select 8 import sys 9 10 11 def echoStr(sockFd, connectionFailed): 12 try: 13 bytesData = sockFd.recv(1024) 14 data = bytesData.decode(encoding="utf-8") 15 if data: 16 print("伺服器回覆:", data) 17 else: 18 """ 19 如果伺服器程式意外終止,那麼套接字也將返回,因為該連線套接字收到FIN,所以sockFd讀取的內容是''就是空,資料長度是0 20 也就是你試圖讀取一個收到FIN的套接字會出現這種情況,通常的錯誤資訊是 "server terminated prematurely" 21 """ 22 print("伺服器 ", sockFd.getpeername(), " 意外中斷連線。") 23 sockFd.close() 24 connectionFailed = True 25 except Exception as err: 26 """ 27 這裡如果丟擲異常通常是因為當連線套接字收到RST之後呼叫 recv()函式產生的 "Connection reset by peer" 錯誤, 28 為什麼套接字會收到RST,通常是向一個收到FIN的套接字執行寫入操作導致的。 29 """ 30 print("伺服器 ", sockFd.getpeername(), " 意外中斷連線。") 31 sockFd.close() 32 connectionFailed = True 33 return connectionFailed 34 35 36 def main(): 37 sockFd = socket.socket() 38 sockFd.connect(("127.0.0.1", 5556)) 39 40 # 用於判斷伺服器是否意外中斷 41 connectionFailed = False 42 while True: 43 data = input("等待輸入:") 44 if data == "Bye": 45 sockFd.send("Bye".encode(encoding="utf-8")) 46 """ 47 shutdown就是主動觸發關閉套接字,傳送FIN,後面的引數是關閉寫這一半,其實就是告訴伺服器客戶端不會再傳送資料了。 48 """ 49 sockFd.shutdown(socket.SHUT_WR) 50 break 51 else: 52 sockFd.send(data.encode(encoding="utf-8")) 53 if echoStr(sockFd, connectionFailed): 54 break 55 56 57 if __name__ == '__main__': 58 main()
改進的服務端程式碼
服務端程式碼沒有做多少改動只是利用TCP機制減少了一些程式碼
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Author: rex.cheny 4 # E-mail: rex.cheny@outlook.com 5 6 import socket 7 import select 8 9 10 def echoStr(readAbledSockFD, rList): 11 try: 12 bytesData = readAbledSockFD.recv(1024) 13 data = bytesData.decode(encoding="utf-8") 14 if data: 15 print("收到客戶端 ", readAbledSockFD.getpeername(), " 訊息:", data) 16 if data.upper() == "EXIT": 17 pass 18 else: 19 readAbledSockFD.send(data.encode(encoding="utf-8")) 20 else: 21 """ 22 如果客戶端程式意外終止或者客戶端主動斷開,那麼select將返回,因為該連線套接字收到FIN,所以readAbledSockFD讀取的內容是''就是空,資料長度是0 23 ,這種情況有兩種可能: 24 1. 客戶端主動斷開,表示EOF,也就是資源無後續資料可以讀取其實也就是連線關閉 25 2. 客戶端程式崩潰,客戶端核心還是會傳送FIN,通常的錯誤資訊是 "server terminated prematurely" 26 所以無論是哪種情況造成,這裡也就是你試圖讀取一個收到FIN的套接字,我們統一視為關閉。 27 """ 28 print("客戶端 ", readAbledSockFD.getpeername(), " *** EOF,主動斷開連線。") 29 rList.remove(readAbledSockFD) 30 readAbledSockFD.close() 31 except Exception as err: 32 """ 33 這裡如果丟擲異常通常是因為當連線套接字收到RST之後呼叫 recv()函式產生的 "Connection reset by peer" 錯誤, 34 為什麼套接字會收到RST,通常是向一個收到FIN的套接字執行寫入操作導致的。 35 """ 36 print("客戶端 ", readAbledSockFD.getsockname(), " 意外中斷連線。") 37 rList.remove(readAbledSockFD) 38 readAbledSockFD.close() 39 40 41 def main(): 42 sockFd = socket.socket() 43 sockFd.bind(("", 5556)) 44 sockFd.listen(5) 45 46 # 這裡為什麼要把這個監聽套接字放入可讀列表中呢?伺服器監聽套接字描述符如果有新連線進來那麼該描述符可讀 47 rList = [sockFd] 48 wList = [] 49 eList = [] 50 51 print("等待客戶端連線......") 52 while True: 53 """ 54 select(),有4個引數,前三個必須也就是感興趣的描述符,第四個是超時時間 55 第一個引數:可讀描述符列表 56 第二個引數:可寫描述符列表 57 第三個引數:錯誤資訊描述符列表 58 對於自己的套接字來說,輸入表示可以讀取,輸出表示可以寫入,套接字就相當於一個管道,對方的寫入代表你的讀取,你的寫入代表對方的讀取 59 60 select函式返回什麼呢?你把感興趣的描述符加入到列表中並交給select後,當有可讀或者有可寫或者錯誤這些描述符就緒後,select就會返回 61 哪些就緒的描述符,你需要做的就是遍歷這些描述符逐一進行處理。 62 """ 63 readSet, writeSet, errorSet = select.select(rList, wList, eList) 64 65 # 處理描述符可讀 66 for readAbledSockFD in readSet: 67 if readAbledSockFD is sockFd: 68 try: 69 connFd, remAddr = sockFd.accept() 70 except Exception as err: 71 """ 72 這裡處理當三次握手完成後,客戶端意外傳送了一個RST,這將導致一個伺服器錯誤 73 """ 74 print("") 75 continue 76 print("新連線:", connFd.getpeername()) 77 # 把新連線加入可讀列表中 78 rList.append(connFd) 79 else: 80 echoStr(readAbledSockFD, rList) 81 82 # 處理描述符可寫 83 for writeAbledSockFd in writeSet: 84 pass 85 86 # 處理錯誤描述符 87 for errAbled in errorSet: 88 pass 89 90 91 if __name__ == '__main__': 92 main()
改進的客戶端程式碼
客戶端為什麼改進?因為之前的客戶端會阻塞在標準輸入中,如果在等待客戶端輸入的時候服務端意外終止,那麼此時客戶端並不知道,只有傳送資料的時候才會知道,這裡我們改進的是客戶端也使用select,它來監控套接字描述符和標準輸入同時使程式不在被阻塞在標準輸入上。你可以測試一下,當伺服器啟動後然後啟動客戶端,這時候客戶端在等待輸入,如果你把服務端終止那麼在上面的版本中客戶端並不知道雖然它得套接字已經收到FIN,但是在下面這版客戶端程式中客戶端會捕捉到這個變化從而直接終止客戶端程式。
#!/usr/bin/env python # -*- coding: utf-8 -*- # Author: rex.cheny # E-mail: rex.cheny@outlook.com """ 解決了當伺服器意外崩潰時客戶端被阻塞在螢幕輸入中,該版本程式使用了select。 """ import socket import select import sys def echoStr(sockFd, connectionFailed): try: bytesData = sockFd.recv(1024) data = bytesData.decode(encoding="utf-8") if data: print("伺服器回覆:", data) else: """ 如果伺服器程式意外終止,那麼套接字也將返回,因為該連線套接字收到FIN,所以sockFd讀取的內容是''就是空,資料長度是0 也就是你試圖讀取一個收到FIN的套接字會出現這種情況,通常的錯誤資訊是 "server terminated prematurely" """ print("與伺服器連線已斷開。") sockFd.close() connectionFailed = True except Exception as err: """ 這裡如果丟擲異常通常是因為當連線套接字收到RST之後呼叫 recv()函式產生的 "Connection reset by peer" 錯誤, 為什麼套接字會收到RST,通常是向一個收到FIN的套接字執行寫入操作導致的。 """ print("伺服器 ", sockFd.getsockname(), " 意外中斷連線。") sockFd.close() connectionFailed = True return connectionFailed def main(): sockFd = socket.socket() sockFd.connect(("127.0.0.1", 5556)) rList = [sockFd, sys.stdin] wList = [] eList = [] # 用於判斷伺服器是否意外中斷 connectionFailed = False while True: r, w, e = select.select(rList, wList, eList) if sockFd in r: if echoStr(sockFd, connectionFailed): break if sys.stdin in r: x = sys.stdin.readline().strip() if x.upper() == "EXIT": sockFd.send(x.encode(encoding="utf-8")) """ shutdown就是主動觸發關閉套接字,傳送FIN,後面的引數是關閉寫這一半,其實就是告訴伺服器客戶端不會再傳送資料了。 為什麼不直接close呢?這是因為假設此時還有伺服器返回的資料在路上那麼你還可以收到。 """ sockFd.shutdown(socket.SHUT_WR) else: sockFd.send(x.encode(encoding="utf-8")) if __name__ == '__main__': main()