ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于状态机的TCP客户端断线重连机制

基于状态机的TCP客户端断线重连机制

去年我负责维护一个跑在边缘侧盒子上的数据采集客户端,它每几秒钟就要上报一次业务数据,表面上看是一个再普通不过的 TCP 客户端。某个凌晨服务端升级重启,告警电话直接打到我这:几千个客户端全在重连,刚拉起的新服务端又被压垮了。查日志的时候发现,所有客户端的重连逻辑基本都是同一个模板——while True: sleep(3); try_connect()。

那次事故之后,我把客户端的连接管理整个重写了一遍,核心思路正是标题里说的:基于状态机的 TCP 客户端断线重连机制。重写完成之后,类似的事故再也没出现过。今天这篇文章,我想把从设计到实现、从参数调优到线上踩坑的完整过程都梳理出来,目标是让你看完之后,能直接照着一套可落地的方案去改进自己项目里的 TCP 重连逻辑。

1. 为什么"断线就重连"这种直觉写法,在生产环境里总是翻车

先说一个很多人没意识到的前提:TCP 连接断没断,客户端并不总是能第一时间知道。你以为的"断线",和网络现实里的"断线",经常不是一回事。不理解这一点,所有重连方案都建立在错误的地基上。

1.1 半开连接:你以为还连着,其实对面早就没了

如果服务端进程正常关闭,操作系统会发出 FIN 包,客户端read()最终会拿到EOF,这种"优雅断开"很容易识别。真正麻烦的是半开连接:服务端所在的主机断电、网线被拔、内核崩溃,或者中间路由器直接丢弃了链路状态,对端根本没有机会发送 FIN 或 RST。此时在客户端操作系统的 TCP 状态机里,这条连接仍然处于 ESTABLISHED,你甚至能查询到它"活着"。

这个状态可以维持很久。你往这条"假连接"上写业务数据,数据只会先进入本地发送缓冲区,TCP 协议栈按重传计时器不断重发,却永远等不到 ACK,直到超时阈值才在应用层抛错;而如果你的代码正阻塞在recv()上等数据,那更是可能永远等下去。

这就像一个管理上的定时炸弹:连接对象没释放、线程不退出、资源一直挂着,整个系统的故障感知能力完全依赖业务层碰巧发一条数据并碰巧撞上错误。所以设计断线重连机制的第一件事,不是"怎么连",而是"怎么判断连接已经死了"。

1.2 固定间隔重连如何一步步演变成重试风暴

很多初版实现会选择"固定间隔 + 死循环",比如每隔 3 秒重连一次。在单连接、低并发的玩具 demo 里,它确实能跑;但一旦放大到生产环境,至少有三个问题会接连暴露:

第一,重试风暴。几千个客户端使用完全相同的重连间隔,当服务端恢复的一瞬间,所有连接请求几乎同时打进 accept 队列。TCP 半连接队列一旦溢出,新连接直接丢弃,服务端又开始闪断,客户端再次同时退避……形成一个越来越剧烈的共振循环。这不是理论推演,我那次事故里就是这么被打垮的。

第二,网络状态与重试策略完全脱节。刚断线的几秒钟内服务端极大概率还没恢复,3 秒一次的频率其实是在制造无意义的失败记录;而真正服务端长时间不可用之后,3 秒一次又太频繁,除了日志刷屏和 CPU 空转,没有任何收益。固定间隔无法随着"已经失败了多少次"而自动调整压力。

第三,程序内部没有"当前处于什么阶段"的感知。业务代码无从得知现在到底是在正常通信、退避等待、还是已经重连了 N 次,很多出错之后的业务补偿逻辑根本无处安放。

1.3 状态机的解法视角:把连接当作有生命周期的对象

我后来重构时的核心转变,是把"连接"从一串connect调用,变成一个拥有明确生命周期的对象:从待机开始,经过建立中、已连接、退避等待,再回到建立中,如此循环。对象在任何时刻都处于某个确定的状态,也只有确定的事件能推动状态切换。

这带来的直接好处是:所有路径都被显式穷举在了一张转换表里,你可以对着表评审、写单测、查日志。不存在"从代码某个犄角旮旯偷偷发起重连"的隐藏路径,也不允许"同一个状态里既在等定时器又在收数据"这种混乱局面。状态机本质上是给复杂的异步网络逻辑装上了轨道,避免它在出错时横冲直撞。

