socketserver — 网络服务器框架

源代码: Lib/socketserver.py


socketserver 模块简化了编写网络服务器的任务。

可用性:非 WASI。

此模块在 WebAssembly 上不起作用或不可用。有关更多信息,请参阅 WebAssembly 平台

有四种基本的具体服务器类

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

这使用互联网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 混合类可用于支持异步行为。

创建服务器需要几个步骤。首先,您必须通过子类化 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

可以使用这些混合类创建每种类型的服务器的分叉和线程版本。例如,ThreadingUDPServer 的创建方式如下

class ThreadingUDPServer(ThreadingMixIn, UDPServer):
    pass

混合类优先,因为它会覆盖 UDPServer 中定义的方法。设置各种属性也会改变底层服务器机制的行为。

ForkingMixIn 和下面提到的 Forking 类仅在支持 fork() 的 POSIX 平台上可用。

block_on_close

ForkingMixIn.server_close 会一直等到所有子进程完成,除非 block_on_close 属性为 False

ThreadingMixIn.server_close 会一直等到所有非守护线程完成,除非 block_on_close 属性为 False

max_children

指定 ForkingMixIn 同时处理请求的子进程数量。如果达到限制,新请求将等待一个子进程完成。

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 来隐藏。

当然,你仍然需要思考!例如,如果服务在内存中包含可能被不同请求修改的状态,则使用分叉服务器毫无意义,因为子进程中的修改永远不会到达父进程中保存并传递给每个子进程的初始状态。在这种情况下,您可以使用线程服务器,但您可能需要使用锁来保护共享数据的完整性。

另一方面,如果您正在构建一个 HTTP 服务器,其中所有数据都存储在外部(例如,在文件系统中),同步类将在处理一个请求时基本上使服务“失聪”——如果客户端接收所请求的所有数据很慢,这可能会持续很长时间。这里适合使用线程或分叉服务器。

在某些情况下,可能适合同步处理请求的一部分,但根据请求数据在分叉的子进程中完成处理。这可以通过使用同步服务器并在请求处理程序类 handle() 方法中执行显式分叉来实现。

在不支持线程或 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 版本中已更改: service_actions 调用已添加到 serve_forever 方法中。

service_actions()

serve_forever() 循环中调用此方法。子类或混合类可以重写此方法,以执行特定于给定服务的操作,例如清理操作。

在 3.3 版本加入。

shutdown()

告知 serve_forever() 循环停止并等待它完成。必须在 serve_forever() 在另一个线程中运行时调用 shutdown(),否则会发生死锁。

server_close()

清理服务器。可以重写。

address_family

服务器套接字所属的协议族。常见示例是 socket.AF_INETsocket.AF_INET6socket.AF_UNIX。如果要使用 IPv6 服务器类,请使用类属性 address_family = AF_INET6 子类化此模块中的 TCP 或 UDP 服务器类。

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 的值并且超时周期已过但未收到任何请求时,将调用此函数。对于分叉服务器,默认操作是收集已退出子进程的状态,而在线程服务器中,此方法不执行任何操作。

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
        pieces = [b'']
        total = 0
        while b'\n' not in pieces[-1] and total < 10_000:
            pieces.append(self.request.recv(2000))
            total += len(pieces[-1])
        self.data = b''.join(pieces)
        print(f"Received from {self.client_address[0]}:")
        print(self.data.decode("utf-8"))
        # just send back the same data, but upper-cased
        self.request.sendall(self.data.upper())
        # after we return, the socket will be closed.

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.
        # We limit ourselves to 10000 bytes to avoid abuse by the sender.
        self.data = self.rfile.readline(10000).rstrip()
        print(f"{self.client_address[0]} wrote:")
        print(self.data.decode("utf-8"))
        # Likewise, self.wfile is a file-like object used to write back
        # to the client
        self.wfile.write(self.data.upper())

不同之处在于,第二个处理程序中的 readline() 调用会多次调用 recv(),直到遇到换行符,而第一个处理程序必须使用 recv() 循环来累积数据,直到遇到换行符。如果它只使用单个 recv() 而没有循环,它只会返回到目前为止从客户端收到的数据。TCP 是基于流的:数据按照发送的顺序到达,但客户端 send()sendall() 调用与服务器上接收它所需的 recv() 调用次数之间没有关联。

这是客户端侧

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, "utf-8"))
    sock.sendall(b"\n")

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

print("Sent:    ", data)
print("Received:", 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(f"{self.client_address[0]} wrote:")
        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:    ", data)
print("Received:", 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 平台上可用。