python网络编程之socketserver

关于socket的基础介绍可以点击python网络编程之socket查看。

socketserver

简介

socket并不能多并发,只能支持一个用户。socketserver则实现了并发处理。当有多个客户端连接时,socketserver都会在服务器上创建一个线程或进程来处理该客户端的请求,一个客户端对应一个服务端的进程或线程,这样增加系统的利用率。
socketserver是socket的封装,简化了编写网络服务程序的任务。python2中为SocketServer,python3中取消了首字母大写,改为socketserver

socketserver中包含了两种类,一种为服务类(server class),一种为请求处理类(request handle class)。前者提供了许多方法,像绑定、监听、运行(建立连接的过程),后者则专注于如何处理用户所发送的数据(事务逻辑)。一般情况下,所有的服务都是先建立连接(也就是建立服务类的实例),然后开始处理用户请求(也就是建立请求处理类的实例)。
socketserver有4个类,分别是TCPServerUDPServerUnixStreamServerUnixDatagramServer

  1. class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)
    TCP协议
  2. class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)
    UDP协议,传输过程中可能会造成数据丢失等情况。
  3. class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
    tcp协议,用于unix机器的进程间通信,不可用于windows主机。
  4. class socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)
    udp协议,用于unix机器的进程间通信,不可用于windows主机。
  • 继承关系
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    +------------+
    | BaseServer |
    +------------+
    |
    v
    +-----------+ +------------------+
    | TCPServer |------->| UnixStreamServer |
    +-----------+ +------------------+
    |
    v
    +-----------+ +--------------------+
    | UDPServer |------->| UnixDatagramServer |
    +-----------+ +--------------------+

上述的4个类用于处理同步的请求,也就是当前请求必须处理完成才能开始下一个请求。不适用于单个请求处理时间很长的情况。
单个请求处理需要很长时间的情况,可以创建一个单独的线程或进程去处理每个请求,ForkingMixInThreadingMixIn类支持异步请求。
当从ThreadingMixIn继承线程连接时,应该明确声明线程意外关闭时的行为。ThreadingMixIn类中定义了一个属性daemon_threads,它用来标识服务器是否等待线程终止。如果希望线程自动执行,应该明确设置标识,默认情况下是False,也就是python将会在ThreadingMixIn创建的所有线程都退出之后才退出。
不论采用何种协议,服务类(server class)的外部方法和属性都是相同的。

创建

  1. 创建一个请求处理类,并且这个类要继承BaseRequsetHandler类,重写父类的handle()方法。
  2. 实例化一个服务类,并给它传递服务端IP地址和上面的请求处理类。推荐在server使用with语句。
  3. 调用服务对象的handle_request()或者serve_forever()方法,前者用于处理一个请求,后者用于处理多个请求。
  4. 调用server_close()关闭socket(使用with语句则不需要)。

Server 创建说明

class socketserver.ForkingMixIn
class socketserver.ThreadingMixIn
创建forking和threading类型的服务端可以使用上面的mix-in类。例如,ThreadingUDPServer创建方法:

1
2
class ThreadingUDPServer(ThreadingMixIn, UDPServer):
pass

先写mix-in类是因为它重写了UDPServer中定义的一个方法。设置各种属性也会改变底层服务机制的运行。
下面提到的ForkingMixIn和Forking类只能在支持fork()的POSIX平台使用。
class socketserver.ForkingTCPServer
class socketserver.ForkingUDPServer
class socketserver.ThreadingTCPServer
class socketserver.ThreadingUDPServer
上面的几个类都是在mix-in类中预定义好的。
要实现服务,定义的类必须继承自BaseRequestHandler,并且要重新定义handle()方法。接下来将其中的server类和request handler类结合起来,就可以跑不同的服务。datagramstream服务对应的request handler类不能相同。这个可以通过使用handler的子类StreamRequestHandlerDatagramRequestHandler隐式实现。
对于何时应该选用何种方式的服务,还是需要根据实际情况分析。

Server 对象

