语音会话跑到一半断开,先别急着自动重连把日志刷满。判断顺序通常是:先把本次断开的关闭码、断开时刻、断开前最后一条数据的时间戳记下来,再拆成两个问题看——断开前是不是长时间没有数据、这条连接已经存活了多久。断开前长时间静默、心跳往返时间明显抖动,更像网络抖动或链路静默;如果连接一直有数据,存活时长又反复落在某个数值附近,更像会话到期或服务端策略回收。两类原因对应不同的重连动作:网络抖动适合退避重连并尽量续接上下文;会话到期通常只能新建会话,重点是确认上下文怎么补回来。
会话中途断连,先分清网络抖动与会话过期,再决定重连策略。适用场景:长时语音会话反复断开且原因不明。操作动作:记录关闭码、断开时间、存活时长、断开前最后一帧间隔,按退避策略重连。验证方式:重连后确认会话 ID 是否变化、上下文能否续接、未确认消息是否重发。风险边界:这个区分只对已采集到的样本成立,网络与服务端策略可能同时起作用,不要凭单次断开就下结论。
记录每次断开的关闭码、关闭时间和当时进行到的回合
同一次断开,也要先分清是谁发起的。主动关闭一般会带正常的关闭码和关闭原因字符串,是被动断开(链路超时、进程被杀、对端异常)时,客户端往往只拿到一个异常关闭码,原因字段是空的。这两类在日志里必须能一眼看出来,否则后面所有判断都站不住脚。
需要记录的字段,建议按连接粒度固定下来:会话 ID、连接 ID、连接建立时间、断开时间、存活毫秒数、关闭码与关闭原因、关闭发起方(本地还是远端)、最后一条入站数据距今多久、最后一条出站数据距今多久、断开时进行到第几个回合、这个回合是等待用户说话还是等待服务端返回、当前已重连次数。记录位置放在客户端统一的连接事件回调里,一处写入,不要散落在业务代码各处;如果服务端侧也保留连接日志,用会话 ID 和连接 ID 去对齐,两边的断开时间差往往能说明是链路先断还是进程先退出。
// 连接关闭回调里的结构化日志骨架(字段名按自己的日志规范替换)
{
"event": "session_close",
"session_id": "s-xxxx",
"conn_id": "c-xxxx",
"opened_at": "2025-01-01T10:00:00Z",
"closed_at": "2025-01-01T10:03:04Z",
"alive_ms": 184300,
"close_code": 1006,
"close_reason": "",
"initiator": "remote",
"last_inbound_ms_ago": 21200,
"last_outbound_ms_ago": 340,
"turn_index": 7,
"turn_state": "awaiting_server",
"reconnect_attempt": 0
}
这段日志的价值在于:关闭码和发起方回答“谁断的”,两个 ms_ago 字段回答“断开前是不是已经没数据了”,turn_index 回答“重连后需要从哪里接”。先把这个记全,再看下两节的对照。
记录心跳往返时间,观察断开前是否长时间没有数据
心跳记录方式不用太复杂:应用层按固定周期发一个轻量 ping,收到 pong 时算一次往返时间,同时无论收到什么数据帧都更新“最后入站时间”。心跳日志不要只记成功,超时和重试也要记,否则断连前那几次失败会被吞掉。
[heartbeat] ts=10:02:40 rtt_ms=45 last_inbound_ago_ms=180 result=ok
[heartbeat] ts=10:02:55 rtt_ms=60 last_inbound_ago_ms=200 result=ok
[heartbeat] ts=10:03:10 rtt_ms=- last_inbound_ago_ms=1500 result=timeout
观察窗口建议取断开前 10 到 30 秒,具体长度结合自己的心跳周期确认。重点看两件事:一是断开前最后一帧入站数据距离断开有多久,二是这段时间 RTT 是平稳还是突然拉高、连续超时。如果关闭前已经静默了远超心跳周期的时长,并且伴随超时,偏网络静默;如果一路都有数据直到关闭码出现,更偏对端主动关闭或策略回收。
| 断开编号 | 断前无数据时长 | 断前 RTT 走势 | 关闭码 | 倾向判断 |
|---|---|---|---|---|
| 1 | 约 3 个心跳周期 | 先升高后连续超时 | 异常码 | 偏网络抖动 |
| 2 | 小于一个心跳周期 | 平稳 | 正常码 | 偏对端主动关闭 |
| 3 | 约 1 个心跳周期 | 平稳 | 异常码 | 需结合上一节存活时长再看 |
这张表按实际日志逐行填,不要提前预设结论。填到十几行之后,抖动和策略回收通常就能分开。
把断开时间与会话时长限制对照,看是否规律出现
对照方法很直接:把每次断开的存活时长列出来,按长短排序,再看是否在某个数值附近聚堆。同时把配置里已知的会话时长上限、空闲超时、令牌有效期列在一旁,比较断开点与这些上限的差距。差距很小且反复出现,会话到期这条线就值得优先怀疑;存活时长散布得很开,则更可能是网络侧。
样本数量上,建议先积累到几十次断开再判断,样本太少时一次网络波动就会把规律带偏。规律性判断依据可以看三点:同类断开是否集中在同一时长区间;同一时长区间内关闭码和关闭原因是否一致;换一个网络环境(例如换一条出口或换个时段)后这个聚堆是否还出现。三点都成立,才适合把它当成会话过期来设计重连逻辑。
按退避策略重连,并确认重连后的会话状态
重连不要固定间隔猛冲。用带抖动的指数退避,并设一个上限,避免服务端短时不可用时把请求堆上去。下面是一段通用骨架,参数按自己的环境和压力预期调整。
// 退避重连骨架(通用示例,非特定 SDK 接口)
const baseDelay = 500; // 首次等待,毫秒
const maxDelay = 30000; // 单次等待上限
let attempt = 0;
function nextDelay() {
const exp = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
const jitter = Math.random() * exp * 0.3; // 抖动,避免同时重连
attempt += 1;
return Math.floor(exp * 0.7 + jitter);
}
// 连接成功后按需重置 attempt,并在日志里带上 attempt 值
重连成功不等于恢复完成,还要按顺序检查这些状态项:会话 ID 是沿用还是新建;新建的话服务端是否还认得之前的上下文;上一个未完成的回合是否已作废,需不需要把用户最后一段话重发;临时凭证或鉴权令牌是否还在有效期内;本地缓存的对话片段与远端实际上下文是否一致。验证方式也很具体:重连后主动走一个完整回合,观察是否出现上下文缺失或回合号错乱;对比重连前后的会话 ID 和回合序号,把结果补回第一节的日志字段里。这样连续几轮之后,断开原因和恢复动作才会形成稳定对应。