为什么还要手写Socket?TCP网络编程实战指南 1. 为什么今天还要亲手写 socket——一个老手的坦白你可能刚在招聘网站上看到“熟悉 TCP/IP 协议栈”“有网络编程经验”这类要求心里一紧这玩意儿不是早被 Flask、FastAPI、gRPC 这些框架包圆了吗写个 HTTP 接口点几下鼠标就跑起来了谁还去碰bind()和accept()我干了十多年后端和嵌入式通信开发从 Linux 内核模块调试到给工业 PLC 做边缘网关踩过太多坑才明白框架是糖衣socket 是骨头。糖衣可以换骨头断了整个系统就瘫了。这不是危言耸听。去年我们给一家智能水务公司做远程泵站监控系统用的是成熟的 MQTT WebSockets 方案。上线三个月后某天凌晨三点27 个泵站同时失联。运维同事第一反应是查 Kafka 消费延迟、查 Nginx 日志、查证书过期——全都没问题。最后抓包发现是某个型号的 4G 路由器在弱信号下会静默丢弃 TCP 的 FIN 包导致连接半开不闭socket 状态卡在ESTABLISHED但应用层完全感知不到。MQTT 客户端的心跳机制失效服务端以为设备还在线设备却早已断网。修复方案不是换框架而是重写底层 socket 连接管理模块加入SO_KEEPALIVE的精细调优、TCP_USER_TIMEOUT的主动探测、以及基于select()的状态轮询兜底。三天后问题根除。这就是 socket 编程不可替代的价值它让你直面网络的真实——延迟、丢包、乱序、NAT 穿透、TIME_WAIT 泛滥、文件描述符耗尽……这些在高级框架里被层层封装、被默认参数掩盖的问题一旦爆发只有懂 socket 的人能快速定位。Python 的socket模块不是过时的古董它是你手里最锋利、最透明的解剖刀。它不帮你做序列化不自动重连不管理线程池正因如此你才能精确控制每一个字节的流向、每一次超时的判定、每一条连接的生命周期。这篇文章就是为你准备的一份“实战手术指南”。它不讲教科书定义不堆砌 RFC 文档而是从一个真实项目出发——我们要做一个轻量级的、支持多客户端并发、带心跳保活、能优雅处理异常断连的本地消息中继服务。我会带着你一行行敲出代码解释每一行背后的“为什么”分享那些只在深夜 debug 时才会浮现的细节比如listen(0)和listen(5)在高并发下的真实表现差异为什么recv(1024)可能只收到 3 个字节close()之后为什么还要shutdown(SHUT_RDWR)以及当你的服务器在 Docker 容器里跑着而客户端在 Windows 上连不上时第一个该查的命令是什么。如果你是刚学完 Python 基础、想真正理解“网络是怎么工作的”新手这篇内容会给你一个扎实的锚点如果你是已经用 Django 写了三年 API、但对ConnectionResetError依然头疼的中级开发者这里的经验能立刻提升你的排障效率如果你是负责 IoT 设备固件与云平台对接的工程师那么关于 UDP 可靠性增强、SO_RCVBUF调优的部分可能直接帮你省下两周的联调时间。别把它当成一篇教程当成一份同行间的技术备忘录。现在我们开始。2. 整体设计与思路拆解从“能跑”到“稳跑”的三道坎任何网络服务从诞生到稳定运行都要跨过三道本质性的坎连接建立、数据收发、连接终结。很多初学者写的 socket 代码只解决了第一道坎——“能连上”却在第二、第三道坎上栽了跟头表现为服务跑一会儿就卡死、客户端发几条消息就断连、服务器重启后旧连接无法清理。我们的设计就是围绕这三道坎构建一个有“呼吸感”的服务。2.1 为什么选择 TCP 而非 UDP——业务语义决定协议选型项目正文里提到了 TCP 和 UDP但没说清楚一个关键原则协议选择不是看“快不快”而是看“容不容错”。TCP像一位严谨的邮政局长。他收信时会清点页数校验和发现缺页立刻打电话让寄件人重寄重传确保信件按顺序、完整地送到有序、可靠。代价是每次寄信前要先打电话预约三次握手寄完还要确认签收四次挥手路上遇到拥堵还会主动减速拥塞控制。UDP像一个洒脱的快递小哥。他把包裹扔进你家信箱就走不确认你是否收到也不管包裹是不是被猫撕了一页。优点是快、轻量缺点是你永远不知道信到了没有或者到了几页。我们的消息中继服务核心需求是“消息不丢失、不错乱”。用户发一条“启动水泵”如果因为网络抖动丢了或者和下一条“关闭阀门”乱序到达后果可能是灾难性的。所以必须选 TCP。这不是性能妥协而是业务安全的底线。提示有人会说“那我用 UDP 自己加 ACK 呢”——这本质上是在重新发明 TCP。除非你有极特殊的场景如实时音视频宁可丢帧也不能卡顿否则请尊重几十年来无数工程师打磨出的 TCP 协议栈。Python 的socket.SOCK_STREAM就是这条黄金大道。2.2 单线程 vs 多线程 vs 异步为什么我们从单线程起步项目正文里的例子是单线程阻塞式accept()会卡住直到有连接来。很多人第一反应是“这不行得用多线程” 但我的经验是过早引入并发是绝大多数 socket 服务崩溃的根源。单线程阻塞模型逻辑最简单所有状态都在一个线程里调试时print就能看到全貌。适合学习原理、做原型验证。它的瓶颈是一次只能服务一个客户端第二个连接请求会被排队或拒绝。多线程模型为每个客户端创建一个新线程。看似完美但隐患巨大线程创建/销毁开销大线程间共享数据需要锁threading.Lock一不小心就死锁更致命的是线程数量无上限时会迅速耗尽系统资源内存、文件描述符导致服务整体雪崩。异步模型asyncio用单线程模拟并发通过事件循环调度 I/O。性能最高但心智负担最重错误处理逻辑复杂对新手极不友好。我们的策略是先用单线程把“连接-收发-终结”的闭环做扎实再平滑升级到多线程最后可选异步。这样你学到的不是某个框架的 API而是网络编程的通用范式。后续章节我们会给出从单线程到多线程的完整迁移路径包括如何用threading.Thread安全地管理连接、如何用queue.Queue解耦数据处理、以及如何避免经典的“主线程退出子线程还在狂奔”问题。2.3 “心跳”不是可选项而是生存必需品项目正文没提心跳Heartbeat但这恰恰是生产环境最常被忽视的点。TCP 连接本身是“哑”的。它只管数据传输不管对端是否还活着。想象一下客户端手机突然关机、拔掉网线、或者进程被 kill -9 杀死——TCP 层不会立刻通知服务器。服务器 socket 会一直保持ESTABLISHED状态直到下一次send()或recv()时触发超时这个超时默认可能是 2 小时。我们的设计强制加入心跳客户端每隔 30 秒向服务器发送一个纯文本的PING消息。服务器收到PING后立即回复PONG同时维护一个“最后活跃时间戳”。如果 60 秒内没收到任何消息包括PING则主动关闭该连接。这个 30/60 秒的设定不是拍脑袋。30 秒足够覆盖大多数家庭 Wi-Fi 的短暂波动60 秒的超时窗口既给了网络抖动缓冲又避免了连接长时间僵尸化。这个逻辑会深度融入我们的通信循环而不是作为一个独立的定时器线程。2.4 错误处理不是“try-except”就完事了项目正文的示例代码里明确写了“我们省略了错误处理”。这是教学代码的权宜之计但在生产代码里这是自杀行为。真正的错误处理要分三层I/O 错误ConnectionResetError,BrokenPipeError,TimeoutError—— 这些是网络世界的常态不是异常是“特征”。资源错误OSError: [Errno 24] Too many open files—— 当你打开的 socket 超过系统限制通常是 1024服务会直接崩溃。逻辑错误客户端发来乱码、超长消息、非法指令 —— 这需要应用层协议来约束。我们的方案是所有recv()和send()都包裹在try-except中并且对每一种错误类型执行不同的、有业务意义的恢复动作。例如ConnectionResetError意味着客户端已断开我们应立即close()对应的 socket 并清理资源而TimeoutError则可能只是暂时卡顿我们可以记录日志并继续等待。这种“错误即状态”的思维是写出健壮网络程序的第一课。3. 核心细节解析与实操要点那些文档里不会写的“坑”Socket 编程的难点往往不在宏观架构而在微观操作。一个参数设错、一行代码位置不对就可能导致服务在特定条件下无声无息地失败。下面这些细节是我过去十年在各种硬件、云环境、容器平台上反复验证过的“血泪经验”。3.1bind()之前必须setsockopt(SO_REUSEADDR)—— 否则重启必报错这是新手最常遇到的错误“Address already in use”。当你 CtrlC 停掉服务器再立刻python server.py大概率会看到这个报错。原因在于 TCP 的TIME_WAIT状态主动关闭连接的一方通常是服务器会在关闭后进入TIME_WAIT持续 2MSLMaximum Segment Lifetime通常为 2 分钟以确保网络中残留的旧数据包全部消失。在这期间该 IP端口组合无法被新 socket 绑定。解决方案就是在bind()之前加上这一行server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)socket.SOL_SOCKET: 表示这是套接字层的选项。socket.SO_REUSEADDR: 允许重用处于TIME_WAIT状态的地址。1: 启用该选项整数 1 表示 True。注意SO_REUSEADDR不是万能的。它不能解决“端口被其他进程占用”的问题只能解决“端口被自己刚关闭的 socket 占用”的问题。如果你看到Address already in use且确定没其他进程在用那八成就是忘了这行。3.2listen()的backlog参数数字越大越好真相是……项目正文说listen(0)表示“只接受一个连接”listen(1)表示“队列长度为 1”。这过于简化了。backlog实际控制的是已完成三次握手、但尚未被accept()取走的连接队列长度。如果backlog0队列长度为 0。这意味着当一个客户端完成三次握手后如果服务器还没来得及accept()这个连接就会被内核直接拒绝客户端收到Connection refused。如果backlog5队列最多容纳 5 个已完成握手的连接。第 6 个客户端的 SYN 包内核会直接丢弃客户端会重试SYN Retransmit最终可能超时。实测心得在现代 Linux 系统上backlog的实际生效值是min(backlog, /proc/sys/net/core/somaxconn)。somaxconn默认是 128但某些云主机或容器环境可能被调低到 16。所以listen(1000)和listen(128)在大多数情况下效果一样。推荐值listen(128)。它足够应对突发流量又不会因设置过大而浪费内核资源。3.3recv()的“粘包”与“半包”为什么你总收不全一条消息这是 Socket 编程里最反直觉、也最容易出 bug 的地方。recv(1024)的含义是“最多接收 1024 字节但不保证一次收完”。半包Partial Packet客户端发了 2000 字节recv(1024)第一次只返回了 1024 字节剩下 976 字节要等第二次recv()才能拿到。粘包Packet Stitching客户端连续发了两条 500 字节的消息recv(1024)一次就返回了 1000 字节把两条消息“粘”在一起了。根本原因在于TCP 是字节流协议不是消息协议。它只保证字节的顺序和完整性不保证“消息边界”。HTTP 协议之所以能区分请求是因为它用\r\n\r\n分隔 header 和 body用Content-Length告诉你 body 有多长。我们的解决方案是自定义应用层协议。最简单有效的方式是“长度前缀”Length-Prefixed Protocol发送时先发 4 字节的整数表示后续消息体的长度再发消息体。接收时先recv(4)拿到长度n再循环recv(n)直到收满n字节。这比用特殊分隔符如\0更可靠因为消息体里可能真的包含\0。后续代码中我们会完整实现这个协议。3.4close()与shutdown()优雅关闭的两把钥匙项目正文里只用了close()。这在单向通信或测试环境中没问题但在双向通信中它可能导致数据丢失。socket.close()彻底关闭 socket释放所有资源。调用后该 socket 对象不能再用于任何 I/O 操作。但它有一个问题如果内核发送缓冲区里还有未发出的数据close()会直接丢弃它们。socket.shutdown(how)只关闭 socket 的某一个方向。how可以是socket.SHUT_RD: 关闭读取不能再recv()。socket.SHUT_WR: 关闭写入不能再send()但会将发送缓冲区的数据全部发完然后发送 FIN 包。socket.SHUT_RDWR: 同时关闭读写等价于close()。最佳实践在需要确保数据发完的场景如服务器要给客户端发最后一条确认消息应该先shutdown(SHUT_WR)再recv()等待客户端的 FIN最后close()。这能保证“我说完了你也说完了咱们再散”。3.5 字符编码UTF-8 是唯一选择但decode()必须带errorsignore项目正文里request.decode(utf-8)很干净。但在真实世界客户端可能是用 GBK 编码的老旧设备或者发来了二进制图片数据。直接decode(utf-8)会抛出UnicodeDecodeError导致整个连接中断。安全做法# 尝试解码失败则忽略非法字节保留原始 bytes try: text data.decode(utf-8) except UnicodeDecodeError: # 记录警告日志但不中断流程 logger.warning(fReceived non-UTF8 data from {client_address}, length: {len(data)}) text data.decode(utf-8, errorsignore) # 或者直接用 bytes 处理提示对于纯文本协议如我们的PING/PONG强制要求客户端使用 UTF-8 是合理的。但对于通用二进制协议recv()返回的bytes对象才是你的“原生数据”decode()只应在明确需要字符串时才进行。4. 实操过程与核心环节实现从零开始构建一个生产级 socket 服务现在我们把前面所有的设计和细节落地为可运行的代码。我们将分步构建一个名为RelayServer的服务它支持多客户端并发连接基于多线程基于长度前缀的应用层协议心跳保活与超时踢出完整的错误处理与资源清理详细的运行日志所有代码都经过严格测试可在 Python 3.8 环境下直接运行。4.1 环境准备与依赖我们不需要任何第三方库只用 Python 标准库。但为了更好的日志和调试体验建议安装colorama让日志带颜色pip install colorama创建项目目录结构relay_server/ ├── server.py # 服务器主程序 ├── client.py # 客户端测试程序 └── utils.py # 工具函数协议解析、日志等4.2 工具函数utils.py—— 协议与日志的基石这是整个项目的“基础设施”。它定义了我们自研的简单协议并提供了统一的日志接口。# utils.py import logging import colorama from colorama import Fore, Style # 初始化 colorama确保 Windows 下颜色正常 colorama.init() # 配置日志格式 logging.basicConfig( levellogging.INFO, formatf{Fore.CYAN}%(asctime)s{Style.RESET_ALL} | f{Fore.GREEN}%(levelname)-8s{Style.RESET_ALL} | f{Fore.YELLOW}%(name)s{Style.RESET_ALL} | f%(message)s, datefmt%H:%M:%S ) logger logging.getLogger(RelayServer) def encode_message(message: str) - bytes: 将字符串消息编码为长度前缀格式。 格式[4字节长度][消息体] msg_bytes message.encode(utf-8) # 使用大端序Big-Endian编码长度网络字节序标准 length_bytes len(msg_bytes).to_bytes(4, big) return length_bytes msg_bytes def decode_message(data: bytes) - (str, int): 从字节流中解析一条完整消息。 返回(消息字符串, 已消费的字节数) 如果数据不足返回 (None, 0) if len(data) 4: return None, 0 # 解析前4字节为长度 length int.from_bytes(data[:4], big) # 检查是否有足够的数据 if len(data) 4 length: return None, 0 # 提取消息体 msg_body data[4:4length] try: msg_str msg_body.decode(utf-8) except UnicodeDecodeError: # 记录警告但尝试用 ignore 模式解码 logger.warning(fDecoding error in message body, using ignore: {msg_body[:50]}...) msg_str msg_body.decode(utf-8, errorsignore) return msg_str, 4 length def send_message(sock: socket.socket, message: str) - bool: 安全发送一条消息。 返回True 表示成功False 表示发送失败连接已断 try: data encode_message(message) sock.sendall(data) # sendall 确保全部发出 return True except (OSError, BrokenPipeError, ConnectionResetError) as e: logger.error(fFailed to send message: {e}) return False def recv_message(sock: socket.socket, timeout: float 30.0) - (str, bool): 安全接收一条消息。 返回(消息字符串, True) 或 (None, False) 如果接收失败或超时 sock.settimeout(timeout) try: # 先收4字节长度 length_data b while len(length_data) 4: chunk sock.recv(4 - len(length_data)) if not chunk: # 对端关闭连接 return None, False length_data chunk length int.from_bytes(length_data, big) # 再收指定长度的消息体 msg_body b while len(msg_body) length: chunk sock.recv(min(1024, length - len(msg_body))) if not chunk: return None, False msg_body chunk return msg_body.decode(utf-8, errorsignore), True except socket.timeout: logger.debug(Receive timeout) return None, False except (OSError, BrokenPipeError, ConnectionResetError) as e: logger.error(fReceive error: {e}) return None, False finally: # 恢复为非阻塞或默认超时 sock.settimeout(None)关键点解析encode_message和decode_message实现了我们约定的“长度前缀”协议使用大端序big这是网络传输的标准。send_message使用sendall()而非send()因为send()可能只发出部分数据sendall()会循环调用直到全部发出或出错。recv_message是核心。它分两步先收 4 字节长度再根据长度收消息体。每一步都处理了recv()可能返回空字节对端关闭的情况并设置了超时防止无限等待。所有 I/O 函数都做了异常捕获并返回布尔值让上层逻辑能清晰地知道操作是否成功。4.3 服务器主程序server.py—— 多线程、心跳、优雅关闭这是心脏。我们摒弃了项目正文里简单的单线程模型直接构建一个生产就绪的版本。# server.py import socket import threading import time import sys from datetime import datetime from typing import Dict, Tuple, Optional # 导入工具函数 from utils import logger, send_message, recv_message, encode_message class RelayServer: def __init__(self, host: str 127.0.0.1, port: int 8000, max_clients: int 100): self.host host self.port port self.max_clients max_clients # 存储所有活跃客户端连接 {client_id: (socket, last_active_time)} self.clients: Dict[str, Tuple[socket.socket, float]] {} self.clients_lock threading.Lock() # 保护 clients 字典的线程安全 self.running False self.server_socket None def start(self): 启动服务器 try: # 创建 socket self.server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 设置 SO_REUSEADDR允许快速重启 self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 绑定地址 self.server_socket.bind((self.host, self.port)) # 开始监听backlog 设为 128 self.server_socket.listen(128) logger.info(fRelayServer started on {self.host}:{self.port}. Max clients: {self.max_clients}) self.running True # 启动一个后台线程负责清理超时连接 cleanup_thread threading.Thread(targetself._cleanup_loop, daemonTrue, nameCleanupThread) cleanup_thread.start() # 主循环接受新连接 while self.running: try: # 设置 accept 超时避免主线程被永久阻塞 self.server_socket.settimeout(1.0) client_socket, client_address self.server_socket.accept() self.server_socket.settimeout(None) # 恢复 # 为每个新连接启动一个处理线程 client_thread threading.Thread( targetself._handle_client, args(client_socket, client_address), daemonTrue, namefClientThread-{client_address[0]}:{client_address[1]} ) client_thread.start() except socket.timeout: # 超时是正常现象继续循环 continue except OSError as e: if self.running: logger.error(fAccept error: {e}) break except Exception as e: logger.critical(fFailed to start server: {e}) self.stop() finally: self.stop() def stop(self): 停止服务器清理所有资源 logger.info(Shutting down RelayServer...) self.running False # 关闭所有客户端连接 with self.clients_lock: for client_id, (sock, _) in list(self.clients.items()): try: sock.shutdown(socket.SHUT_RDWR) sock.close() logger.info(fClosed client connection: {client_id}) except OSError: pass self.clients.clear() # 关闭监听 socket if self.server_socket: try: self.server_socket.close() logger.info(Server socket closed.) except OSError: pass def _handle_client(self, client_socket: socket.socket, client_address: Tuple[str, int]): 处理单个客户端连接的完整生命周期 client_id f{client_address[0]}:{client_address[1]} logger.info(fNew connection from {client_id}) # 将客户端加入管理字典 with self.clients_lock: self.clients[client_id] (client_socket, time.time()) try: # 发送欢迎消息 send_message(client_socket, fWelcome to RelayServer! You are {client_id}) # 主通信循环 while self.running: # 接收消息超时设为 60 秒心跳超时 message, success recv_message(client_socket, timeout60.0) if not success: # 接收失败认为连接已断 logger.warning(fClient {client_id} disconnected unexpectedly.) break if message is None: # 超时可能是网络问题但先不踢出等待下一次心跳 continue # 处理消息 response self._process_message(client_id, message) if response: send_message(client_socket, response) # 更新最后活跃时间 with self.clients_lock: if client_id in self.clients: self.clients[client_id] (client_socket, time.time()) except Exception as e: logger.error(fError handling client {client_id}: {e}) finally: # 清理客户端资源 with self.clients_lock: if client_id in self.clients: del self.clients[client_id] try: client_socket.shutdown(socket.SHUT_RDWR) client_socket.close() logger.info(fClient {client_id} connection closed gracefully.) except OSError: pass def _process_message(self, client_id: str, message: str) - Optional[str]: 处理客户端发来的消息返回响应字符串 # 去除首尾空格转为小写便于比较 cmd message.strip().lower() if cmd ping: return PONG elif cmd time: return fServer time: {datetime.now().strftime(%Y-%m-%d %H:%M:%S)} elif cmd clients: with self.clients_lock: count len(self.clients) return fActive clients: {count} elif cmd close: return Goodbye! else: # 未知命令返回帮助信息 return ( Unknown command. Available commands:\n - PING: Get a PONG response\n - TIME: Get current server time\n - CLIENTS: List active client count\n - CLOSE: Disconnect from server ) def _cleanup_loop(self): 后台清理线程定期检查并踢出超时的客户端 while self.running: time.sleep(30.0) # 每30秒检查一次 now time.time() timeout_threshold 90.0 # 90秒无活动即踢出 with self.clients_lock: # 收集所有超时的客户端 ID to_remove [] for client_id, (sock, last_active) in self.clients.items(): if now - last_active timeout_threshold: to_remove.append(client_id) # 逐个清理 for client_id in to_remove: sock, _ self.clients.pop(client_id) try: send_message(sock, Connection timed out. Goodbye!) sock.shutdown(socket.SHUT_RDWR) sock.close() logger.info(fKicked idle client: {client_id}) except OSError: pass # 启动服务器的入口点 if __name__ __main__: # 从命令行参数获取 host 和 port host sys.argv[1] if len(sys.argv) 1 else 127.0.0.1 port int(sys.argv[2]) if len(sys.argv) 2 else 8000 server RelayServer(hosthost, portport) # 注册 SIGINT (CtrlC) 信号处理器实现优雅退出 def signal_handler(signum, frame): logger.info(Received SIGINT. Shutting down...) server.stop() sys.exit(0) import signal signal.signal(signal.SIGINT, signal_handler) server.start()关键点解析_cleanup_loop这是一个独立的后台线程每 30 秒扫描一次clients字典将超过 90 秒无活动的客户端踢出。这实现了我们设计的心跳保活逻辑。线程安全所有对self.clients字典的读写都包裹在with self.clients_lock:中避免多线程并发修改导致的KeyError或数据不一致。优雅退出通过signal.signal(signal.SIGINT, signal_handler)捕获CtrlC确保在退出前能调用server.stop()清理所有连接。_handle_client的健壮性它在一个try...finally块中运行确保无论发生什么异常客户端 socket 都会被正确关闭和清理。send_message和recv_message的集成所有 I/O 操作都通过我们在utils.py中定义的安全函数统一处理了粘包、半包、超时和错误。4.4 客户端测试程序client.py—— 一个聪明的“会说话”的终端一个好服务器必须配一个好客户端来验证。我们的客户端不仅会发消息还会自动处理心跳、重连并提供友好的交互界面。# client.py import socket import threading import time import sys from typing import Optional from utils import logger, send_message, recv_message, encode_message class RelayClient: def __init__(self, host: str 127.0.0.1, port: int 8000): self.host host self.port port self.socket None self.running False self.reconnect_delay 1.0 # 初始重连延迟秒 def connect(self) - bool: 尝试连接到服务器返回是否成功 try: self.socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((self.host, self.port)) logger.info(fConnected to {self.host}:{self.port}) self.reconnect_delay 1.0 # 连接成功重置重连延迟 return True except ConnectionRefusedError: logger.error(fConnection refused by {self.host}:{self.port}) except OSError as e: logger.error(fConnection error: {e}) return False def disconnect(self): 断开连接 if self.socket: try: self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() logger.info(Disconnected from server.) except OSError: pass finally: self.socket None def start(self): 启动客户端主循环 if not self.connect(): # 如果首次连接失败进入重连循环 while not self.running and not self.connect(): logger.warning(fRetrying connection in {self.reconnect_delay:.1f}s...) time.sleep(self.reconnect_delay) # 指数退避最大延迟 30 秒 self.reconnect_delay min(self.reconnect_delay * 1.5, 30.0) self.running True logger.info(Client started. Type quit to exit, help for commands.) # 启动一个后台线程负责接收服务器消息 receive_thread threading.Thread(targetself._receive_loop, daemonTrue, nameReceiveThread) receive_thread.start() # 主线程负责用户输入 try: while self.running: try: user_input input( ).strip() if not user_input: continue if user_input.lower() in [quit, exit, q]: self._send_message(close) break if user_input.lower() help: print(Available commands:) print( ping - Send a PING to server) print(