socketserver — 網路伺服器框架

原始碼: Lib/socketserver.py


socketserver 模組簡化了編寫網路伺服器的任務。

可用性: 非 WASI。

此模組在 WebAssembly 上無法工作或不可用。有關更多資訊,請參閱 WebAssembly 平臺

有四個基本的具體伺服器類

class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

這使用 Internet TCP 協議,該協議提供客戶端和伺服器之間連續的資料流。如果 *bind_and_activate* 為 true,則建構函式會自動嘗試呼叫 server_bind()server_activate()。其他引數將傳遞給 BaseServer 基類。

class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

這使用資料報,資料報是離散的資訊包,在傳輸過程中可能會無序到達或丟失。引數與 TCPServer 的相同。

class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)

這些不太常用的類與 TCP 和 UDP 類類似,但使用 Unix 域套接字;它們在非 Unix 平臺上不可用。引數與 TCPServer 的相同。

這四個類 同步 處理請求;每個請求必須在下一個請求開始之前完成。如果每個請求都需要很長時間才能完成,因為它需要大量計算,或者因為它返回大量客戶端處理速度較慢的資料,則不適合這種情況。解決方案是建立一個單獨的程序或執行緒來處理每個請求;ForkingMixInThreadingMixIn mix-in 類可用於支援非同步行為。

建立伺服器需要幾個步驟。首先,您必須透過子類化 BaseRequestHandler 類並重寫其 handle() 方法來建立請求處理程式類;此方法將處理傳入的請求。其次,您必須例項化一個伺服器類,並將伺服器的地址和請求處理程式類傳遞給它。建議在 with 語句中使用伺服器。然後呼叫伺服器物件的 handle_request()serve_forever() 方法來處理一個或多個請求。最後,呼叫 server_close() 關閉套接字(除非您使用了 with 語句)。

當從 ThreadingMixIn 繼承以獲得執行緒連線行為時,您應該明確宣告您希望執行緒在突然關閉時如何表現。ThreadingMixIn 類定義了一個屬性 *daemon_threads*,該屬性指示伺服器是否應該等待執行緒終止。如果您希望執行緒自主執行,則應顯式設定該標誌;預設值為 False,這意味著 Python 在 ThreadingMixIn 建立的所有執行緒退出之前不會退出。

伺服器類具有相同的外部方法和屬性,無論它們使用什麼網路協議。

伺服器建立注意事項

繼承圖中共有五個類,其中四個類代表四種類型的同步伺服器

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+

請注意,UnixDatagramServer 派生自 UDPServer,而不是 UnixStreamServer — IP 伺服器和 Unix 伺服器之間的唯一區別是地址族。

class socketserver.ForkingMixIn
class socketserver.ThreadingMixIn

可以使用這些 mix-in 類建立每種型別的伺服器的 forking 和 threading 版本。例如,ThreadingUDPServer 的建立方式如下

class ThreadingUDPServer(ThreadingMixIn, UDPServer):
    pass

mix-in 類首先出現,因為它覆蓋了 UDPServer 中定義的方法。設定各種屬性也會更改底層伺服器機制的行為。

ForkingMixIn 和下面提到的 Forking 類僅在支援 fork() 的 POSIX 平臺上可用。

block_on_close

ForkingMixIn.server_close 會等待所有子程序完成,除非 block_on_close 屬性為 False

ThreadingMixIn.server_close 會等待所有非守護執行緒完成,除非 block_on_close 屬性為 False

daemon_threads

對於 ThreadingMixIn,透過將 ThreadingMixIn.daemon_threads 設定為 True 來使用守護執行緒,從而無需等待執行緒完成。

在 3.7 版本中更改: ForkingMixIn.server_closeThreadingMixIn.server_close 現在會等待所有子程序和非守護執行緒完成。添加了一個新的 ForkingMixIn.block_on_close 類屬性,用於選擇使用 3.7 之前的行為。

