【Python】使用socketserver創建一個異步TCP服務器

概述

這篇文章是講解如何使用socketserver創建一個異步TCP服務器,其中Python版本爲3.5.1。python

 

socketserver主要的類

socketserver模塊中的類主要有如下幾個:
一、BaseServer 包含服務器的核心功能與混合類(mix-in)的鉤子功能。這個類主要用於派生,不要直接生成這個類的類對象,能夠考慮使用TCPServer和UDPServer類。
二、TCPServer:基本的網絡同步TCP服務器
三、UDPServer:基本的網絡同步UDP服務器
四、ForkingMixIn:實現了核心的進程化功能,用於與服務器類進行混合(mix-in),以提供一些異步特性。不要直接生成這個類的對象。
五、ThreadingMixIn:實現了核心的線程化功能,用於與服務器類進行混合(mix-in),以提供一些異步特性。不要直接生成這個類的對象。
六、ForkingTCPServer: ForkingMixIn與TCPServer的組合
七、ForkingUDPServer:ForkingMixIn與UDPServer的組合
八、BaseRequestHandler:基本的請求處理類
九、StreamRequestHandler:TCP請求處理類的一個實現
十、DataStreamRequestHandler:UDP請求處理類的一個實現服務器

 

BaseRequestHandler類

BaseRequestHandler類的實例h能夠實現如下方法:

一、h.handle() 調用該方法執行實際的請求操做。調用該函數能夠不帶任何參數,可是幾個實例變量包含有用的值。h.request包含請求,h.client_address包含客戶端地址,h.server包含調用處理程序的實例。對於TCP之類的數據流服務,h.request屬性是套接字對象。對於數據報服務,它是包含收到數據的字節字符串。

二、h.setup() 該方法在handle()以前調用。默認狀況下,它不執行任何操做。若是但願服務器實現更多鏈接設置(如創建SSL鏈接),能夠在這裏實現。

三、h.finish() 調用本方法能夠在執行完handle()以後執行清除操做。默認狀況下,它不執行任何操做。若是setup()和handle()方法都不生成異常,則無需調用該方法。

網絡

 

官方例程

 

首先上官方給出的例程app

 

[python] view plain copy <span style="font-size:14px;">import socket import threading import socketserver class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): data = str(self.request.recv(1024), 'ascii') cur_thread = threading.current_thread() response = bytes("{}: {}".format(cur_thread.name, data), 'ascii') self.request.sendall(response) class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass  
def client(ip, port, message): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((ip, port)) sock.sendall(bytes(message, 'ascii')) response = str(sock.recv(1024), 'ascii') print("Received: {}".format(response)) if __name__ == "__main__": # Port 0 means to select an arbitrary unused port 
    HOST, PORT = "localhost", 0 server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler) ip, port = server.server_address # Start a thread with the server -- that thread will then start one  # more thread for each request 
    server_thread = threading.Thread(target=server.serve_forever) # Exit the server thread when the main thread terminates 
    server_thread.daemon = True server_thread.start() print("Server loop running in thread:", server_thread.name) client(ip, port, "Hello World 1") client(ip, port, "Hello World 2") client(ip, port, "Hello World 3") server.shutdown() server.server_close()</span>

 


client函數是創建一個客戶端,能夠不用管它。主要部分是在於主函數,ThreadedTCPServer類和ThreadedTCPRequestHandler類。ThreadedTCPServer類繼承了BaseRequestHandler類,ThreadedTCPRequestHandler繼承了ThreadingMixIn和TCPServer異步

正常輸入以下:socket

$ python ThreadedTCPServer.py
Server loop running in thread: Thread-1
Received: Thread-2: Hello World 1
Received: Thread-3: Hello World 2
Received: Thread-4: Hello World 3函數

 

增長功能

上面部分主要是講解官方的例程,下面這一部分是博主本身增長的功能。工具

一、獲取客戶端的ip和port

若是想在TCP創建鏈接後打印「<ip>:<port> is connect!」信息出來,並獲取客戶端的ip地址和端口信息,能夠在ThreadedTCPRequestHandler類裏面改寫setup函數。
 1 [python] view plain copy  2  
 3 client_addr = []  4   
 5 class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):  6   
 7     def setup(self):  8         ip = self.client_address[0].strip()     # 獲取客戶端的ip 
 9         port = self.client_address[1]           # 獲取客戶端的port 
10         print(ip+":"+str(port)+" is connect!") 11         client_addr.append(self.client_address) # 保存到隊列中 
12   
13     def handle(self): 14         data = str(self.request.recv(1024), 'ascii') 15         cur_thread = threading.current_thread() 16         response = bytes("{}: {}".format(cur_thread.name, data), 'ascii') 17         self.request.sendall(response)
 
在主函數中添加下面語句,便可打印出鏈接過的客戶端信息:
1 [python] view plain copy 2  
3 print("\nclient_addr:"+str(client_addr))
 

二、保持TCP長鏈接

