socketserver --- 用于网络服务器的框架

源代码: Lib/socketserver.py


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

适用范围: not WASI.

此模块在 WebAssembly 平台上无效或不可用。请参阅 WebAssembly 平台 了解详情。

该模块具有四个基础实体服务器类:

classsocketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

该类使用互联网 TCP 协议,它可以提供客户端与服务器之间的连续数据流。如果 bind_and_activate 为真值,该类的构造器会自动尝试调用 server_bind()server_activate()。其他形参会被传递给 BaseServer 基类。

classsocketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

该类使用数据包,即一系列离散的信息分包,它们可能会无序地到达或在传输中丢失。该类的形参与 TCPServer 的相同。

classsocketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
classsocketserver.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 服务器的唯一区别是地址族。

classsocketserver.ForkingMixIn
classsocketserver.ThreadingMixIn

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

classThreadingUDPServer(ThreadingMixIn, UDPServer):
 pass

混合类先出现,因为它重载了 UDPServer 中定义的一个方法。设置各种属性也会改变下层服务器机制的行为。

ForkingMixIn 和下文提及的分叉类仅在支持 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 版之前的行为。

classsocketserver.ForkingTCPServer
classsocketserver.ForkingUDPServer
classsocketserver.ThreadingTCPServer
classsocketserver.ThreadingUDPServer
classsocketserver.ForkingUnixStreamServer
classsocketserver.ForkingUnixDatagramServer
classsocketserver.ThreadingUnixStreamServer
classsocketserver.ThreadingUnixDatagramServer

这些类都是使用混合类来预定义的。

Added in version 3.12: 增加了 ForkingUnixStreamServerForkingUnixDatagramServer 类。

要实现一个服务,你必须从 BaseRequestHandler 派生一个类并重定义其 handle() 方法。然后你可以通过组合某种服务器类型与你的请求处理器类来运行各种版本的服务。 请求处理器类对于数据报和流服务必须是不相同的。这可以通过使用处理器子类 StreamRequestHandlerDatagramRequestHandler 来隐藏。

当然,你仍然需要动点脑筋! 举例来说,如果服务包含可能被不同请求所修改的内存状态则使用分叉服务器是没有意义的,因为在子进程中的修改将永远不会触及保存在父进程中的初始状态并传递到各个子进程。 在这种情况下,你可以使用线程服务器,但你可能必须使用锁来保护共享数据的一致性。

另一方面,如果你是在编写一个所有数据保存在外部(例如文件系统)的 HTTP 服务器,同步类实际上将在正在处理某个请求的时候"失聪" -- 如果某个客户端在接收它所请求的所有数据时很缓慢这可能会是非常长的时间。这时线程或分叉服务器会更为适用。

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

另一种可以在既不支持线程也不支持 fork() 的环境(或者对于本服务来说这两者开销过大或不适用)中处理多个同时请求的方式是维护一个显式的部分完成的请求表并使用 selectors 来决定接下来要处理哪个请求(或者是否要处理一个新传入的请求)。 这对于流式服务来说特别重要,因为每个客户端可能会连接很长的时间(如果不能使用线程或子进程)。

Server 对象

classsocketserver.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 秒对 shutdown 进行轮询。忽略 timeout 属性。它还会调用 service_actions(),这可被子类或混合类用来提供某个给定服务的专属操作。 例如,ForkingMixIn 类使用 service_actions() 来清理僵尸子进程。

在 3.3 版本发生变更: service_actions 调用添加到 serve_forever 方法。

service_actions()

此方法会在 serve_forever() 循环中被调用。此方法可被子类或混合类所重载以执行某个给定服务的专属操作,例如清理操作。

Added in version 3.3.

shutdown()

通知 serve_forever() 循环停止并等待它完成。 shutdown() 必须在 serve_forever() 运行于不同线程时被调用否则它将发生死锁。

server_close()

清理服务器。此方法可被重载。

address_family

服务器套接字所属的协议族。常见的例子有 socket.AF_INET, socket.AF_INET6socket.AF_UNIX 等。如果你想要 IPv6 服务器类请子类化此模块中的 TCP 或 UDP 服务器类并设置类属性 address_family = AF_INET6.

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 版本发生变更: 添加了对 context manager 协议的支持。退出上下文管理器与调用 server_close() 等效。