2. 状态怎么划分、事件怎么定义:一张转换表说清楚

很多工程师一听到状态机就觉得是理论玩具,但 TCP 重连恰恰是状态机最典型的落地场景。这里的核心工作只有两件事:把状态枚举完整,把事件定义清楚。只要这两个集合是完备的,转换表就是顺理成章的结果。

2.1 核心状态:IDLE / CONNECTING / CONNECTED / BACKOFF

我最终保留的四个核心状态是:

  • IDLE:初始待机,或者被用户显式停止。此时没有任何连接资源。
  • CONNECTING:已经发起connect,正在等待连接结果,可能是成功、拒绝或者超时。
  • CONNECTED:连接建立成功,可以正常收发业务数据。
  • BACKOFF:连接不可用,正在等待下一次重连的退避时间。

很多场景还会再加一个 OFFLINE 状态,用于"重连次数达到上限,彻底放弃,等待人工介入"。如果你的业务有这种诉求,可以在设计阶段预留;我初次实现时没有加,后面补充也不复杂,但最好一开始就在转换表里留好位置。

2.2 触发事件与转换表

事件集合也需要穷举。我这里定义六个事件:用户启动(START)、连接成功(CONNECT_OK)、连接失败(CONNECT_FAIL)、连接丢失(CONNECTION_LOST)、退避定时器到期(RETRY_TIMER)、用户停止(STOP)。

整个状态机的核心,就是下面这张转换表:

当前状态事件动作新状态
IDLESTART发起 connect,重试计数归零CONNECTING
CONNECTINGCONNECT_OK启动收发/心跳,通知业务连接成功CONNECTED
CONNECTINGCONNECT_FAIL失败次数加一,计算退避时间并启动定时器BACKOFF
CONNECTEDCONNECTION_LOST关闭 socket,通知业务连接断开BACKOFF
BACKOFFRETRY_TIMER发起 connectCONNECTING
BACKOFFSTOP取消定时器,复位所有资源IDLE
CONNECTINGSTOP标记本次连接作废,关闭 socketIDLE
CONNECTEDSTOP关闭 socket,通知业务连接断开IDLE

这八条转换看起来不难,但真正容易出问题的,是在实现时漏掉几条边缘转换。比如用户可能在 CONNECTING 过程中按下停止,此时连接线程还挂在半路上,如果不对 STOP 做专门处理,这个线程之后投递的 CONNECT_OK 事件就会落在 IDLE 状态上,形成一个"幽灵连接"。转换表上必须有这一行,代码里才有人去处理这个场景。

2.3 几条最容易遗漏的转换规则

第一,CONNECT_FAIL 之后要不要立即重试?我的选择是不立即重试,而是切到 BACKOFF 等待一个基础间隔。因为连续失败通常意味着网络侧的整体性问题,立即重试只会加剧 1.2 里说的重试风暴。退避的第一次等待,就给整个集群一个呼吸窗口。

第二,CONNECTED 状态下重新收到 START 应该怎么处理?答案是忽略,或者返回一个错误。状态机不允许"已经在线却又发起连接"的语义,这个限制必须在入口处拦截,否则会出现两个并行连接互相覆盖资源的恶性 bug。

第三,BACKOFF 状态下如果收到了 CONNECTION_LOST 呢?这类事件理论上不该出现,因为连接已经断了,但为了防止事件队列里残留的老事件,实现时需要一个代际编号来丢弃过期事件。这个我在第 5 节的踩坑部分详细展开。

第四,STOP/START 的幂等性。连续 STOP 两次、连续 START 两次,都必须安全。状态机天然适合处理这种语义,因为非法事件进入当前状态后会走兜底分支,要么忽略要么告警,不会产生副作用。

3. 实现一个通用重连状态机:代码骨架与线程模型

设计清楚之后,代码反而是最简单的一步。下面我用 Python 写一个可运行骨架,展示整个线程模型和状态切换逻辑。核心思想在任何语言里都一样:状态机的状态变更必须发生在单一线程里,其他线程只负责投递事件。

3.1 分层设计:状态机不碰 socket,IO 线程只投事件

我强烈建议把代码拆成两层:

  • 状态机层:只维护状态、事件队列、定时器和回调。它完全不关心 socket 细节,甚至不关心你连接的是 TCP 还是 Unix Socket。
  • 连接管理层:真正执行connect、recv、close,把结果变成事件投递给状态机。