class socketserver.ForkingTCPServer
class socketserver.ForkingUDPServer
class socketserver.ThreadingTCPServer
class socketserver.ThreadingUDPServer
class socketserver.ForkingUnixStreamServer
class socketserver.ForkingUnixDatagramServer
class socketserver.ThreadingUnixStreamServer
class socketserver.ThreadingUnixDatagramServer

這些類是使用混合類預定義的。

在 3.12 版本中新增: 添加了 ForkingUnixStreamServerForkingUnixDatagramServer 類。

要實現一個服務,你必須從 BaseRequestHandler 派生一個類,並重新定義它的 handle() 方法。然後,你可以將伺服器類之一與請求處理程式類結合使用,從而執行該服務的各種版本。請求處理程式類對於資料報或流服務必須不同。這可以透過使用處理程式子類 StreamRequestHandlerDatagramRequestHandler 來隱藏。

當然,你仍然需要動腦筋!例如,如果服務包含可以在不同請求中修改的記憶體中的狀態,則使用 fork 伺服器是沒有意義的,因為子程序中的修改永遠不會到達父程序中儲存並傳遞給每個子程序的初始狀態。在這種情況下,你可以使用執行緒伺服器,但你可能需要使用鎖來保護共享資料的完整性。

另一方面,如果你正在構建一個 HTTP 伺服器,其中所有資料都儲存在外部(例如,在檔案系統中),則同步類本質上會使該服務在處理一個請求時“失聰” – 如果客戶端接收所有請求的資料的速度很慢,則這可能持續很長時間。在這種情況下,執行緒或 fork 伺服器是合適的。

在某些情況下,可能適合同步處理請求的一部分,但要根據請求資料在 fork 的子程序中完成處理。這可以透過使用同步伺服器並在請求處理程式類的 handle() 方法中顯式 fork 來實現。

在既不支援執行緒也不支援 fork() 的環境中(或者在這些方法對於該服務來說成本太高或不合適的情況下)處理多個同時請求的另一種方法是維護一個部分完成的請求的顯式表,並使用 selectors 來決定接下來要處理哪個請求(或者是否要處理新的傳入請求)。這對於流服務尤其重要,在流服務中,每個客戶端都有可能長時間連線(如果無法使用執行緒或子程序)。

伺服器物件

class socketserver.BaseServer(server_address, RequestHandlerClass)

這是模組中所有 Server 物件的超類。它定義了介面(如下所示),但不實現大多數方法,這些方法在子類中完成。這兩個引數分別儲存在 server_addressRequestHandlerClass 屬性中。

fileno()

返回伺服器正在監聽的套接字的整數檔案描述符。此函式最常傳遞給 selectors,以允許在同一程序中監視多個伺服器。

handle_request()

處理單個請求。此函式按順序呼叫以下方法:get_request()verify_request()process_request()。如果使用者提供的處理程式類的 handle() 方法引發異常,則會呼叫伺服器的 handle_error() 方法。如果在 timeout 秒內未收到任何請求,則會呼叫 handle_timeout(),並且 handle_request() 將返回。

serve_forever(poll_interval=0.5)

處理請求,直到顯式的 shutdown() 請求。每隔 poll_interval 秒輪詢一次是否需要關閉。忽略 timeout 屬性。它還會呼叫 service_actions(),子類或混入類可以使用它來提供特定於給定服務的操作。例如,ForkingMixIn 類使用 service_actions() 來清理殭屍子程序。

在 3.3 版本中更改: serve_forever 方法中增加了 service_actions 的呼叫。

service_actions()

serve_forever() 迴圈中呼叫。子類或混入類可以重寫此方法以執行特定於給定服務的操作,例如清理操作。

在 3.3 版本中新增。

shutdown()

告知 serve_forever() 迴圈停止並等待其完成。必須在 serve_forever() 在不同執行緒中執行時呼叫 shutdown(),否則會發生死鎖。

server_close()

清理伺服器。可以被重寫。

address_family

伺服器套接字所屬的協議族。常見的例子有 socket.AF_INETsocket.AF_UNIX

