12.6. socketserver — 创建网络服务器

目的:创建网络服务器。

socketserver 模块是一个创键网络服务器的框架。它定义了通过TCP,UDP,Unix 流和 Unix 数据报处理同步网络请求(服务器请求处理程序阻塞,直到请求完成)的类。它还提供了混合类,可以轻松转换服务器,为每个请求使用单独的线程或进程。

处理请求的责任在服务器类和请求处理程序类之间分配。服务器处理通信问题,例如侦听套接字和接受连接,请求处理程序处理“协议”问题,如解释传入数据,处理数据以及将数据发送回客户端。这种责任划分意味着许多应用程序可以使用现有服务器类之一而无需任何修改,并为其提供请求处理程序类以使用自定义协议。

服务器类型

socketserver中定义了五种不同的服务器类。BaseServer定义了API,并不打算实例化和直接使用。TCPServer使用TCP / IP套接字进行通信。UDPServer使用数据报套接字。UnixStreamServer 和 UnixDatagramServer 使用 Unix 域套接字,仅在Unix平台上可用。

服务器对象

要构造服务器,请向其传递一个侦听请求的地址和一个请求处理程序(不是实例)。地址格式取决于服务器类型和使用的套接字系列。参考 socket 模块文档了解详情。

实例化服务器对象后,使用 handle_request()serve_forever() 来处理请求。serve_forever() 方法在无限循环中调用 handle_request(),但是如果应用程序需要将服务器与另一个事件循环集成,或者使用 select() 监视不同服务器的多个套接字,它可以直接调用 handle_request()

实现服务器

我们创建服务器时,一般要充分利用现存的类并且要写一些自定义的请求处理类。不过对于普通情况,BaseServer 包含几个可以被子类覆盖的方法,我们可以基于它来实现。

  • verify_request(request, client_address): 返回 True 表示要处理它,False 则是忽略该请求。比如,服务器会拒绝超过 IP 范围限制的请求。
  • process_request(request, client_address): 调用 finish_request() 来做处理请求的实际工作。它还可以创建独立的线程或进程作为混合类使用。
  • finish_request(request, client_address): 基于服务器构造器创建处理请求的处理器实例。后续调用请求处理器的 handle() 处理请求。

请求处理器

请求处理器所做的工作是接收即将到来的请求并决定下一步的执行。处理器负责实现套接字层之上的协议(HTTP,XML-RPC,AMQP 等)。请求处理器从数据通道中读取请求,然后处理请求,最后进行响应回复。类中有3个可以被覆盖的方法。

  • setup(): 配置请求处理器以处理请求。StreamRequestHandler 类中的 setup() 方法会创建类文件对象来读取或写入套接字。
  • handle(): 在这里处理请求。负责编译请求,处理数据,发送响应。
  • finish(): 清理 setup() 所创建的任何东西。

大部分处理器实现时一般都只覆盖 handle() 方法。

回显例子

本例实现一组简单的服务器/请求处理器来接受 TCP 连接之后返回任何客户端发过来的数据。让我们从写请求处理器开始。

socketserver_echo.py

import logging
import sys
import socketserver

logging.basicConfig(level=logging.DEBUG,
                    format='%(name)s: %(message)s',
                    )

class EchoRequestHandler(socketserver.BaseRequestHandler):

    def __init__(self, request, client_address, server):
        self.logger = logging.getLogger('EchoRequestHandler')
        self.logger.debug('__init__')
        socketserver.BaseRequestHandler.__init__(self, request,
                                                 client_address,
                                                 server)
        return

    def setup(self):
        self.logger.debug('setup')
        return socketserver.BaseRequestHandler.setup(self)

    def handle(self):
        self.logger.debug('handle')

        # 把数据返回给客户端
        data = self.request.recv(1024)
        self.logger.debug('recv()->"%s"', data)
        self.request.send(data)
        return

    def finish(self):
        self.logger.debug('finish')
        return socketserver.BaseRequestHandler.finish(self)

唯一需要实现的方法其实只有 EchoRequestHandler.handle(),不过我们实现的方法如前一样包含一些说明调用步骤的信息。EchoServer 除了每次调用方法时多一些输出日志外其他与 TCPServer 并无不同。