这样拆分之后,状态机部分可以直接脱离网络做单元测试——你只需要向事件队列里塞各种事件,断言状态是否按转换表正确迁移。这一条我自己吃过苦头:早期版本把 socket 对象直接揉进状态机里,每次想复现一个 bug 都要先造网络环境,痛苦至极。先画状态表、再用假事件驱动测试,是我重构后收益最大的实践。

3.2 状态机核心代码实现

import queue import random import threading from enum import Enum, auto class State(Enum): IDLE = auto() CONNECTING = auto() CONNECTED = auto() BACKOFF = auto() class Event(Enum): START = auto() CONNECT_OK = auto() CONNECT_FAIL = auto() CONNECTION_LOST = auto() RETRY_TIMER = auto() STOP = auto() class ReconnectStateMachine(threading.Thread): """重连状态机主类。 connector: 实际执行网络连接的连接器对象。 config: dict,包含 base_interval, max_interval, factor, jitter, connect_timeout。 callbacks: dict,包含 on_connecting/on_connected/on_disconnected/on_reconnect/on_stopped。 """ def __init__(self, connector, config, callbacks): super().__init__(daemon=True) self.connector = connector self.cfg = config self.cb = callbacks self.events = queue.Queue() self.state = State.IDLE self.attempt = 0 # 已失败次数,用于计算退避 self.running = True self._timer = None self._timer_gen = 0 # 定时器代际编号,防止过期定时器 def post(self, event): """供 IO 线程或其他线程调用,往状态机投递事件。""" self.events.put(event) def run(self): # 状态机主循环:所有状态变更只在这里发生 while self.running: event = self.events.get() self._dispatch(event) def _dispatch(self, e): old = self.state # 核心转换逻辑,对应文章里的状态转换表 if self.state == State.IDLE and e is Event.START: self.attempt = 0 self._do_connect() elif self.state == State.CONNECTING and e is Event.CONNECT_OK: self._on_connected() elif self.state == State.CONNECTING and e is Event.CONNECT_FAIL: self.attempt += 1 self._enter_backoff() elif self.state == State.CONNECTED and e is Event.CONNECTION_LOST: self._on_disconnected() self.attempt += 1 self._enter_backoff() elif self.state == State.BACKOFF and e is Event.RETRY_TIMER: self._do_connect() elif e is Event.STOP: self._cancel_timer() if self.state in (State.CONNECTING, State.CONNECTED): self.connector.close() self.state = State.IDLE if self.cb.get("on_stopped"): self.cb["on_stopped"]() elif e is Event.START and self.state is not State.IDLE: return # 非 IDLE 状态的重复 START 直接忽略 else: # 兜底:走到这里说明转换表有漏洞,或者有过期事件混入 print(f"[state-machine] unexpected transition: {self.state} + {e}") return print(f"[state-machine] {old.name} -> {self.state.name} via {e.name}") # ---- 以下为内部动作 ---- def _do_connect(self): self.state = State.CONNECTING if self.cb.get("on_connecting"): self.cb["on_connecting"](self.attempt) # connect 是阻塞操作,放线程里执行,避免卡住状态机主循环 threading.Thread(target=self._connect_worker, daemon=True).start() def _connect_worker(self): ok = self.connector.connect(timeout=self.cfg.get("connect_timeout", 3.0)) self.post(Event.CONNECT_OK if ok else Event.CONNECT_FAIL) def _on_connected(self): self.state = State.CONNECTED if self.cb.get("on_connected"): self.cb["on_connected"]() def _enter_backoff(self): self.state = State.BACKOFF interval = self._backoff_interval() # 开启新定时器前,先让旧定时器失效 self._timer_gen += 1 gen = self._timer_gen self._cancel_timer() self._timer = threading.Timer( interval, lambda: self.post(Event.RETRY_TIMER) if gen == self._timer_gen else None, ) self._timer.start() if self.cb.get("on_reconnect"): self.cb["on_reconnect"](self.attempt, interval) def _cancel_timer(self): if self._timer: self._timer.cancel() self._timer = None def _backoff_interval(self): # 指数退避公式:base * factor^(attempt-1),封顶 max_interval,再叠加抖动 cur = self.cfg["base_interval"] * (self.cfg["factor"] ** (self.attempt - 1)) cur = min(cur, self.cfg["max_interval"]) jitter = self.cfg.get("jitter", 0.0) if jitter > 0: cur *= random.uniform(1 - jitter, 1 + jitter) return cur def stop(self): self.post(Event.STOP)