官方例程中是創建了TCP鏈接後就立刻斷開,若是想創建長鏈接,能夠在handle函數中添加while循環,同時修改代碼爲:先判斷緩衝區是否有數據,有數據才進行響應;改寫finish函數,能夠看到finish的信息並無打印出來。若是註釋掉while循環語句,能夠看到finish的信息會打印出來。
 1 [python] view plain copy  2  
 3 class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):  4   
 5     def setup(self):  6         ip = self.client_address[0].strip()     # 獲取客戶端的ip 
 7         port = self.client_address[1]           # 獲取客戶端的port 
 8         print(ip+":"+str(port)+" is connect!")  9         client_addr.append(self.client_address) # 保存到隊列中 
10   
11     def handle(self): 12         while True: # while循環 
13             data = str(self.request.recv(1024), 'ascii') 14             if data:    # 判斷是否接收到數據 
15                 cur_thread = threading.current_thread() 16                 response = bytes("{}: {}".format(cur_thread.name, data), 'ascii') 17  self.request.sendall(response) 18   
19     def finish(self): 20         print("client is disconnect!")

 

 
 
感謝評論區歇業的漁夫的建議,while True 循環創建長鏈接的方式很是佔用CPU資源,最好在循環裏面增長一個time.sleep(0.1)的休眠。

三、服務器給客戶端發送請求

如今的例程是在ThreadedTCPRequestHandler類裏面調用self.request.sendall方法來給客戶端發送數據,並且只能被動發送數據,若是我想主動給客戶端發送數據,又該怎麼辦呢?下面是實現服務器主動給客戶端發送請求的功能。
 
TCP鏈接想要發送數據,只要找到相關的方法直接調用便可,因而我對ThreadedTCPServer這個類的實例server的方法找了很久,也沒有找到發送的方法。後來我查資料注意到了一句話:「對於TCP之類的數據流服務,h.request屬性是套接字對象。」我以爲我能夠這樣作:使用這個套接字對象發送數據。通過嘗試後,驗證成功。下面只放上核心代碼:
 1 [python] view plain copy  2  
 3 client_addr = []  4 client_socket = []  5   
 6 class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):  7   
 8     def setup(self):  9         ip = self.client_address[0].strip()     # 獲取客戶端的ip 
10         port = self.client_address[1]           # 獲取客戶端的port 
11         print(ip+":"+str(port)+" is connect!") 12         client_addr.append(self.client_address) # 保存到隊列中 
13         client_socket.append(self.request)      # 保存套接字socket 
14   
15     def handle(self): 16         while True: # while循環 
17             data = str(self.request.recv(1024), 'ascii') 18             if data:    # 判斷是否接收到數據 
19                 cur_thread = threading.current_thread() 20                 response = bytes("{}: {}".format(cur_thread.name, data), 'ascii') 21  self.request.sendall(response) 22   
23     def finish(self): 24         print("client is disconnect!") 25  client_addr.remove(self.client_address) 26         client_socket.remove(self.request)

 

 

以後在主函數中經過client_socket隊列調用sendall或sendto方法便可。例如我在主函數這樣寫(已經註釋掉client函數調用):
1 [python] view plain copy 2  
3 message = bytes("clientTest\n", "ascii") 4 while True: 5     time.sleep(2) 6     if client_addr: 7         client_socket[0].sendall(message)

 

 

修改服務器ip地址爲空及端口爲8080,使用socket調試工具鏈接該服務器,便可每隔2s接收到「clientTest」字符串。
 
 

四、服務器接收客戶端數據超時後斷開

下面繼續添加新的功能,假設客戶端每隔一段時間發送數據給服務器(心跳包),若是在必定時間內服務器沒有接受到心跳包,代表客戶端已經斷開了鏈接,這個時候服務器能夠主動斷開客戶端的鏈接了。那麼咱們在原有的代碼增長此功能。實際上,只須要修改ThreadedTCPRequestHandler類便可。
 1 [python] view plain copy  2  
 3 class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):  4   
 5     ip = ""  
 6     port = 0  7     timeOut = 6     # 設置超時時間變量 
 8   
 9     def setup(self): 10         self.ip = self.client_address[0].strip()     # 獲取客戶端的ip 
11         self.port = self.client_address[1]           # 獲取客戶端的port 
12         self.request.settimeout(self.timeOut)        # 對socket設置超時時間 
13         print(self.ip+":"+str(self.port)+"鏈接到服務器!") 14         client_addr.append(self.client_address) # 保存到隊列中 
15         client_socket.append(self.request)      # 保存套接字socket 
16   
17     def handle(self): 18         while True: # while循環 
19             try: 20                 data = str(self.request.recv(1024), 'ascii') 21             except socket.timeout:  # 若是接收超時會拋出socket.timeout異常 
22                 print(self.ip+":"+str(self.port)+"接收超時!即將斷開鏈接!") 23                 break       # 記得跳出while循環 
24   
25             if data:    # 判斷是否接收到數據 
26                 cur_thread = threading.current_thread() 27                 response = bytes("{}: {}".format(cur_thread.name, data), 'ascii') 28  self.request.sendall(response) 29   
30     def finish(self): 31         print(self.ip+":"+str(self.port)+"斷開鏈接!") 32  client_addr.remove(self.client_address) 33         client_socket.remove(self.request)

 

 

使用socket調試工具鏈接該服務器後,不發送任何數據,過了6秒鐘後,服務器端主要打印以下數據:
192.168.10.53:26408鏈接到服務器! 192.168.10.53:26408接收超時!即將斷開鏈接! 192.168.10.53:26408斷開鏈接!
相關文章
相關標籤/搜索