class EchoServer(socketserver.TCPServer):

    def __init__(self, server_address,
                 handler_class=EchoRequestHandler,
                 ):
        self.logger = logging.getLogger('EchoServer')
        self.logger.debug('__init__')
        socketserver.TCPServer.__init__(self, server_address,
                                        handler_class)
        return

    def server_activate(self):
        self.logger.debug('server_activate')
        socketserver.TCPServer.server_activate(self)
        return

    def serve_forever(self, poll_interval=0.5):
        self.logger.debug('waiting for request')
        self.logger.info(
            'Handling requests, press <Ctrl-C> to quit'
        )
        socketserver.TCPServer.serve_forever(self, poll_interval)
        return

    def handle_request(self):
        self.logger.debug('handle_request')
        return socketserver.TCPServer.handle_request(self)

    def verify_request(self, request, client_address):
        self.logger.debug('verify_request(%s, %s)',
                          request, client_address)
        return socketserver.TCPServer.verify_request(
            self, request, client_address,
        )

    def process_request(self, request, client_address):
        self.logger.debug('process_request(%s, %s)',
                          request, client_address)
        return socketserver.TCPServer.process_request(
            self, request, client_address,
        )

    def server_close(self):
        self.logger.debug('server_close')
        return socketserver.TCPServer.server_close(self)

    def finish_request(self, request, client_address):
        self.logger.debug('finish_request(%s, %s)',
                          request, client_address)
        return socketserver.TCPServer.finish_request(
            self, request, client_address,
        )

    def close_request(self, request_address):
        self.logger.debug('close_request(%s)', request_address)
        return socketserver.TCPServer.close_request(
            self, request_address,
        )

    def shutdown(self):
        self.logger.debug('shutdown()')
        return socketserver.TCPServer.shutdown(self)

最后一步我们来写一下主程序,我们将服务器运行在一个线程中,然后发送一些数据来看一下返回一个数据要调用哪些方法。

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0)  # 让内核去分配端口
    server = EchoServer(address, EchoRequestHandler)
    ip, port = server.server_address  # 获取分配到的端口

    # 在一个线程中启动服务器
    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # 不要退出了。
    t.start()

    logger = logging.getLogger('client')
    logger.info('Server on %s:%s', ip, port)

    # 连接服务器
    logger.debug('creating socket')
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    logger.debug('connecting to server')
    s.connect((ip, port))

    # 发送数据
    message = 'Hello, world'.encode()
    logger.debug('sending data: %r', message)
    len_sent = s.send(message)

    # 接收响应
    logger.debug('waiting for response')
    response = s.recv(len_sent)
    logger.debug('response from server: %r', response)

    # 执行清理
    server.shutdown()
    logger.debug('closing socket')
    s.close()
    logger.debug('done')
    server.socket.close()

我们运行后会看到如下信息

$ python3 socketserver_echo.py

EchoServer: __init__
EchoServer: server_activate
EchoServer: waiting for request
EchoServer: Handling requests, press <Ctrl-C> to quit
client: Server on 127.0.0.1:55484
client: creating socket
client: connecting to server
client: sending data: b'Hello, world'
EchoServer: verify_request(<socket.socket fd=7, family=AddressFamily
.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1',
55484), raddr=('127.0.0.1', 55485)>, ('127.0.0.1', 55485))
EchoServer: process_request(<socket.socket fd=7, family=AddressFamil
y.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1',
 55484), raddr=('127.0.0.1', 55485)>, ('127.0.0.1', 55485))
EchoServer: finish_request(<socket.socket fd=7, family=AddressFamily
.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1',
55484), raddr=('127.0.0.1', 55485)>, ('127.0.0.1', 55485))
EchoRequestHandler: __init__
EchoRequestHandler: setup
EchoRequestHandler: handle
client: waiting for response
EchoRequestHandler: recv()->"b'Hello, world'"
EchoRequestHandler: finish
client: response from server: b'Hello, world'
EchoServer: shutdown()
EchoServer: close_request(<socket.socket fd=7, family=AddressFamily.
AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 5
5484), raddr=('127.0.0.1', 55485)>)
client: closing socket
client: done

注意

每次运行程序时端口都会变化,因为内核会自动分配一个可用的端口。如果你想每次都是同一个特定端口的话,在地址元组中分配具体的端口号来代替 0

下面是更加简洁的实现,去除了日志部分。只重写了 handle() 方法。

socketserver_echo_simple.py

import socketserver

class EchoRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # 返回数据给客户端
        data = self.request.recv(1024)
        self.request.send(data)
        return

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0)  # 让内核去分配端口
    server = socketserver.TCPServer(address, EchoRequestHandler)
    ip, port = server.server_address  # 获取分配的端口

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # 不要退出了。
    t.start()

    # 连接服务器
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # 发送数据
    message = 'Hello, world'.encode()
    print('Sending : {!r}'.format(message))
    len_sent = s.send(message)

    # 接收数据
    response = s.recv(len_sent)
    print('Received: {!r}'.format(response))

    # 执行清理
    server.shutdown()
    s.close()
    server.socket.close()

本例比较简单,因为 TCPServer 基本上帮我们都写好了。

$ python3 socketserver_echo_simple.py

Sending : b'Hello, world'
Received: b'Hello, world'

线程和进程

要让服务器支持线程或者进程,需要包含合适的混合式(mix-in)类作为继承对象。混合式类会覆盖 process_request() 方法,覆盖后当有请求需要处理时会开启一个新的进程或线程去执行。

使用线程的话,混合式类为 ThreadingMixIn

socketserver_threaded.py

import threading
import socketserver

class ThreadedEchoRequestHandler(
        socketserver.BaseRequestHandler,
):

    def handle(self):
        # 将数据返回给客户端
        data = self.request.recv(1024)
        cur_thread = threading.currentThread()
        response = b'%s: %s' % (cur_thread.getName().encode(),
                                data)
        self.request.send(response)
        return

class ThreadedEchoServer(socketserver.ThreadingMixIn,
                         socketserver.TCPServer,
                         ):
    pass

if __name__ == '__main__':
    import socket

    address = ('localhost', 0)  # 让内核决定端口
    server = ThreadedEchoServer(address,
                                ThreadedEchoRequestHandler)
    ip, port = server.server_address  # 获取所分配的端口

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # 不要退出了
    t.start()
    print('Server loop running in thread:', t.getName())

    # 连接服务器
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # 发送数据
    message = b'Hello, world'
    print('Sending : {!r}'.format(message))
    len_sent = s.send(message)

    # 接收响应
    response = s.recv(1024)
    print('Received: {!r}'.format(response))

    # 执行清理
    server.shutdown()
    s.close()
    server.socket.close()

线程服务器的响应信息为处理该次请求的线程标识。

$ python3 socketserver_threaded.py

Server loop running in thread: Thread-1
Sending : b'Hello, world'
Received: b'Thread-2: Hello, world'

要使用进程的话,混合式类为 ForkingMixIn

socketserver_forking.py

import os
import socketserver

class ForkingEchoRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        # 返回数据给客户端
        data = self.request.recv(1024)
        cur_pid = os.getpid()
        response = b'%d: %s' % (cur_pid, data)
        self.request.send(response)
        return

class ForkingEchoServer(socketserver.ForkingMixIn,
                        socketserver.TCPServer,
                        ):
    pass

if __name__ == '__main__':
    import socket
    import threading

    address = ('localhost', 0)  # 让内核决定端口
    server = ForkingEchoServer(address,
                               ForkingEchoRequestHandler)
    ip, port = server.server_address  # 获取分配的端口

    t = threading.Thread(target=server.serve_forever)
    t.setDaemon(True)  # 不要退出了
    t.start()
    print('Server loop running in process:', os.getpid())

    # Connect to the server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))

    # 发送数据
    message = 'Hello, world'.encode()
    print('Sending : {!r}'.format(message))
    len_sent = s.send(message)

    # 接收响应
    response = s.recv(1024)
    print('Received: {!r}'.format(response))

    # 执行清理
    server.shutdown()
    s.close()
    server.socket.close()

与线程一样,响应信息包含处理该次请求的进程 ID 号。

$ python3 socketserver_forking.py

Server loop running in process: 22599
Sending : b'Hello, world'
Received: b'22600: Hello, world'

参阅

  • socketserver 标准库文档
  • socket -- 底层数据通信
  • select -- 底层异步 I/O 工具
  • asyncio -- 异步 I/O,事件循环和并发工具。
  • SimpleXMLRPCServer -- 使用 socketserver 创建的 XML-RPC 服务器.
  • Unix Network Programming, Volume 1: The Sockets Networking API, 3/E By W. Richard Stevens, Bill Fenner, and Andrew M. Rudoff. Published by Addison-Wesley Professional, 2004. ISBN-10: 0131411551 (这是一本书,中译名应该是:Unix 网络编程:卷1:套接字网络 API
  • Foundations of Python Network Programminng, 3/E By Brandon Rhodes and John Goerzen. Published by Apress, 2014. ISBN-10: 1430258543 (这也是一本书,Python 网络编程基础