(三)通過執行緒編寫一個簡單的併發伺服器
概述
之前在上一節通過使用fork來實現了一個併發程式,它很經典但是效率不高主要是太消耗資源因為fork一個程序的開銷很大,假如100客戶端連線就需要100個程序,這樣不是不可以只是這種方式不太高階,下面我們通過使用執行緒來實現併發,因為產生一個執行緒的開銷要小的多,當然對於大規模併發的場景使用執行緒也不是最好的選擇,但是學習socket程式設計的過程中這些東西是需要了解的。
程式碼段
伺服器端
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Author: rex.cheny 4 # E-mail: [email protected] 5 6 import socket 7 import time 8 import threading 9 10 11 def echoStr(connFd): 12print("新連線:", connFd.getpeername()) 13while True: 14bytesData = connFd.recv(1024) 15data = bytesData.decode(encoding="utf-8") 16print("收到客戶端訊息:", data) 17if data == "Bye": 18""" 19這裡需要關閉連線,在之前的fork模式中這裡是直接返回的 20不過使用執行緒則需要先關閉再返回。 21""" 22connFd.close() 23return 24else: 25time.sleep(1) 26connFd.send(data.encode(encoding="utf-8")) 27 28 29 def main(): 30sockFd = socket.socket() 31sockFd.bind(("", 5555)) 32sockFd.listen(5) 33 34print("等待客戶端連線......") 35while True: 36connFd, remAddr = sockFd.accept() 37try: 38""" 39這裡產生一個執行緒來處理連線,我們在這裡啟動執行緒後不能像fork模式那樣關閉連線套接字 40因為執行緒是共享程序資源的所以你這裡如果關閉那麼這個TCP連線也就斷了。而之前在fork 41模式中需要關閉是因為程序的資源是隔離的父子程序對同一個檔案描述符的兩次引用,而在 42執行緒裡對這個一個檔案描述符只引用了一次,所以這裡不能關閉。 43""" 44t = threading.Thread(target=echoStr, args=(connFd,)) 45t.start() 46except Exception as err: 47print(err) 48 49 50 if __name__ == '__main__': 51main()
客戶端程式碼
與之前的相同,不過這裡還是放進來
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Author: rex.cheny 4 # E-mail: [email protected] 5 6 import socket 7 8 9 def echoStr(sockFd, data): 10sockFd.send(data) 11bytesData = sockFd.recv(1024) 12data = bytesData.decode(encoding="utf-8") 13print(data) 14 15 16 def main(): 17sockFd = socket.socket() 18sockFd.connect(("127.0.0.1", 5555)) 19 20for i in range(1, 11): 21data = "第:" + str(i) + " 條訊息。" 22echoStr(sockFd, data.encode(encoding="utf-8")) 23 24echoStr(sockFd, "Bye".encode(encoding="utf-8")) 25sockFd.close() 26 27 28 if __name__ == '__main__': 29main()
結果演示
效果一樣。不過這裡有一個問題其實包括之前的fork版本的程式也有問題就是如果處理客戶端請求的程序崩潰,那麼伺服器端TCP協議棧會發送FIN,這時候客戶端肯定可以收到並回復ACK,但是如果客戶端這時候剛好要傳送下一條訊息,這時候客戶端就是在一個已收到FIN的套接字裡寫資料,那麼它會收到一個RST,它寫完了馬上會呼叫讀(從程式碼裡可以看出來)那麼讀一個收到RST的套接字將會報錯。而如果它收到FIN的時候剛好是讀則會得到一個EOF。這也就是上一節裡末尾的圖所說明的內容。其實如果客戶端能夠及時對收到的FIN做出反應那麼將避免對收到FIN的套接字進行操作,其實在我們的例子中效果不明顯因為我的客戶端程式是自動傳送訊息的,如果你改成輸入訊息則會非常明顯。因為會阻塞在使用者輸入的地方,因為這種阻塞模式的套接字程式,收到FIN後它無法處理,換句話說對於客戶端來說它在處理兩個檔案描述符一個是套接字的一個是標準輸入的,要想解決這個問題我們就需要用到多路複用。