class socketserver.BaseServer(server_address, RequestHandlerClass)
BaseServer是所有Server对象的超类,它定义了下面的这些接口,但大多数都未实现,在子类中细化。

  • fileno()
    返回server监听socket的文件描述符,整数。通常用来传递给select.select()以允许一个进程监视多个服务器。
  • handle_request()
    处理单个请求。该函数依次调用get_request()verify_request()process_request()方法。假如用户提提供的handle()方法抛出异常,将会调用服务端的handle_error()方法。如果在timeout时间(单位:s)内未接到用户请求,将会调用handle_timeout()并返回handle_request()
  • serve_forever(poll_interval=0.5)
    处理请求,直接收到一个明确的shutdown()请求。每隔pool_interval时间轮询一次shutdown。忽略timeout属性。
  • server_actions()
    serve_forever()循环中调用。该方法可以在子类中重写,为指定的服务执行指定的动作。
  • shutdown()
    告诉serve_forever()循环停止并等待其停止。
  • server_close()
    清理服务。可以被重写。
  • address_family()
    server socket所属的协议簇。例如socket.AF_INETsocket.AF_UNIX
  • RequestHandlerClass
    用户提供的请求处理类。这个类为每个请求创建实例。
  • server_address
    server监听的地址。地址的格式由协议决定。对于Internet protocols,就是一个元组,包括字符串类型的地址和整型的端口号。

    Request Handler 对象

    class socketserver.BaseRequestHandler
    这是所有request handler对象的超类。它定义了下面的接口。一个具体的request handler子类必须定义一个新的handle()方法,并且可以重写任何其他的方法。会为每一个请求创建子类的实例。
  • setup()
    hanlde()方法自行任何初始化动作之前被调用。默认什么也不做。
  • handle()
    该函数处理服务请求的所有工作。默认什么也不做。可用的几个实例的属性:self.requestself.client_addressself.server
    对于stream服务,self.request返回一个socket对象。对于datagram服务,self.request返回string和socket
  • finsh()
    handle()方法之后被调用,执行一些清理动作。默认什么也不做。假如setup()出现异常,这个函数将不会被调用。
    class socketserver.StreamRequestHandler
    class socketserver.DatagramRequestHandler
    上面的2个BaseRequestHandler的子类重写了setup()finsh()方法,同时提供self.rfileself.wfile属性。self.rfileself.wfile可以读取或写入,以获得请求数据或将数据返回到客户端。

    实例

    服务端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import socketserver

    class MyTcpHandler(socketserver.BaseRequestHandler):
    def handle(self):
    while True:
    try:
    self.data = self.request.recv(1024).strip()
    print("{} wrote:".format(self.client_address[0]), end=" ")
    print(self.data)
    self.request.sendall(self.data.upper())
    except ConnectionResetError as e:
    print("err ", e)
    break

    if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    with socketserver.ThreadingTCPServer((HOST, PORT), MyTcpHandler) as server:
    server.serve_forever()
客户端
1
2
3
4
5
6
7
8
9
10
11
import socket

client = socket.socket()
client.connect(('localhost', 9999))
while True:
msg = input(">>>").strip()
if len(msg) ==0:continue
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print("recv:>",data.decode())
client.close()

实现效果
在客户端输入内容,服务端可看到输入内容;
多个客户端可同时连接,服务端可分别显示不同客户端显示的内容。
更多关于socketserver的内容点击socketserver — A framework for network servers查看官方介绍。

select/poll/epoll

select/poll/epoll也是IO multiplexing(I/O多路复用),有时候也称这种I/O方式为event driven IO。它的好处在于单个process就可以同时处理多个网络连接的I/O。
基本原理是select/poll/epoll函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

select

通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
selecet目前几乎所有的平台都支持。
一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有的socket进行一次线性扫描,所以这也浪费了一定的开销。

poll

它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

epoll

epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

更多关于select模块的内容点击select — Waiting for I/O completion查看官方介绍。
网络编程更多的关于事件驱动模型异步I/OI/O多路复用的内容可以参考网络编程基础

Recommended Posts