请求处理器对象

classsocketserver.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 对象。

classsocketserver.StreamRequestHandler
classsocketserver.DatagramRequestHandler

这些 BaseRequestHandler 子类重载了 setup()finish() 方法,并提供了 rfilewfile 属性。

rfile

用于读取所接受请求的文件对象。支持 io.BufferedIOBase 可读接口。

wfile

用于写入所回复内容的文件对象。支持 io.BufferedIOBase 可写接口。

在 3.6 版本发生变更: wfile 也支持 io.BufferedIOBase 可写接口。

例子

socketserver.TCPServer 示例

以下是服务端:

importsocketserver
classMyTCPHandler(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.
 """
 defhandle(self):
 # self.request 是连接到客户端的 TCP 套接字
 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"))
 # 发回同样的数据,但转为大写形式
 self.request.sendall(self.data.upper())
 # 在我们返回后,套接字将被关闭。
if __name__ == "__main__":
 HOST, PORT = "localhost", 9999
 # 创建服务器,绑定到 localhost 的 9999 端口
 with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
 # 激活服务器;它将持续运行直到你
 # 使用 Ctrl-C 中断程序
 server.serve_forever()

一个使用流(通过提供标准文件接口来简化通信的文件型对象)的替代请求处理器类:

classMyTCPHandler(socketserver.StreamRequestHandler):
 defhandle(self):
 # self.rfile 是由该处理器创建的文件型对象。
 # 我们现在可以使用 readline() 代替原始 recv() 调用
 # 我们自己限制为 10000 字节以避免被发送方滥用。
 self.data = self.rfile.readline(10000).rstrip()
 print(f"{self.client_address[0]} wrote:")
 print(self.data.decode("utf-8"))
 # 类似地,self.wfile 是用于写回到客户端的
 # 文件型对象
 self.wfile.write(self.data.upper())

不同之处在于第二个处理器中的 readline() 调用将多次调用 recv() 直至遇到一个换行符,而第一个处理器必须使用一个 recv() 循环来累积数据直至遇到一个换行符。如果它只使用一个 recv() 而不带循环则将只返回当前已从客户端接收的内容。TCP 是基于流的:数据将按其发送顺序到达,但在客户端 send()sendall() 调用和服务端需要接收它的 recv() 调用次数之间并没有关联。

以下是客户端:

importsocket
importsys
HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])
# 创建一个套接字 (SOCK_STREAM 表示一个 TCP 套接字)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
 # 连接到服务器并发送数据
 sock.connect((HOST, PORT))
 sock.sendall(bytes(data, "utf-8"))
 sock.sendall(b"\n")
 # 从服务器接收数据并关闭
 received = str(sock.recv(1024), "utf-8")
print("Sent: ", data)
print("Received:", received)

这个示例程序的输出应该是像这样的:

服务器:

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

客户端:

$ pythonTCPClient.pyhelloworldwithTCP
Sent: hello world with TCP
Received: HELLO WORLD WITH TCP
$ pythonTCPClient.pypythonisnice
Sent: python is nice
Received: PYTHON IS NICE

socketserver.UDPServer 示例

以下是服务端:

importsocketserver
classMyUDPHandler(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().
 """
 defhandle(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()

以下是客户端:

importsocket
importsys
HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])
# SOCK_DGRAM 是用于 UDP 套接字的套接字类型
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 如你所见,没有 connect() 调用;UDP 没有连接。
# 数据是通过 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 类的示例:

importsocket
importthreading
importsocketserver
classThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
 defhandle(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)
classThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
 pass
defclient(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__":
 # 端口 0 表示选择任意一个未使用的端口
 HOST, PORT = "localhost", 0
 server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
 with server:
 ip, port = server.server_address
 # 启动一个服务器线程 -- 该线程将在此后
 # 为每个请求再启动一个线程
 server_thread = threading.Thread(target=server.serve_forever)
 # 在主线程终结时退出服务器线程
 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()

这个示例程序的输出应该是像这样的:

$ pythonThreadedTCPServer.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 系统平台上可用。