Python-SocketServer模块

1、简介

socket编程过于底层,编程虽然有套路,但是想要写出健壮的代码还是比较困难的,所以很多语言都对
socket底层API进行封装,Python的封装就是socketserver模块。它是网络服务编程框架,便于企业级快速开发。

2、类的继承关系

复制代码
+------------+
| BaseServer |
+------------+
|
v
+-----------+       +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+       +------------------+
|
v
+-----------+       +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+       +--------------------+
复制代码
复制代码
SocketServer简化了网络服务器的编写。
它有4个同步类:
TCPServer
UDPServer
UnixStreamServer
UnixDatagramServer。
2个Mixin类:ForkingMixIn 和 ThreadingMixIn 类,用来支持异步。由此得到
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
fork是创建多进程,thread是创建多线程。
fork需要操作系统支持,Windows不支持。
复制代码

3、编程接口

3.1、方法介绍

复制代码
socketserver.BaseServer(server_address, RequestHandlerClass)

需要提供服务器绑定的地址信息,和用于处理请求的RequestHandlerClass类。
RequestHandlerClass类必须是BaseRequestHandler类的子类,在BaseServer中代码如下:

# BaseServer代码
class BaseServer:
    def __init__(self, server_address, RequestHandlerClass):
        """Constructor. May be extended, do not override.可扩展不可覆盖"""
        self.server_address = server_address
        self.RequestHandlerClass = RequestHandlerClass # 请求处理类
        self.__is_shut_down = threading.Event()
        self.__shutdown_request = False
    def finish_request(self, request, client_address): # 处理请求的方法
        """Finish one request by instantiating RequestHandlerClass."""
        self.RequestHandlerClass(request, client_address, self) # RequestHandlerClass构造
复制代码

3.2、BaseRequestHandler类

3.2.1、介绍

复制代码
它是和用户连接的用户请求处理类的基类,定义为
BaseRequestHandler(request, client_address, server)

服务端Server实例接收用户请求后,最后会实例化这个类。
它被初始化时,送入3个构造参数:request, client_address, server自身
以后就可以在BaseRequestHandler类的实例上使用以下属性:
self.request是和客户端的连接的socket对象
self.server是TCPServer实例本身
self.client_address是客户端地址
这个类在初始化的时候,它会依次调用3个方法。子类可以覆盖这些方法。

# BaseRequestHandler要子类覆盖的方法
class BaseRequestHandler:
    def __init__(self, request, client_address, server):
        self.request = request
        self.client_address = client_address
        self.server = server
        self.setup()
        try:
            self.handle()
        finally:
            self.finish()
    def setup(self): # 每一个连接初始化
        pass
    def handle(self): # 每一次请求处理
        pass
    def finish(self): # 每一个连接清理
        pass
复制代码

3.2.2、测试代码

复制代码
import threading
import socketserver


class MyHandler(socketserver.BaseRequestHandler):
    def handle(self):
        # super().handle() # 可以不调用,父类handle什么都没有做
        print('-' * 30)
        print(self.server)  # 服务
        print(self.request)  # 服务端负责客户端连接请求的socket对象
        print(self.client_address)  # 客户端地址
        print(self.__dict__)
        print(self.server.__dict__)  # 能看到负责accept的socket
        print(threading.enumerate())
        print(threading.current_thread())
        print('-' * 30)


addr = ('0.0.0.0', 9999)
server = socketserver.ThreadingTCPServer(addr, MyHandler)  # 注意参数是MyHandler类
# server.handle_request() # 一次性
server.serve_forever()  # 永久循环执行
复制代码
测试结果说明,handle方法相当于socket的recv方法。
每个不同的连接上的请求过来后,生成这个连接的socket对象即self.request,客户端地址是self.client_address。

3.3、问题

测试过程中,上面代码,连接后立即断开了,为什么?
怎样才能客户端和服务器端长时间连接?
复制代码
import threading
import socketserver
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


class MyHandler(socketserver.BaseRequestHandler):
    def handle(self):
        # super().handle() # 可以不调用,父类handle什么都没有做
        print('-' * 30)
        print(self.server)  # 服务
        print(self.request)  # 服务端负责客户端连接请求的socket对象
        print(self.client_address)  # 客户端地址
        print(self.__dict__)
        print(self.server.__dict__)  # 能看到负责accept的
        print(threading.enumerate())
        print(threading.current_thread())
        print('-' * 30)
        for i in range(3):
            data = self.request.recv(1024)
            logging.info(data)
        logging.info('====end====')