这段代码的核心在于_dispatch里用if/elif显式罗列转换组合,每一种组合都对应状态转换表中的一行。self.running没有单独处理优雅退出,真实项目中可以补充一个 EMPTY 事件来结束主循环,这里不再展开。

3.3 网络连接管理与事件投递

连接管理层的核心职责是:执行 connect、启动接收循环、在检测到异常时通知状态机。下面是一个简化版本:

import socket import threading class TcpConnector: def __init__(self, host, port, recv_timeout=5.0): self.host = host self.port = port self.recv_timeout = recv_timeout self.sock = None self.closing = False # 关闭标记,防止主动关闭后继续上报事件 self.machine = None # 由外部注入状态机引用 def connect(self, timeout=3.0): self.closing = False try: self.sock = socket.create_connection((self.host, self.port), timeout=timeout) self.sock.settimeout(self.recv_timeout) # 连接成功后启动后台接收循环 threading.Thread(target=self._recv_loop, daemon=True).start() return True except OSError: self.close() return False def _recv_loop(self): try: while True: data = self.sock.recv(4096) if not data: break # 这里可以回调上层处理业务数据 except (OSError, socket.timeout): pass finally: try: self.sock.close() except OSError: pass # 只有非主动关闭时才上报连接丢失 if not self.closing and self.machine: self.machine.post(Event.CONNECTION_LOST) def close(self): self.closing = True try: if self.sock: self.sock.shutdown(socket.SHUT_RDWR) except OSError: pass try: if self.sock: self.sock.close() except OSError: pass

注意到_recv_loop里的finally做了closing判断。这是必须的:当用户主动 stop 时,connector.close()会把 socket 关闭,接收循环必然报错退出,此时如果不加判断,CONNECTION_LOST 事件会被投递到已经回到 IDLE 的状态机上,触发兜底分支,还会污染日志。

还有一个细节是connect()中的socket.settimeout。如果没有设置读超时,recv会永久阻塞,一旦出现半开连接,你只能依赖应用层心跳来拔除假死连接。这个问题我放在第 5 节细讲。

3.4 回调设计:业务侧如何感知连接状态变化

业务侧不应该直接访问状态机内部变量,而是通过回调感知状态变化。我用到的回调集合如下:

回调触发时机典型用途
on_connecting发起每次 connect 时更新界面/日志,记录重连次数
on_connected连接建立成功恢复业务发送队列,重新鉴权
on_disconnected连接丢失暂停业务发送,保存未确认数据
on_reconnect进入退避等待时暴露下次退避时长,便于监控
on_stopped用户主动停止后清理业务侧资源

这些回调有一个铁律:不能在里面做耗时阻塞操作。因为回调运行在状态机线程上,任何阻塞都会冻结整个重连机制。如果业务需要在连接建立后做一次完整的数据恢复,正确做法是把任务投递到业务线程池,回调里只做"投递"这一个动作。

4. 退避参数怎么定:指数退避、抖动与重试上限

状态机的骨架搭好后,真正影响线上表现的是退避参数。参数定得好,服务端恢复时客户端既不会错过窗口,也不会形成风暴;参数定得差,即使状态机逻辑没问题,也会在关键时刻帮倒忙。

4.1 固定 sleep 为什么不行,指数退避为什么有效

固定间隔的问题在于它用同一个压力应对所有阶段。刚断线的第 1 秒,服务端大概率没恢复,这时高频率重连是无用功;断线后的第 10 分钟,网络可能早已恢复,固定间隔又显得过于迟钝。

指数退避的思路是"每次失败后等得更久一点":第一次失败等 1 秒,第二次等 2 秒,第三次 4 秒……这样既在初期保留了快速恢复的敏感度,又在后期自然降低了系统的重试压力。它的本质是把重连行为从"客户端单方面的执念"变成"客户端与服务端恢复节奏的协商"。

4.2 一套开箱即用的默认参数

如果你不想从零调参,可以直接参考这套我用了多轮的值:

参数建议值说明
base_interval1 秒第一次退避的等待时间
factor2.0指数增长倍数
max_interval60 秒退避时间上限,防止拉开太久
jitter0.3在计算值基础上做 ±30% 随机抖动
connect_timeout3 秒单次 connect 尝试的最大耗时
recv_timeout5 秒接收循环的读超时,配合心跳使用
max_retries0(表示无限重连)或按业务要求设置上限

对应的退避序列大概是:1s、2s、4s、8s、16s、32s、60s、60s……在加入 ±30% 抖动后,实际值会在这些基准值附近波动。上限 60 秒意味着,即使服务端宕机很长时间,客户端也会保持每分钟一次的轻量探测,既不过度打扰,也不至于彻底与服务器失联。

4.3 抖动(jitter)和防惊群的设计细节

抖动的价值在我那次事故里体现得淋漓尽致。如果没有抖动,所有客户端在同一时刻失败、又在同一时刻醒来重连,恢复瞬间的连接请求会像海啸一样拍到服务端脸上。加了 ±30% 的随机抖动后,各个客户端的退避时间被打散,重连请求在时间轴上均匀分布,服务端 accept 队列的压力就平稳多了。

实现抖动时有一个容易犯错的地方:抖动必须作用在"已经算出但尚未封顶"的值上,还是作用在封顶后的值上?我的做法是先按指数计算、封顶到 max_interval,再叠加抖动。这样做的原因是防止抖动把 1 秒的基础间隔放大到 2 秒,不稳定;同时也防止 max_interval 通过抖动变成超过上限的值。

另一个容易被忽略的问题是幂运算溢出。如果 factor 是 2、重试次数超过几十次,factor ** attempt在 C 语言里可能直接溢出成负数,导致退避时间变成 0。所以生产代码里更稳妥的做法是用循环乘法逐次累乘,而不是指数运算符;Python 这种任意精度语言没有这个风险,但如果你在嵌入式或 C/C++ 环境里实现,一定要留意。

5. 实测高频踩坑:连接假死、重复定时器、回堵塞死

状态机和参数只是纸面的设计,真正把它跑死在生产环境里,你会遇到几个高频坑。这三个坑我都在实际项目中踩过,每一个都值得单独拿出来说。

5.1 坑一:TCP keepalive 不够快,必须自己做应用层心跳

很多人会问:TCP 不是自带 keepalive 吗?为什么还要自己搞心跳?因为 TCP keepalive 的默认探测周期极长,多数系统默认按分钟甚至小时计算,而且它只能证明"对端 TCP 栈还活着",无法证明"对端应用层还正常"。一个服务进程死循环卡住但内核未崩溃的场景,TCP keepalive 完全无能为力。

所以我在连接层引入了应用层心跳机制:客户端定时发送一个很小的 ping 包,服务端回 pong;如果在连续 N 个周期内既没有 pong 也没有任何业务数据,就判定连接死亡,主动关闭 socket,触发 CONNECTION_LOST。

这里有一个设计取舍:心跳周期怎么定?我通常用 15 秒发一次心跳,连续 3 次无响应才判定死连接,也就是最差 45 秒内感知故障。这个值比 TCP keepalive 快得多,又不会因为网络瞬时抖动频繁误杀。

还要注意,某些服务端协议并不会主动回应心跳包,那就要退化成"基于读超时+业务数据判断":如果超过 N 秒没有任何 ingress 数据,就主动发一个探活包,看能否收到任何响应数据。总之,心跳方案必须和你的服务端协议配合,单独为了心跳去要求服务端改协议,往往在跨团队协作时很困难。

5.2 坑二:重试定时器被重复调度,状态机重入

我最早一版代码里出过一个诡异现象:客户端日志里 CONNECTING 状态连续出现两次,重连间隔完全乱掉,甚至同时存在多个定时器在跑。排查了很久才发现,问题出在事件重复投递和定时器取消不彻底。

具体来说,recv_loop中 socket 异常触发 CONNECTION_LOST,但 socket 关闭过程又触发了另一次异常路径,于是同一个断连事实被投递了两次事件;状态机第一次收到事件后进入 BACKOFF 并启动了一个 Timer,第二次收到事件时竟然又走了一遍_enter_backoff,于是出现了两个 Timer。两个 Timer 各自到点后都投递 RETRY_TIMER,重连请求自然翻倍。

