Python网络编程(3)——SocketServer模块与简单并发服务器

时间:2022-05-18 23:58:24

 

主要类型

  该模块有四个比较主要的类,其中常用的是 TCPServer 和 UDPServer。

  1. TCPServer

  2. UDPServer

  3. UnixStreamServer,类似于TCPServer提供面向数据流的套接字连接,但是旨在UNIX平台上可用;

  4. UnixDatagramServer,类似于UDPServer提供面向数据报的套接字连接,但是旨在UNIX平台上可用;

  这四个类型同步地处理请求,也就是说一个请求没有完成之前是不会处理下一个请求的,这种模式当然不适合生产环境,一个客户端连接就可能拖延所有的执行。所以这个模块还提供了两种支持异步处理的类: 

  5. ForkingMixIn,为每一个客户端请求派生一个新的进程去专门处理;

  6. ThreadingMixIn,为每一个客户端请求派生一个新的线程去专门处理;

  继承自这两个类型的服务端在处理新的客户端连接时不会阻塞,而是创建新的进/线程专门处理客户端的请求。

编程框架 

  首先从高层面介绍一下使用SocketServer模块开发多进程/线程 异步服务器的流程:

  1. 根据需要选择一个合适的服务类型,如,面向TCP连接多进程服务器:  ForkingTCPServer 

  2. 创建一个请求处理器(request handler)类型,这个类型的 handle()(类似于回调函数)方法中定义如何处理到达的客户端连接。

  3. 实例化服务器,传入服务器绑定的地址和第2步定义的请求处理器类

  4. 调用服务器实例的 handle_request() 或 serve_forever() 方法,一次或多次处理客户请求。

 

具体流程

1. 选择合适的服务器类型

  上文介绍了SocketServer模块提供的几种主要的基本服务类型和两种MixIn类型,就是构造适合需求的多线程/进程服务器的原料。是否使用、如何使用这两个MixIn类型是由程序员决定的,利用的就是Python的多重继承机制,将MixIn类型作为代码库(而不是初始化实例的工具),为实例提供新的方法。

  SocketServer模块提供的主要服务类型和两种MixIn类型,可以有以下的组合:

 

TCPServer

UDPServer
ForkingMixIn ForkingTCPServer

ForkingUDPServer

ThreadingMixIn ThreadingTCPServer

ThreadingUDPServer

 

  只要根据需要选择特定类型的server类型即可(例如 ThreadingTCPServer,面向TCP连接的多线程服务器),即便自己定义多进程/线程的server类型,也不过多重继承对应的连接server类和MinIn而已。比如,面向TCP连接线程式异步服务器类型,实际上就是:

class ForkingTCPServer(FrokingMixIn, TCPServer):
pass

  Python 的多重继承机制保证了这里只要继承了必要的父类就可以完成目标类型的定义,不需要添加额外的内容。

 

2. 定义请求处理器

  SocketServer模块提供了 BaseRequestHandler 类型用于定制Handler类型,自定义的Handler类型只要继承自 BaseRequestHandler 并覆写它的 handle() 方法即可。handle() 方法定义如何处理客户端的请求,服务器只是封装了socket对象的众多操作流程以及进程、线程等的管理,然后对于每一个客户端请求调用handle() 方法。handle() 方法就是server为client创建新的线程后调用的回调函数。

 

  BaseRequestHandler 实例的一些属性非常有用,可以用来获得一些和连接相关的信息,包括客户端套接字的地址、服务端当前连接的socket对象等:

(1)获取client端socket对象的地址

h.client_address

  h.client_address 是 client 的地址,IPv4地址族中就是 (host, port) 二元组。该属性由基类在连接建立时设置。

 

(2)获取创建自己的 server 对象

h.server

  该属性保存创建这个 BaseRequestHandler 实例的 server 对象。

 

(3)从 BaseRequestHandler 实例获取连接套接字

h.request
  • 对 TCP server,h.request 属性是连接到 client 的连接套接字对象;
  • 对 UDP server,h.request 属性是一个二元组(data, sock),data 是 client 端发送的数据(最大8192字节),sock是server端套接字。

  使用这个属性可以获取在这个进/线程中与client套接字建立连接的连接套接字,从而可以使用这个套接字与client端通信。

   StreamRequestHandler 和 DatagramRequestHandler 则屏蔽了 self.request 对TCP和UDP连接的区别,二者都重定义了 setup() 和 finish() 方法,提供统一的 self.rfile 和 self.wfile 属性以便从客户端读取数据或向客户端发送数据。

 

  BaseRequestHandler 实例 h 提供如下的接口,他们都可以根据需要重写:

(1)初始化

BaseRequestHandler.setup()

  在 handle() 方法之前调用 setup() ,完成一些初始化的工作,默认什么也不做。

  

(2)回调函数 handle()

BaseRequestHandler.handle()

  handle() 完成所有的对于每个请求的处理工作,也就是实现服务端的业务逻辑,默认情况下什么也不做。要与 client 端通信,最终还是要通过建立连接的套接字对象,这里可以使用 h.request 属性获取连接套接字,对于TCP服务器和UDP服务器的区别,参考 h.request 属性。

  

(3)终止化

BaseRequestHandler.finish()

  作用:在handle() 方法之后调用,完成一些清理的工作,默认的情形下什么也不做。如果setup()方法抛出异常,那么该方法不会被调用。

 

 3. 实例化服务器

  实例化服务器时传入服务器需要绑定的地址是必要的,另一方面还应该传入自定义的Handler类型,服务器实例将对每一个客户端连接调用它的 handle() 方法。

  例如:

server = ForkingTCPServer((host, port), MyRequestHandler)

 