addr = ('0.0.0.0', 9999)
server = socketserver.ThreadingTCPServer(addr, MyHandler)
server.serve_forever()  # 永久
复制代码
将ThreadingTCPServer换成TCPServer,同时连接2个客户端观察效果。
ThreadingTCPServer是异步的,可以同时处理多个连接。
TCPServer是同步的,一个连接处理完了,即一个连接的handle方法执行完了,才能处理另一个连接,且只有主线程。

3.4、总结

创建服务器需要几个步骤:
1. 从BaseRequestHandler类派生出子类,并覆盖其handle()方法来创建请求处理程序类,此方法将处理传入请求
2. 实例化一个服务器类,传参服务器的地址和请求处理类
3. 调用服务器实例的handle_request()或serve_forever()方法
4. 调用server_close()关闭套接字

4、实现EchoServer

4.1、需求

顾名思义,Echo,来什么消息回显什么消息
客户端发来什么信息,返回什么信息

4.2、代码

复制代码
import threading
import socketserver


class Handler(socketserver.BaseRequestHandler):
    def setup(self):
        super().setup()
        self.event = threading.Event()

    def finish(self):
        super().finish()
        self.event.set()

    def handle(self):
        super().handle()
        print('-' * 30)
        while not self.event.is_set():
            data = self.request.recv(1024).decode()
            print(data)
            msg = '{} {}'.format(self.client_address, data).encode()
            self.request.send(msg)


server = socketserver.ThreadingTCPServer(('127.0.0.1', 9999), Handler)
print(server)
threading.Thread(target=server.serve_forever, name='EchoServer',daemon=True).start()
while True:
    cmd = input('>>')
    if cmd == 'quit':
        server.server_close()
        break
    print(threading.enumerate())
复制代码

5、总结

为每一个连接提供RequestHandlerClass类实例,依次调用setup、handle、finish方法,且使用了
try...finally结构保证finish方法一定能被调用。这些方法依次执行完成,如果想维持这个连接和客户端通信,就需要在handle函数中使用循环。
socketserver模块提供的不同的类,但是编程接口是一样的,即使是多进程、多线程的类也是一样,大大减少了编程的难度。
将socket编程简化,只需要程序员关注数据处理本身,实现Handler类就行了。这种风格在Python十分常见。

6、示例

6.1、用SocketServer来改写ChatServer

6.1.1、代码

使用ThreadingTCPServer改写ChatServer
复制代码
import threading
from socketserver import ThreadingTCPServer, StreamRequestHandler
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


class ChatHandler(StreamRequestHandler):
    clients = {}

    def setup(self):
        super().setup()
        self.event = threading.Event()
        self.clients[self.client_address] = self.wfile

    def handle(self):
        super().handle()  # 虽然父类什么都没做,但是调用是个好习惯
        while not self.event.is_set():
            data = self.rfile.read().strip()
            if data == b'quit' or data == b'':
                break
            msg = "From {}:{}. data={}".format(*self.client_address, data)
            for f in self.clients.values():
                f.write(msg.encode())
                f.flush()

    def finish(self):
        self.clients.pop(self.client_address)
        super().finish()
        self.event.set()


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999):
        self.server = ThreadingTCPServer((ip, port), ChatHandler)
        self.server.daemon_threads = True

    def start(self):
        threading.Thread(
            target=self.server.serve_forever, name='chatserver',
            daemon=True).start()

    def stop(self):
        self.server.server_close()


if __name__ == '__main__':
    cs = ChatServer()
    cs.start()
    while True:
        cmd = input('>>').strip()
        if cmd == 'quit':
            cs.stop()
            break
        print(threading.enumerate())
复制代码

6.1.2、问题

上例 self.clients.pop(self.client_address) 能执行到吗?
如果连接的线程中handle方法中抛出异常,例如客户端主动断开导致的异常,线程崩溃,self.clients的pop方法还能执行吗?
当然能执行,基类源码保证了即使异常,也能执行finish方法。但不代表不应该不捕获客户端各种异常。
注意:此程序线程不安全

6.2、使用IO多路复用改写群聊软件

