2. TCP多客户端连接管理

任务目标

实现一个基于 selectors 模块的 TCP 多客户端回显服务器,能够同时处理多个客户端连接,并将每个客户端发送的消息原样返回。客户端可随时连接、发送消息并断开,服务器无需为每个连接创建独立线程。

环境依赖

仅使用 Python 3.7+ 标准库,无需安装第三方包。核心模块为 socket、selectors。

完整实现

import selectors
import socket
import sys

sel = selectors.DefaultSelector()

def accept_connection(sock):
    conn, addr = sock.accept()
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, read_message)
    print(f"[连接] {addr} 已接入")

def read_message(conn):
    try:
        data = conn.recv(1024)
    except ConnectionResetError:
        data = b""
    if data:
        conn.sendall(data)  # 回显
    else:
        addr = conn.getpeername()
        sel.unregister(conn)
        conn.close()
        print(f"[断开] {addr} 已离开")

def main(host="127.0.0.1", port=8888):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((host, port))
    server.listen(100)
    server.setblocking(False)
    sel.register(server, selectors.EVENT_READ, accept_connection)
    print(f"[启动] 监听 {host}:{port}")

    try:
        while True:
            events = sel.select(timeout=None)
            for key, _ in events:
                callback = key.data
                callback(key.fileobj)
    except KeyboardInterrupt:
        print("\n[退出] 服务器关闭")
    finally:
        sel.close()
        server.close()

if __name__ == "__main__":
    if len(sys.argv) == 3:
        main(sys.argv[1], int(sys.argv[2]))
    else:
        main()

运行与验证

启动服务器:

python server.py

默认监听 127.0.0.1:8888,也可通过命令行参数指定:python server.py 0.0.0.0 9000。

验证步骤:

  1. 打开两个终端,分别运行 nc 127.0.0.1 8888 或 telnet 127.0.0.1 8888,建立两个独立客户端连接。
  2. 在客户端 A 输入 hello,应立即收到 hello 回显;在客户端 B 输入 world,同样收到 world。两个连接互不阻塞。
  3. 关闭其中一个客户端(Ctrl+C 或输入 Ctrl+D),服务器终端应打印 [断开] 信息,另一个客户端仍可正常收发。
  4. 服务器终端出现 [连接] 与 [断开] 日志,且无异常堆栈,即判定通过。

关键说明

  • 服务器使用 selectors 实现 I/O 多路复用,单线程即可管理所有连接,避免了线程切换开销与共享内存竞争问题。
  • 所有 socket 均设为非阻塞模式,配合事件循环驱动;recv() 返回空字节串表示客户端已关闭连接,此时注销并释放资源。
  • 回显逻辑在 read_message 中完成,sendall 保证数据完整发送。若需扩展为广播或聊天室,只需在此函数中修改转发目标即可。