socketserver – 网络服务器框架
socketserver模块简化了编写网络服务器的任务
socketserver模块中有四个基本的服务器类:
- class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)
这个类使用TCP协议(Transmission Control Protocol传输控制协议),这个协议提供了在服务器和客户端之间的连续的数据流,如果bind_and_activate为True,构造器会自动的试图调用server_bind()和server_activate()方法。其它的参数会被传递到它的父类BaseServer - class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)
这个类使用UDP协议(User Datagram Protocol用户数据报协议),这种协议发送的是不连续的信息包,这些信息包到达目的地的顺序可能与发送的顺序不同,在传输过程中,信息包也有可能会丢失。它的参数与TCPServer一样. - class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)
这两个类不太常用,与上面两个类类似,但使用的是Unix域套接字(原文Unix domain socket不知道翻译的对不),它们在非Unix平台不可用。它们的参数与TCPServer一样。
以上四个类都是同步处理网络请求的,也就是说,处理完毕一个请求之后,才能继续处理下一个请求。在某些情况下,比如处理某个请求需要大量的计算,或者服务器需要返回大量的数据而客户端又处理得比较慢,导致处理一个请求需要很长时间,那么,简单的使用上面的四个类就不太合适了。解决方案是创建单独的进程或线程来处理耗时任务,ForkingMixIn 和 ThreadingMixIn 这两个混合类(mix-in class)可以用来支持异步行为。
创建一个网络服务器需要几个步骤:
- 创建一个请求处理器类(request handler class),这个类继承自BaseRequestHandler类,并且需要重写它的handle() 方法,这个方法用来处理传入的请求
- 实例化一个服务器类(上面提到的四个类之一),传入服务器地址和请求处理器类作为参数,建议把服务器对象与with语句搭配使用
- 调用服务器对象的handle_request() 或者 serve_forever() 方法,来处理一个或者多个请求
- 调用server_close() 方法来关闭socket(当然,如果你使用了with语句,就不需要手动关闭服务器了)
当混合ThreadingMinIn类来添加多线程连接行为的时候,你需要明确的声明当服务器遇到意外关闭的时候,你的线程应该如何应对。ThreadingMixIn类定义了一个deamon_threads属性,这个属性决定了服务器是否等待线程终结。如果你想要你的服务器自动处理等待线程的行为,就需要明确的设置这个属性,这个属性的默认值是False,意味着线程不是守护线程,True表示线程是守护线程。(当所有非守护线程都退出的时候,python也会退出)
对于之前提到的四个基本的服务器类,无论它们使用哪种网络协议,它们对外暴露的方法和属性都是一样的。
创建服务器的注意事项
下面的继承图中有五个类,其中四个类是四种类型的同步服务器类
注意UnixDataframServer类继承自UDPServer,而不是继承自UnixStreamServer,它们之间的区别仅仅是地址族(adress family)的差别
class socketserver.ForkingMixIn
class socketserver.ThreadingMixIn
多进程和多线程版本的每种类型的服务器类,可以通过混合上面两个类来实现,比如一个多线程的UDP服务器类可以通过下面的方式来创建:
class ThreadingUDPServer(ThreadingMixIn, UDPServer):
pass
混合类型(ThreadingMixIn)必须放在继承列表的首位,这样它才能覆盖UDPServer类的方法。通过设置混合类型提供的属性,可以改变底层服务器机制的行为。
下面所提到的ForkingMixIn类和其它的Forking类都只能在支持fork()的POSIX平台下使用。
- socketserver.ForkingMixIn.server_close()
这个方法会等待所有的子进程全部完成后关闭服务器,除非socketserver.ForkingMixIn.block_on_close属性被设置为False - socketserver.ThreadingMixIn.server_close()
这个方法等待所有的非守护线程完成后,关闭服务器,除非socketserver.ThreadingMixIn.block_on_close属性被设置为False。
通过设置ThreadingMixIn.daemon_threads为True可以将线程设置为守护线程(即服务器不会等待此线程完成后再退出)
Python 3.7版本的新变化:
socketserver.ForkingMixIn.server_close()
socketserver.ThreadingMixIn.server_close()
这两个方法现在会等待所有子进程和非守护线程完成后才会退出
并且添加了一个
socketserver.ForkingMixIn.block_on_close
属性,来选择性的加入3.7版本之前的行为
class socketserver.ForkingTCPServer
class socketserver.ForkingUDPServer
class socketserver.ThreadingTCPServer
class socketserver.ThreadingUDPServer
这些预定义的类就是使用上述的混合类定义的
实现一个服务,你需要子类化BaseRequestHandler并且重写它的handle()方法。通过组合某种类型的服务器类,你就可以运行多种类型的服务。请求处理器类必须与服务器类是不同的类。当然这些细节都可以无视,而直接使用现成的类StreamRequestHandler 和 DatagramRequestHandler。
当然,你仍然需要动动脑筋。比如,多进程服务器的进程之间会各自保留状态信息的副本,对状态信息的修改不会影响到其它进程,其它进程也不会感知到状态信息的修改,这种情况下(需要共享数据),可以考虑使用多线程服务器类,但是,既然涉及了共享数据,你又不得不考虑使用线程锁来解决数据同步的问题。
另一方面,如果你正在构建一个HTTP服务器,服务器的所有数据都是存储在外部的(比如存储在文件系统中),这样的话,当服务器正忙于处理一个请求的时候,这个服务器实际上会变成一个…“聋子”(意思是它听不到其它的请求),尤其是当这个正在被处理得请求需要消耗相当长时间的时候,或者客户端接收数据很慢的时候,在诸如此类的场景下,使用多线程或者多进程的服务器会比较好。
在有些情况下,同步的处理请求的一部分,剩余的部分放到新的进程中去完成,可能会更合适。这可以通过在一个同步服务器的请求处理器类的handle()方法中明确的创建新进程来实现。
如果一个环境既不支持多线程也不支持多进程(可能支持这些功能的代价太大,或者并不适合此服务器),那么还有另一种方法来处理高并发的请求:维护一个明确的记录未处理完成的请求的表格,使用selectors来决定下一步该处理表格中的哪个请求,或者是否处理一个新请求。这对于那些提供流服务的服务器特别重要,因为它的客户端可能会连接很长时间(如果不支持多线程或多进程的话),asyncore模块提供了另一种处理此类问题的方法。
服务器对象
class socketserver.BaseServer(server_address, RequestHandlerClass)
这是这个模块中所有其它服务器类的父类。它定义了通用接口,大多数底层的但并未实现的方法,这些方法将在子类中实现。两个参数分别存储在server_address和RequestHandlerClass属性中。
-
fileno()
返回服务器正在监听的套接字的int类型的文件描述符。通常情况下,这个函数会被传递给选择器(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()来清理僵尸子进程
Python 3.3 的变化:
为serve_forever方法添加service_actions方法 -
service_actions()
这个方法在serve_forever()方法中被调用。
这个方法可以被子类或混合类重写,来为特定的服务提供专属操作,比如清理操作
Python 3.3 版本新添加的 -
shutdown()
让serve_forever()循环退出,并等待它完成退出
shutdown()必须在和serve_forever()不同的线程中调用,否则会造成死锁。 -
server_close()
清理服务器,可以重写。 -
address_family
此属性表示服务器的套接字所属的协议族,常见的有:
socket.AF_INET
socket.AF_UNIX -
RequestHandlerClass
用户提供的请求处理器类,对于每个请求,都会创建一个这个类的实例。 -
server_address
服务器所监听的地址。
不同的协议族有不同的地址格式,查看socket模块的文档来获取更详细的信息
对于IP协议,地址是一个拥有两个元素的元组:一个字符串类型的IP地址和一个整数类型的端口号,例如(‘127.0.0.1’,80) -
socket
服务器所监听的传入请求的套接字
前述的几种服务器类支持以下一些类变量:
- allow_reuse_address
服务器是否允许重用一个地址。这个变量默认为False,并且可以在子类中设置,来改变策略 - request_queue_size
请求队列的大小。如果服务器处理某个请求需要花费大量的时间,那么当服务器忙碌时传入的请求会被放入一个队列,这个队列最多容纳request_queue_size个请求。一旦队列装满,后续的请求会得到一个“拒绝连接”(Connection denied)的错误。变量的默认值是5,但可以在子类中重写。 - socket_type
服务器所使用的套接字的类型,通常的取值是:
socket.SOCK_STREAM
socket.SOCK_DGRAM - timeout
超时时间,以秒为单位,设置为None表示不需要超时时间。
如果handle_request()方法在timeout秒之内没有收到任何传入的请求,则handle_timeout()方法将被调用。
还有一些服务器方法,可以被BaseServer的子类(比如TCPServer)所重写。这些方法对于服务器对象的外部用户来说,并没有什么卵用。
- finish_request(request, client_address)
这是实际处理请求的地方,负责实例化RequestHandlerClass并且调用这个实例的handle()方法 - get_request()
必须接受一个来自套接字的请求,并且返回一个包含了新套接字对象和客户端地址的二元组。 - handle_error(request, client_address)
当RequestHandlerClass实例的handle()方法抛出异常(Exception)时,这个方法会被调用。默认的行为是向标准错误流打印追踪栈信息,并且继续处理后续的请求。
Python 3.6 的变化:
只在抛出由Exception派生的异常时,才会调用此方法。 - handle_timeout()
当timeout属性设置了某个非None的值,并且超过timeout时间后仍然没有收到请求,则这个方法会被调用。在多进程的分支服务器上,默认的行为是收集所有已经退出的子进程的状态,而在多线程服务器上,这个方法什么都不做。 - process_request(request,client_address)
调用finish_request()方法来实例化RequestHandlerClass。这个方法也可以被子类重写,创建新的进程或线程来处理请求。ForkingMixIn和ThreadingMixIn类已经做好了这些事情。 - server_activate()
由服务器的构造器调用,来激活服务器。在TCP服务器上,这个方法的默认行为就是调用套接字对象的listen()方法。可以被子类重写。 - server_bind()
由服务器的构造器调用,将套接字与指定的地址绑定起来,可以被子类重写。 - verify_request(request, client_address)
必须返回一个布尔值;返回True,则请求会被处理,返回False,则请求会被拒绝。这个方法可以被重写,用来实现服务器的访问控制。默认总是返回True。
Python 3.6 的变化:
添加了对上下文管理器协议的支持(即with语句块)。
退出上下文管理器等同于调用server_close()方法。
请求处理器对象
class socketserver.BaseRequestHandler
这是所有请求处理器类的超类。它定义了底层的接口。
一个具体的请求处理器子类必须实现新的handle()方法,并重写其它一些方法。
对于每个请求,都会创建一个新的子类的实例
- setup()
在handle()方法之前调用,来执行一些必要的初始化动作。
默认实现什么都不做。 - handle()
服务一个请求所需要的所有工作都由这个函数来完成。
默认实现什么都不做。
它(应该指的是请求处理器对象)有几个可用的实例属性,能够用来访问服务器信息:
self.request 请求
self.client_address 客户端地址
self.server 服务器对象实例
对于数据报(datagram)和流(stream)服务来说,它们所对应的self.request的类型是不同的。
对于流服务,self.request 是一个套接字对象
对于数据报服务,self.request 是一个包含字符串和套接字对象的二元组。 - finish()
在handle()之后调用,执行一些必要的清理工作。
默认实现什么都不做。
如果setup()抛出异常,则这个函数不会被调用。
class socketserver.StreamRequestHandler
class socketserver.DatagramRequestHandler
这些BaseRequestHandler的子类重写了setup() 和 finish() 方法,并且提供self.rfile 和 self.wfile 属性。
self.rfile 和self.wfile 属性可以分别读取数据或者将数据返回给客户端。
这两个类的 rfile 属性支持 io.BufferedIOBase 可读接口。
这两个类的 wfile 属性支持 io.BufferedIOBase 可写接口。
Python 3.6 的变化:
StreamRequestHandler.wfile 也支持 io.BufferedIOBase 可写接口。
示例
socketserver.TCPServer 示例
这是服务器端:
import socketserver
class MyTCPHandler(socketserver.BaseRequestHandler):
"""
我们的服务器的请求处理器.
对于每一个连接,都会实例化一次请求处理器,
而且它必须重写handle()方法来实现与客户端的交流.
"""
def handle(self):
# self.request 是连接到客户端的TCP类型的套接字
self.data = self.request.recv(1024).strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
# 这里仅仅回传了相同的内容,只不过转换成了大写
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()
一个利用流的请求处理器类的替代品(这是一个类文件对象,通过与标准文件对象一样的接口来与客户端进行简单的交流)
class MyTCPHandler(socketserver.StreamRequestHandler):
def handle(self):
# self.rfile 是请求处理器所创建的类文件对象;
# 我们可以使用 readline() 来替代原来的 recv() 调用
self.data = self.rfile.readline().strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
# 类似的, self.wfile 是一个用来将数据写回客户端的类文件对象
self.wfile.write(self.data.upper())
它们之间的不同点在于:第二个处理器的readline()方法会多次调用recv()方法,直到遇到一个换行符;然而,第一个处理器的recv()方法会返回由客户端的sendall()方法发送的全部内容。
这是客户端:
import socket
import sys
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 + "\n", "utf-8"))
# 从服务器接收数据,然后关闭
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):
"""
这个类工作起来跟TCP处理器是类似的,
只不过 self.request 是一个包含数据和
客户端套接字的二元组
因为这里没有连接, 所以在通过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 代表使用的是 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: {}".format(data))
print("Received: {}".format(received))
这个示例的输出与之前 TCP 的版本一样
异步混合类示例
要构建一个异步处理器, 可以使用 ThreadingMixIn 和 ForkingMixIn 类.
一个 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__":
# 端口 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()
示例的输出应该看起来像这样:
$ 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 平台下可用。
关于此翻译
翻译这篇文档的初衷是,翻译者是个英语渣,然而又在学习Python的这个模块,看英文文档实在是太慢了,也不方便回过头来复习,所以索性在学习的过程中顺便把中文写下来,方便以后回来看
这篇文档是基于Python 3.9.5 官方文档翻译的
原文地址: socketserver
刚才也说了,翻译者是个英语渣,所以翻译过程中难免会有错误,对于模块功能的理解也可能不全面,还请各路大神指正,先行谢过,膜拜!!