不需要启动多线程来执行socket的accept、recv方法了
复制代码
import threading
import selectors
import socket
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9999):
        self.addr = ip, port
        self.sock = socket.socket()
        self.sock.setblocking(False)  # 非阻塞
        self.event = threading.Event()
        # 构建本系统最优Selector
        self.selector = selectors.DefaultSelector()

    def start(self):
        self.sock.bind(self.addr)
        self.sock.listen()
        key = self.selector.register(self.sock, selectors.EVENT_READ,
                                     self.accept)
        threading.Thread(target=self.select, name='select').start()

    def select(self):
        with self.selector:
            while not self.event.is_set():
                events = self.selector.select(0.5)  # 超时返回[]
                # 监听注册的对象的事件,发生被关注事件则返回events
                for key, mask in events:
                    key.data(key.fileobj, mask)

    def accept(self, server: socket.socket, mask):
        conn, raddr = server.accept()
        conn.setblocking(False)
        logging.info("New client {} accepted. fd={}".format(raddr,
                                                            conn.fileno()))
        key = self.selector.register(conn, selectors.EVENT_READ, self.recv)

    def recv(self, conn: socket.socket, mask):
        data = conn.recv(1024).strip()
        if data == b'' or data == b'quit':
            self.selector.unregister(conn)
            conn.close()  # 关闭前一定要注销
            return
        msg = "Your msg={}".format(data.decode()).encode()
        logging.info(msg)
        for key in self.selector.get_map().values():
            print(key.data.__name__)
            # 特别注意,绑定的方法==和is的区别
            print(key.data is self.accept, key.data == self.accept)
            print(key.data is self.recv, key.data == self.recv)
            if key.data == self.recv:
                key.fileobj.send(msg)

    def stop(self):
        self.event.set()


if __name__ == '__main__':
    cs = ChatServer()
    cs.start()
    while True:
        cmd = input('>>').strip()
        if cmd == 'quit':
            cs.stop()
            break
        print(*cs.selector.get_map().values())
复制代码
本例只完成基本功能,其他功能如有需要,请自行完成。
注意使用IO多路复用,使用了几个线程?
特别注意key.data == self.recv

6.3、自己实现HTTPServer

复制代码
import threading
import selectors
import socket
import logging
import webob

# pip install webob
FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
html_content = """
<html>
<head><title></title></head>
<body>
   欢迎访问首页
</body>
</html>
"""


class WebServer:
    def __init__(self, ip='0.0.0.0', port=80):
        self.addr = ip, port
        self.sock = socket.socket()
        self.sock.setblocking(False)  # 非阻塞
        self.event = threading.Event()
        # 构建本系统最优Selector
        self.selector = selectors.DefaultSelector()

    def start(self):
        self.sock.bind(self.addr)
        self.sock.listen()
        key = self.selector.register(self.sock, selectors.EVENT_READ, self.accept)
        threading.Thread(target=self.select, name='select').start()

    def select(self):
        with self.selector:
            while not self.event.is_set():
                events = self.selector.select(1)  # 超时返回[]
                # 监听注册的对象的事件,发生被关注事件则返回events
                print(events)
                for key, mask in events:
                    key.data(key.fileobj, mask)

    def accept(self, server: socket.socket, mask):
        conn, raddr = server.accept()
        conn.setblocking(False)
        logging.info("New client {} accepted. fd={}".format(raddr, conn.fileno()))
        key = self.selector.register(conn, selectors.EVENT_READ, self.recv)

    def recv(self, conn: socket.socket, mask):
        with conn:  # 用完就断
            try:
                data = conn.recv(1024).strip()
                # 收到request报文,下面要做url映射等,此处都省略
                request = webob.Request.from_bytes(data)
                print(request.url)
                print('=' * 30)
                response = webob.Response(html_content, status=201)
                response.headers.add('Server', 'MageServer')
                firstline = 'HTTP/1.1 {}'.format(response.status)
                print(response.headerlist)
                headers = "\r\n".join(
                    [firstline] + ["{}: {}".format(k, v) for k, v in response.headerlist] + ['', '']
                )  # 响应头:第一行、头部字段、2个回车换行
                body = response.body
                print(type(headers), type(body))
                content = headers.encode() + body
                conn.send(content)
            finally:
                self.selector.unregister(conn)

    def stop(self):
        self.event.set()


if __name__ == '__main__':
    cs = WebServer()
    cs.start()
    while True:
        cmd = input('>>').strip()
        if cmd == 'quit':
            cs.stop()
            break
复制代码

 

posted @   小粉优化大师  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示