解决这个问题的组合拳是三层防护:

  • 连接层加closing标记,_recv_loop退出时只投递一次事件;
  • 定时器使用代际编号_timer_gen,每次进入退避都会让旧定时器失效;
  • 状态机转换表严格限制:只有 BACKOFF 状态的 RETRY_TIMER 才被处理,其他状态下收到 RETRY_TIMER 一律进兜底丢弃。

这三层合在一起,即使有一个过期事件漏网,也不会造成实质影响。这是我强烈建议保留的三重保险,缺一层都可能在某些极端时序下翻车。

5.3 坑三:在状态回调里做阻塞调用,把整个重连循环卡死

状态机的run循环是串行的:处理完一个事件才会取下个事件。如果有人在on_connected回调里直接做了同步的阻塞调用,比如发一个等待响应的大请求,或者同步等待数据库查询结果,状态机主循环就被冻结了。此时服务端立刻断线,CONNECTION_LOST 事件在队列里排队,重连机制完全瘫痪,但你还找不出明显报错——因为代码没有异常,只是"不动了"。

我的处理原则是:所有回调都是"信使"而不是"工人"。回调只做三类事:

  1. 更新内存状态、计数器、日志;
  2. 投递任务到业务线程池;
  3. 对不耗时的轻量操作(如设置一个标志位)直接执行。

只有业务线程池里的 worker 才允许做连接建立后的重登录、数据补发、鉴权刷新等耗时操作。这个边界一旦划清楚,状态机主循环永远不会被业务拖累。

6. 验证方法、可观测性设计与后续演进

写完状态机、配好参数、避开三个大坑之后,这套机制到底稳不稳,必须靠系统化的验证。我总结了一套故障注入清单和验收标准,每次换新项目都会照着一遍一遍过。

6.1 故障注入清单:照着这个列表测一遍

测试场景注入方式预期行为
服务端优雅关闭服务端进程调用 close客户端 recv 收到 EOF,立即触发 CONNECTION_LOST
服务端断电/网线断开直接关闭服务端网卡客户端本侧无感知,靠心跳超时判定,最差约 45 秒内进入 BACKOFF
服务端端口不可用停掉服务监听端口connect 返回拒绝或超时,进入 BACKOFF,按退避曲线重试
服务端反复重启脚本循环 kill + 启动客户端在 CONNECTED/BACKOFF/CONNECTING 间反复转移,但不得出现多个定时器
客户端主动停止调用 stop无论当前在哪个状态,都会回到 IDLE,定时器被取消
重试次数超限设置 max_retries 后长期断网进入 OFFLINE/停止重试,让业务介入

每个场景除了看最终是否成功重连,还要重点观察过程日志:状态转换是否符合预期、退避间隔是否按指数增长、有没有出现非法转换兜底告警。如果兜底分支打印了哪怕一条日志,说明你的事件或者转换表还有漏洞,不能放过。

6.2 可观测性:状态转换日志与指标

状态机最讨喜的一点是它天生适合做可观测性。每次状态转换都打印一条结构化日志,记录from_state -> to_state、触发事件、当前重试次数、下一次退避间隔,这些信息在线上排障时价值巨大。

我在实际项目里还会暴露一组基础指标:当前状态、最近一次重连耗时、累计重连次数、当前退避等待剩余时间。服务端升级重启后,打开监控面板就能看到所有客户端的重连计数在上涨、退避间隔在拉大,而不是像从前那样只能从海量日志里猜现场。

6.3 一套重连机制的验收标准

给这套机制做验收,我的标准可以浓缩为三句话:

  • 任何时刻,只有一条"通向连接"的路径在推进,不存在并行连接或并行定时器;
  • 无论网络怎么折腾,状态机都能沿着转换表走回 CONNECTED,并且每次失败后等待时间只增不减(加上抖动后也在合理范围内波动);
  • 业务侧始终能通过回调感知当前连接状态,而不需要自己去猜。

满足这三点,这套重连机制在大多数 TCP 客户端场景里就足够稳了。

回头看我做过的几次重连重构,最值钱的其实不是那几段代码,而是"先把状态转换表画出来"这个习惯。无论项目用什么语言,遇到任何连接管理问题,第一步永远是枚举状态、枚举事件、画一张表让所有人评审;表定下来,实现不过是在做翻译。你如果正在被 while True 重连逻辑折磨,不妨先把那个循环删掉,从画表开始试试。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进