4. 调用服务器实例的处理方法

  服务器实例的 handle_request() 方法与 serve_forever() 方法分别用于单次处理或一直处理请求,可以直接在脚本中调用这些方法,也可以在新的进程或者线程中调用这些方法,启动服务器:

import threading

...

server = ForkingTCPServer((host, port), MyRequestHandler)
server_thread = threading.Thread(target = server.serve_forever)
server_thread.start()

   则在一个新的线程中创建一个 多进程的TCP服务器,每当一个新的连接到来,他都会创建一个新的进程去服务client端的请求。

 

实例:

  该例子使用 SocketServer 模块实现一个简单的多线程 TCP 服务器

import SocketServer
class EchoHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        print("Connected from: ", self.client_address)
        while True:
            recvData = self.request.recv(1024)
            if not recvData:
                break
            self.request.sendall(recvData)
        self.request.close()
        print("Disconnected from: ", self.client_address)

srv = SocketServer.ThreadingTCPServer(("", 4424), EchoHandler)
srv.serve_forever()

   该例在当前线程中创建一个多线程TCP服务器,其功能是将客户端发送的数据回显给客户。

   可见,在handle()中定义服务器的业务逻辑,任何需要对连接socket对象的操作,都可以通过 self.request 属性操作,在TCP连接中,这个属性就是server端的socket对象。在定义完Handler类型后,将其作为参数传给选择的server类型即可。

  该多线程TCP server 的运行示例:

('Connected from: ', ('127.0.0.1', 63235))
('Connected from: ', ('127.0.0.1', 63236))
('Disconnected from: ', ('127.0.0.1', 63235))
('Disconnected from: ', ('127.0.0.1', 63236))

  这里发起两个客户端同时请求服务端,发现服务端确实具有了基本的并发能力。


 

补充:

  SocketServer 中的 TCPServer、UDPServer 提供的可供使用的属性、方法(实际上都来自于其父类 SocketServer.BaseServer ):

属性:

 BaseServer.address_family 

  内容:服务器套接字对象的地址族,如 socket.AF_INET 、 socket.AF_UNIX 等。

 

 BaseServer.RequestHandlerClass 

  内容:用户自定义,传给服务器构造函数Handler类型,服务器会为每一个请求创建一个该类型的实例。

 

 BaseServer.server_address 

  内容:服务器监听的地址,具体的形式依赖于地址族,如AF_INET形式的 ('127.0.0.1', 80) 等。

 

 BaseServer.socket 

  内容:服务器监听的套接字对象

 

 BaseServer.allow_reuse_address 

  内容:是否允许地址重用,默认为False,子类可以更改。

 

 BaseServer.request_queue_size 

  内容:请求队列的长度,一旦等待服务的请求数达到这个限制,后续到来的请求收到“Connection denied”错误,通常该值默认为5,子类可以覆写。

  

 BaseServer.socket_type 

  内容:服务器所用套接字的类型,如 socket.SOCK_STREAM 和 socket.SOCK_DGRAM 。

 

 BaseServer.timeout 

  内容:服务器的超时限制,如果是None,那么没有设置超时;如果在指定的时限内 handle_request() 方法没有获得请求,那么将会调用 handle_timeout() 方法。

 

 BaseServer.fileno() 

  内容:返回服务器所用套接字对象的fd,一种典型的用法是传给 select.select() 方法便于在一个进程中监控多个服务器。

 

方法:

 BaseServer.server_bind()

  作用:由实例的构造函数调用,将套接字绑定到目标地址,可以覆写。

 

 BaseServer.server_activate() 

  作用:由实例的构造函数调用,监听服务器套接字,可以覆写。

 

 BaseServer.handle_request() 

  作用:处理单个请求,依次调用 get_request() 、 verify_request() 和 process_request() 方法,如果用户自定义的handle()方法抛出异常,则调用 handle_error() 方法,如果超过 self.timeout 秒没有获得请求,调用 handle_timeout() 方法,然后 handle_request() 方法返回。

 

   BaseServer.get_request()

    作用:服务器对象的使用者不一定需要直接调用该方法,从套接字接受一个请求。返回一个二元组,首元是新的已经连接的套接字对象,次元是客户端的地址。

 

   BaseServer.verify_request(request, client_address) 

    作用:服务器对象的使用者不一定需要直接调用该方法。返回一个布尔值,为True时处理请求,为False时拒绝请求,可以通过覆写该方法为服务器实现访问控制。默认的实现总是返回True。

 

   BaseServer.process_request(request, client_address) 

    作用:该方法体现MixIn的作用,此处可能会创建线程或进程来处理用户的请求,最终都是该方法通过调用 finish_request() 来为每个请求实例化一个用户自定义的Handler实例,然后投到新建的线程或进程中,处理用户的请求。

 

     BaseServer.finish_request() 

      作用:为每个请求实例化一个用户自定义的Handler实例,并调用其 handle() 方法。

 

   BaseServer.handle_error(request, client_address) 

    作用:当用户自定义的Handler实例的 handle() 方法抛出异常时,调用该方法。默认的工作是将traceback打印到标准输出,然后继续处理请求。

 

   BaseServer.handle_timeout() 

     作用:当self.timeout属性规定的超时上限达到时(不是None)还没有等到请求。多进程服务器的默认行为是收集所有已经退出的子进程的状态,多线程服务器默认什么也不做。

 

 BaseServer.serve_forever(poll_interval=0.5) 

  作用:一直处理请求,直到显式调用 shutdown() 函数,每隔 poll_interval (默认0.5)秒轮询一遍shutdown,该函数无视 self.timeout 。

 

 BaseServer.shutdown() 

  作用:停止 serve_forever() 循环直到其停止。

 


Contact_me: darren_wang_a^t_outlook.com