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 種類,分別是:

  1. TCPServer
  2. UDPServer
  3. UnixStreamServer
  4. UnixDatagramServer

基本上, UnixStreamServerUnixDatagramServer 無法在非 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. 撰寫 1 個類別繼承 socketserver 模組的 BaseRequestHandler 類別,並覆寫父類別的 handle() 方法,這個自製的類別將會專門被用來處理 client 送來的要求,在此可以暫時稱為 Handler, 例如:
import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        pass
  1. 實例化前述所提及的 4 種 Sever 類型的其中之一,並傳入參數 IP 位址,通訊埠(port)以及第 1 步驟所撰寫的 Handler 名稱,例如:
server = socketserver.TCPServer((127.0.0.1, 9999), MyTCPHandler)
  1. 呼叫第 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() 沒有任何作用,因此可以視需求覆寫 RequestHandlersetup() 方法。

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. 實作範例 1 所介紹的繼承 RequestHandler 的子類別

  2. 額外建立 1 類別同時繼承 socketserver.ThreadingMixIn 及一開始提及的 4 種 socketserver 之ㄧ,例如:

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass
  1. 使用上述建立的子類別實例化 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

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!