Python socketserver 模組 — 方便的 Socket Server Framework 使用教學
Last updated on May 15, 2024 in Python 程式設計 - 中階 by Amo Chen ‐ 5 min read
socketserver 是 Python 1 個內建模組,主要用來簡化撰寫 Socket Server 的工作,因此在官網中也宣稱它是一個方便的 Socket Server Framework 。
The socketserver module simplifies the task of writing network servers.
本文環境
- Python 3
socketserver 模組簡介
使用前記得 import socketserver 模組。
import socketserver
通常在開發網路應用時,不外乎使用 TCP, UDP 等協定,因此 socketserver 模組提供 4 種基本的 Server 種類,分別是:
- TCPServer
- UDPServer
- UnixStreamServer
- UnixDatagramServer
基本上, UnixStreamServer 與 UnixDatagramServer 無法在非 Unix-like 的平台上運作,若無特殊需求,只要使用 TCPServer, UDPServer 即可。
These four classes process requests synchronously; each request must be completed before the next request can be started.
而這 4 種 Server 都屬於 synchronously ,每次只能處理 1 個要求(request),處理完 1 個要求之後,下 1 個要求才會被處理,因此當每 1 個要求都需要很長的執行時間的情況下,選擇使用 synchronously 就顯得十分不適合。
如果要能夠同時處理數個要求,可以考慮使用 asynchronous 的方式,只要利用 socketserver 模組中定義的 ThreadingMixIn 類別或 ForkingMixIn 就可以讓 server 也支援 asynchronous 。
ThreadingMixIn
代表每收到 1 個要求就會建立 1 個新的執行緒處理要求。ForkingMixIn
代表每收到 1 個要求就會建立 1 個新的 process 處理要求,不過ForkingMixIn
只有 POSIX 系列系統才支援(如 Linux, macOS, FreeBSD 等等)。
socketserver 模組教學
使用 socketserver 模組基本 3 步驟如下(不含 asynchronous ):
- 撰寫 1 個類別繼承 socketserver 模組的 BaseRequestHandler 類別,並覆寫父類別的 handle() 方法,這個自製的類別將會專門被用來處理 client 送來的要求,在此可以暫時稱為 Handler, 例如:
import socketserver
class MyTCPHandler(socketserver.BaseRequestHandler):
def handle(self):
pass
- 實例化前述所提及的 4 種 Sever 類型的其中之一,並傳入參數 IP 位址,通訊埠(port)以及第 1 步驟所撰寫的
Handler
名稱,例如:
server = socketserver.TCPServer((127.0.0.1, 9999), MyTCPHandler)
- 呼叫第 2 步的實例(instance)的
serve_forever()
方法
範例 1 - Synchronously TCPServer
以下示範 1 個可以用 telnet
連線並且有 3 個指令 USER, PASS, EXIT 能夠使用的的 TCPServer 。
範例 1
import re
import socketserver
class MyTCPHandler(socketserver.BaseRequestHandler):
def setup(self):
self.greeting = b'Hello, guest!\r\n'
self.need_to_greet = True
self.need_to_exit = False
def handle(self):
while not self.need_to_exit:
if self.need_to_greet == True:
self.request.sendall(self.greeting)
self.need_to_greet = False
else:
self.request.sendall(b'$ ')
try:
self.data = self.request.recv(1024).decode().strip()
except Exception:
continue
if self.data.startswith('USER '):
self.set_username(self.data)
elif self.data.startswith('PASS '):
self.set_password(self.data)
elif self.data.startswith('EXIT'):
self.request.sendall(b'Good bye!\r\n')
self.need_to_exit = True
else:
self.request.sendall(b'Available commands: USER, PASS, EXIT\r\n')
def set_username(self, username):
cols = re.split('\s', username)
print('USER is', cols[1])
def set_password(self, password):
cols = re.split('\s', password)
print('PASS is', cols[1])
if __name__ == '__main__':
binding = ('localhost', 9999)
server = socketserver.TCPServer(binding, MyTCPHandler)
print('Server is listening on localhost:9999')
server.serve_forever()
上述是一個極其簡易的 TCPServer 範例,在啟動之後會監聽 localhost:9999
, 本機的客戶端可使用以下指令進行測試:
$ telnet localhost 9999
成功之後,就可以輸入 USER <你的名字>
, PASS <任何字串>
或者 EXIT
測試。
說明一下範例 1 程式碼。
首先說明 MyTCPHandler 的 2 個主要方法 setup()
, handle()
,這 2 個方法是 RequestHandler
中所定義的 3 個方法中的 2 個( 3 個方法分別是 setup()
, handle()
, finish()
,詳見 RequestHandler Objects 。
setup()
會在 handle()
執行前被呼叫,這個方法可以被用來初始化處理要求前所需要的變數,例如在本範例中的 setup()
方法中,就初始化了 handle()
所需要用到的 greeting
, need_to_greet
, need_to_exit
3 個屬性,預設的 setup()
沒有任何作用,因此可以視需求覆寫 RequestHandler
的 setup()
方法。
handle()
則是用來處理要求的方法,是唯一必須實作的方法。
當客戶端(client side)連線時所送出的每一個要求,都會交由 handle()
處理,在範例 1 中,我們用一個 while
迴圈,確保客戶端會保持連線狀態,並且能夠輸入一些指令與伺服端(server side)進行互動。此外,每1 個要求都可以使用 self.request
這個屬性獲得,因此可以看到範例使用 self.request.sendall()
回傳訊息給客戶端,並且使用 self.request.recv(1024)
取得客戶端傳送至伺服端的訊息。
p.s. 網路傳輸都是使用 bytes, 所以範例 1 中才會有多個 b'...'
字串,以及 encode()
, decode()
轉換 bytes 與字串的操作
p.s. 可使用 self.client_address
取得客戶端的IP位址
p.s. 可使用self.server
獲得 Server實例,進行伺服物件的操作,例如設定 timeout
屬性等,詳見Server Objects
另外,沒有提到的是 finish()
方法,這個方法在 handle()
執行結束之後,會自動被呼叫(預設的 finish()
與 setup()
相同,都不會進行任何動作),如果伺服端需要在處理完要求之後需要進行其他善後的動作,可以選擇撰寫在 finish()
方法中。
簡而言之, setup()
, handle()
與 finish()
是 1 個流水線的概念,各自有不同的職責:
setup() ⭢ handle() ⭢ finish()
最後,範例 1 設定了 binding
變數將 IP 位址及通訊埠分別設定為 localhost 及 9999 後,實例化了 socketserver.TCPServer
後,呼叫了 serve_forever()
開始執行並接受客戶端的要求。
範例 1 雖然可以順利運作,但是它卻有 1 個問題 —— 無法接受 2 個以上客戶端連線 。
原因也很簡單,因為目前的 Sever 是單 process 且單執行緒的架構,第 1 個連上的客戶會讓 process 卡在 handle()
的 while
迴圈內,相當於佔著 server 不放,因此 server 無法處理第 2 個客戶。
這正好也呼應前文所說的:
synchronously server, 每次只能處理 1 個要求(request),處理完 1 個要求之後,下 1 個要求才會被處理,因此當每 1 個要求都需要很長的執行時間的情況下,選擇使用 synchronously 就顯得十分不適合
為了解決這個問題,我們在範例 2 中將 server 改為 asynchronous 讓它能夠同時處理 2 個以上的連線。
範例 2 - Asynchronous TCPServer
第 2 個範例是支援 asynchronous 的 socketserver 。
讓 socketserver 能夠支援 asynchronous 的主要做法也分為 3 步:
實作範例 1 所介紹的繼承 RequestHandler 的子類別
額外建立 1 類別同時繼承 socketserver.ThreadingMixIn 及一開始提及的 4 種 socketserver 之ㄧ,例如:
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
- 使用上述建立的子類別實例化 server, Python 官網提供 1 個很好的範例可以參考,範例如下:
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):
allow_reuse_address = True
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)
with server:
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()
在上述的範例中,可以清楚地看到多執行緒的 socketserver 是利用 threading.Thread(target=server.serve_forever)
來啟動的,接著為了測試執行緒的運作,範例模擬了 3 個客戶端連上伺服端的情況(即連續呼叫 3 次 client 函式)。
執行結果如下所示:
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
從結果可以看到不同的要求被不同的執行緒處理。
知道怎麼做之後,我們就可以進一步將範例 1 改成多執行緒的版本,以下範例 2 是範例 1 改成多執行緒的版本:
範例 2
import re
import socketserver
class MyTCPHandler(socketserver.BaseRequestHandler):
def setup(self):
self.greeting = b'Hello, guest!\r\n'
self.need_to_greet = True
self.need_to_exit = False
def handle(self):
while not self.need_to_exit:
if self.need_to_greet == True:
self.request.sendall(self.greeting)
self.need_to_greet = False
else:
self.request.sendall(b'$ ')
try:
self.data = self.request.recv(1024).decode().strip()
except Exception:
continue
if self.data.startswith('USER '):
self.set_username(self.data)
elif self.data.startswith('PASS '):
self.set_password(self.data)
elif self.data.startswith('EXIT'):
self.request.sendall(b'Good bye!\r\n')
self.need_to_exit = True
else:
self.request.sendall(b'Available commands: USER, PASS, EXIT\r\n')
def set_username(self, username):
cols = re.split('\s', username)
print('USER is', cols[1])
def set_password(self, password):
cols = re.split('\s', password)
print('PASS is', cols[1])
class MyThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
if __name__ == '__main__':
binding = ('localhost', 9999)
server = MyThreadedTCPServer(binding, MyTCPHandler)
print('Server is listening on localhost:9999')
server.serve_forever()
上述範例其實也只增加實作有繼承 socketserver.ThreadingMixIn
的 TCPServer, 就輕鬆讓 server 具有多執行緒處理多連線的功能。
p.s. ForkingMixIn
也是一樣的做法
總結
以上就是 Python socketserver 模組的概要說明,如果有興趣的話,可以到官網閱讀 官方文件 。
References
https://docs.python.org/3/library/socketserver.html