在人工智能对话、实时数据看板、在线协同编辑等场景中,流式响应已成为前端与后端通信的主流方式。然而,网络波动、服务端重启或者浏览器资源限制,常常导致流式响应意外中断。用户可能正等待一个长文本生成,或实时价格更新,突然数据流“卡住了”,用户体验大打折扣。如何让前端在断连后自动重连,并实现“断点续传”——即从上次中断的位置继续接收数据,而不是从头开始?这成为开发团队必须攻克的技术难题。

流式响应中断的典型场景

流式响应通常基于 Server-Sent Events(SSE)WebSocket 实现。以当前最热门的 AI 大模型对话为例,模型逐 token 生成回答,通过 SSE 推送到前端。一旦网络短暂波动,SSE 连接断开,前端只能等待超时或手动刷新。类似地,在金融行情看板中,WebSocket 链路若断断续续,缺失的行情数据将导致图表出现缺口。

传统的重连方案往往只是重新建立连接,并请求全量数据。这在数据量巨大时浪费带宽与算力,且在前端状态复杂时容易造成数据冲突或重复渲染。更智能的做法是:记录最后一次成功接收的数据位置(如 SSE 的 last-event-id、WebSocket 的消息序列号或时间戳),在重连时告知服务端“从哪开始”,服务端据此继续推送未完成的数据块。

前端自动重连与续传的技术路径

1. 基于 SSE 的自动重连

SSE 协议原生支持 Last-Event-ID 字段。当连接意外断开,前端浏览器会自动携带该字段发起重连请求,服务端读取该 ID 后,从对应位置继续发送事件流。但浏览器的默认重连行为不够健壮:它可能等待数秒甚至数十秒,且不提供指数退避等策略。因此,开发者通常需要自己实现 EventSource 封装

具体做法是:监听 onerror 事件,在触发后主动关闭连接,并根据重试次数计算延迟(如 1s、2s、4s……),重新创建 EventSource 实例。每次收到消息时,更新本地存储的 lastEventId。服务端需在接口中支持 Last-Event-ID 解析,并缓存最近的事件队列。例如,某直播弹幕系统就采用此方案,在断连后用户不会丢失中间弹幕。

2. 基于 WebSocket 的断点续传

WebSocket 没有内置重连续传机制,开发者需要自定义协议。一种常见做法是:在消息头部加入递增的序列号(seq)。前端每次收到消息后,记录最新的 seq,同时将消息内容存入临时缓冲区。断连时,保留 seq 值;重连成功后,发送 {"type":"resume","seq":123} 给服务端,服务端从 seq+1 开始重推数据。

为了处理乱序或重复消息,前端还需引入去重逻辑。例如,使用 Set 记录已收到的 seq,重复的包直接丢弃。某跨境电商的实时价格推送系统就采用此方案,确保用户在切换网络时,价格曲线连续无缺口。

3. 基于 fetch + ReadableStream 的流式请求

现代浏览器支持通过 fetch 获取 ReadableStream,实现类似 SSE 的效果。这种场景下,数据通过 HTTP 分块传输。前端需要手动监听流的关闭,并在出错时记录已经读取的字节偏移量。重连时发送 Range 头部(如 Range: bytes=1024-),服务端从指定字节继续发送。这要求服务端支持 HTTP 断点续传,且消息边界可识别。适用于大文件流式下载或长文本逐步输出的场景。

工程实践中的挑战与优化

自动重连续传看似美好,落地时却有不少坑。第一,服务端必须是有状态的——它需要记住不同客户端的数据发送进度。这对高并发系统是巨大压力,常借助 Redis 或本地内存队列实现临时存储,并设置 TTL 自动清理。第二,幂等性设计:前端可能重发多次恢复请求,服务端需保证同一份数据不会重复推送,或者前端通过去重容忍重复。第三,用户体验层面:断连后是否显示加载提示?重连成功是否需要平滑补上缺失内容?AI 对话中若生成了一半的字,突然补全后半句,用户可能感到突兀,这时前端需要将之前的内容与补发内容无缝拼接。

某知名 AI 对话产品的技术团队曾公开分享:他们使用 SSE 加指数退避重连,并将每个 token 的索引附带在事件数据中,前端通过索引拼接到正确位置。即使断连 10 秒,对话框也能自动恢复,用户几乎无感知。

未来展望

随着实时应用的增长,流式通信的可靠性愈发关键。HTTP/3 与 WebTransport 的普及将带来更低延迟和更好的拥塞控制,但应用层智能的重连续传逻辑仍是刚需。前端开发者需要从“被动等待”转向“主动恢复”,让用户在面对网络波动时,依然享受流畅的实时体验。那根断掉的流式线,终将被一条自动连接的“智能线”重新接上。