RequestHandlerClass

使用者提供的請求處理類;為每個請求建立一個此類的例項。

server_address

伺服器正在監聽的地址。地址的格式因協議族而異;有關詳細資訊,請參閱 socket 模組的文件。對於網際網路協議,這是一個包含一個字串(給出地址)和一個整數埠號的元組:例如 ('127.0.0.1', 80)

socket

伺服器將監聽傳入請求的套接字物件。

伺服器類支援以下類變數

allow_reuse_address

伺服器是否允許重用地址。預設為 False,可以在子類中設定以更改策略。

request_queue_size

請求佇列的大小。如果處理單個請求需要很長時間,那麼在伺服器繁忙時到達的任何請求都會被放入佇列,最多 request_queue_size 個請求。一旦佇列滿了,來自客戶端的進一步請求將收到“連線被拒絕”錯誤。預設值通常為 5,但可以被子類覆蓋。

socket_type

伺服器使用的套接字型別;socket.SOCK_STREAMsocket.SOCK_DGRAM 是兩個常見的值。

timeout

超時持續時間,以秒為單位測量,如果不需要超時,則為 None。如果 handle_request() 在超時期間內沒有收到任何傳入請求,則呼叫 handle_timeout() 方法。

有多種伺服器方法可以被諸如 TCPServer 等基礎伺服器類的子類重寫;這些方法對伺服器物件的外部使用者沒有用處。

finish_request(request, client_address)

透過例項化 RequestHandlerClass 並呼叫其 handle() 方法來實際處理請求。

get_request()

必須接受來自套接字的請求,並返回一個 2 元組,其中包含用於與客戶端通訊的 *新* 套接字物件和客戶端的地址。

handle_error(request, client_address)

如果 RequestHandlerClass 例項的 handle() 方法引發異常,則會呼叫此函式。預設操作是將回溯列印到標準錯誤並繼續處理進一步的請求。

在 3.6 版本中更改: 現在僅針對從 Exception 類派生的異常呼叫。

handle_timeout()

timeout 屬性設定為除 None 之外的值,並且在沒有收到任何請求的情況下經過了超時時間段時,將呼叫此函式。forking 伺服器的預設操作是收集任何已退出的子程序的狀態,而線上程伺服器中,此方法不執行任何操作。

process_request(request, client_address)

呼叫 finish_request() 來建立 RequestHandlerClass 的例項。如果需要,此函式可以建立一個新的程序或執行緒來處理請求;ForkingMixInThreadingMixIn 類就是這樣做的。

server_activate()

由伺服器的建構函式呼叫以啟用伺服器。TCP 伺服器的預設行為只是在伺服器的套接字上呼叫 listen()。可以被重寫。

server_bind()

由伺服器的建構函式呼叫以將套接字繫結到所需的地址。可以被重寫。

verify_request(request, client_address)

必須返回一個布林值;如果值為 True,則將處理該請求;如果為 False,則將拒絕該請求。可以重寫此函式以實現伺服器的訪問控制。預設實現始終返回 True

在 3.6 版本中更改: 添加了對 上下文管理器 協議的支援。退出上下文管理器等效於呼叫 server_close()

請求處理程式物件

class socketserver.BaseRequestHandler

這是所有請求處理程式物件的超類。 它定義瞭如下的介面。具體的請求處理程式子類必須定義一個新的 handle() 方法,並且可以覆蓋任何其他方法。為每個請求建立一個新的子類例項。

setup()

handle() 方法之前呼叫,以執行所需的任何初始化操作。預設實現不執行任何操作。

handle()

此函式必須完成為服務請求所需的所有工作。預設實現不執行任何操作。有幾個例項屬性可供它使用;請求可用作 request;客戶端地址為 client_address;伺服器例項為 server,以防它需要訪問每個伺服器的資訊。

對於資料報或流服務, request 的型別不同。 對於流服務, request 是一個套接字物件;對於資料報服務, request 是一對字串和套接字。

finish()

