Python 如何正确实现一个 TCP 服务端?

TCP服务端应使用with管理socket生命周期,捕获KeyboardInterrupt、OSError等异常,选择多线程或asyncio实现并发,循环处理recv/send以应对粘包与半包问题。

socket 模块实现一个健壮、可维护的 TCP 服务端,核心是处理连接管理、异常、并发和资源释放,而不是只写几行 bind + listen + accept

使用上下文管理器确保 socket 正确关闭

手动调用 close() 容易遗漏,尤其在异常路径中。推荐用 with 语句自动管理 socket 生命周期:

  • 服务端 socket 用 socket.socket(...) 创建后立即包进 with
  • 每个客户端连接也应在独立的 with 中处理(或显式 try/finally
  • 避免“忘记 close() 导致端口占用、文件描述符泄漏”这类低级但高频问题

必须捕获常见异常并合理退出

TCP 服务端运行中会遇到多种中断信号和网络异常,不能让未捕获异常直接崩溃:

  • KeyboardInterrupt(Ctrl+C):应优雅关闭监听 socket 并退出
  • OSErrorConnectionAbortedError:客户端异常断开时 recv 返回空或抛异常,需跳过或清理连接
  • socket.timeout:若设置了 settimeout(),需捕获并继续循环,而非终止服务

支持多客户端需明确选择并发模型

单线程 accept + 阻塞 recv 只能服务一个客户。生产环境常用两种方式:

  • 多线程:每 accept 到一个连接,就 threading.Thread(target=handle_client, args=(conn, addr)).start();适合 I/O 密集、连接数不超百的场景
  • select / epoll / asyncio:用事件驱动避免线程开销;Python 3.7+ 推荐 asyncio.start_server(),代码简洁且性能好
  • 不建议用简单 for 循环轮询多个连接(效率低、不可扩展)

收发数据要处理粘包与半包

TCP 是流式协议,recv(n) 不保证一次收齐 n 字节,send() 也可能只发出部分数据:

  • 接收时用循环读取直到满足预期长度,或按协议解析(如首 4 字节为包长)
  • 发送时检查 send() 返回值,若小于待

    发字节数,需缓存剩余部分并重试
  • 不要假设 recv(1024) 一定收到 1024 字节 —— 这是新手最常踩的坑