handle() 方法之後呼叫,以執行所需的任何清理操作。預設實現不執行任何操作。如果 setup() 引發異常,則不會呼叫此函式。

request

用於與客戶端通訊的 socket.socket 物件。

client_address

BaseServer.get_request() 返回的客戶端地址。

server

用於處理請求的 BaseServer 物件。

class socketserver.StreamRequestHandler
class socketserver.DatagramRequestHandler

這些 BaseRequestHandler 子類覆蓋了 setup()finish() 方法,並提供了 rfilewfile 屬性。

rfile

從中讀取接收請求的檔案物件。支援 io.BufferedIOBase 可讀介面。

wfile

用於寫入回覆的檔案物件。支援 io.BufferedIOBase 可寫介面。

在 3.6 版本中更改: wfile 也支援 io.BufferedIOBase 可寫介面。

示例

socketserver.TCPServer 示例

這是伺服器端

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    The request handler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print("Received from {}:".format(self.client_address[0]))
        print(self.data)
        # just send back the same data, but upper-cased
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
        # Activate the server; this will keep running until you
        # interrupt the program with Ctrl-C
        server.serve_forever()

另一種請求處理程式類,它利用流(透過提供標準檔案介面來簡化通訊的類似檔案的物件)

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        # self.rfile is a file-like object created by the handler;
        # we can now use e.g. readline() instead of raw recv() calls
        self.data = self.rfile.readline().strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # Likewise, self.wfile is a file-like object used to write back
        # to the client
        self.wfile.write(self.data.upper())

區別在於,第二個處理程式中的 readline() 呼叫將多次呼叫 recv(),直到遇到換行符,而第一個處理程式中的單個 recv() 呼叫將只返回到目前為止從客戶端的 sendall() 呼叫接收到的內容(通常是全部,但這不能由 TCP 協議保證)。

這是客戶端

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# Create a socket (SOCK_STREAM means a TCP socket)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    # Connect to server and send data
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data + "\n", "utf-8"))

    # Receive data from the server and shut down
    received = str(sock.recv(1024), "utf-8")

print("Sent:     {}".format(data))
print("Received: {}".format(received))

示例的輸出應該如下所示

伺服器

$ python TCPServer.py
127.0.0.1 wrote:
b'hello world with TCP'
127.0.0.1 wrote:
b'python is nice'

客戶端

$ python TCPClient.py hello world with TCP
Sent:     hello world with TCP
Received: HELLO WORLD WITH TCP
$ python TCPClient.py python is nice
Sent:     python is nice
Received: PYTHON IS NICE

socketserver.UDPServer 示例

這是伺服器端

import socketserver

class MyUDPHandler(socketserver.BaseRequestHandler):
    """
    This class works similar to the TCP handler class, except that
    self.request consists of a pair of data and client socket, and since
    there is no connection the client address must be given explicitly
    when sending data back via sendto().
    """

    def handle(self):
        data = self.request[0].strip()
        socket = self.request[1]
        print("{} wrote:".format(self.client_address[0]))
        print(data)
        socket.sendto(data.upper(), self.client_address)

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    with socketserver.UDPServer((HOST, PORT), MyUDPHandler) as server:
        server.serve_forever()

這是客戶端

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# SOCK_DGRAM is the socket type to use for UDP sockets
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# As you can see, there is no connect() call; UDP has no connections.
# Instead, data is directly sent to the recipient via sendto().
sock.sendto(bytes(data + "\n", "utf-8"), (HOST, PORT))
received = str(sock.recv(1024), "utf-8")

print("Sent:     {}".format(data))
print("Received: {}".format(received))

示例的輸出應與 TCP 伺服器示例完全相同。

非同步混合類

要構建非同步處理程式,請使用 ThreadingMixInForkingMixIn 類。

ThreadingMixIn 類的示例

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)
    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()

示例的輸出應該如下所示

$ 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

ForkingMixIn 類的使用方式相同,只是伺服器將為每個請求生成一個新程序。僅在支援 fork() 的 POSIX 平臺上可用。