1. 项目缘起:为什么我会写 Tcp SSE Utils
先说说这个工具诞生的背景。我在做物联网网关和数据中台项目时,碰到一个很典型的场景:设备端通过 TCP 长连接上报数据,而前端 Web 页面需要实时看到这些数据的变化。最开始用的是 WebSocket,后来发现对于"单向推送占绝大多数、客户端极少上行"的场景来说,WebSocket 多少有点"杀鸡用牛刀"——握手复杂、服务端实现麻烦、还要处理心跳和二进制分帧。直到我认真用了一次 SSE(Server-Sent Events),才意识到这个"老技术"在特定场景下有多顺手,于是把 TCP 和 SSE 结合起来做了一套工具集,也就是今天要聊的 Tcp SSE Utils。
这套工具解决的核心问题有三个:一是 TCP 服务端的管理与数据接入,包括连接建立、粘包半包处理、心跳保活、断线重连;二是把 TCP 数据流转换成 SSE 流式输出,推给浏览器端;三是提供在线调试能力,让开发者不用写前端页面就能验证整条链路是否通。适合谁来参考?如果你正在做物联网数据采集、实时监控大屏、日志实时推送、AI 对话流式输出这类项目,这篇内容应该能帮你少踩不少坑。
从热词里也能看到大家的痛点非常集中:tcp三次握手、sse流式输出、tcp连接、断线自动重连、流式输出markdown渲染器等等。这些词背后对应的是同一个需求——怎么稳定地把源源不断的数据从后端推到浏览器,并且保证连接不轻易断、断了能自动恢复、数据顺序不乱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计思路拆解
2.1 TCP 和 SSE 各自的定位
先说清楚这两个协议的关系。TCP 是传输层协议,负责在两个程序之间建立可靠的双向字节流连接。它的核心特点是面向连接、可靠传输、按序到达,我们常说的"三次握手"就是建立连接时双方确认收发能力的过程,而"四次挥手"是断开时的四次交互。
SSE 是应用层协议,全称 Server-Sent Events,建立在 HTTP 之上。客户端通过普通的 HTTP 请求连接服务端,服务端保持这个连接不关闭,然后持续向客户端推送文本数据。它和 WebSocket 最大的区别是单向的——数据只能从服务端流向客户端,但正因为单向,它实现起来极其简单,而且天然支持自动重连和事件 ID。
把 TCP 和 SSE 放在一起,其实就是做一个"协议翻译器":TCP 那边面对的是千奇百怪的设备或客户端,SSE 这边面对的是浏览器。中间的工具集负责把 TCP 收到的数据规范化处理,再通过 SSE 通道推送给前端。
2.2 为什么不用 WebSocket 而用 SSE
很多人会问这个问题。我个人的判断标准很简单:如果你的场景是"服务端主动推、客户端偶尔发个指令",SSE 是更轻量的选择。WebSocket 的握手需要额外的 Upgrade 请求,服务端要维护完整的帧协议,消息格式要自己定义,出错排查的成本更高。
SSE 的优势在于:
- 基于 HTTP,不需要额外端口,可以直接复用现有网关和负载均衡
- 自带断线重连机制,浏览器端的事件源对象会自动重连
- 有事件 ID,客户端可以断点续传,不丢消息
- 可以用普通的文本格式调试,在浏览器地址栏直接看输出
劣势也有,就是客户端上行不方便,以及数据只能文本格式传输,二进制要先编码。但这两个劣势在我的场景里基本不构成问题。
2.3 工具集的模块划分
Tcp SSE Utils 整体拆成四个模块:
- TCP Server 模块:负责监听端口、接受连接、读取数据、处理粘包半包、维护连接状态。
- 连接管理器:负责连接池管理、心跳检测、断开自动重连、连接超时控制。
- SSE 推送模块:把内部数据总线上的消息序列化成 SSE 格式,推送给订阅的浏览器客户端。
- 调试辅助模块:提供一个网页版的在线测试页面,可以手动建立 TCP 连接发数据,也可以直接查看 SSE 推送效果。
这四个模块互相独立、通过内部接口通信,这样任何一个模块出问题都不会拖垮整个系统。
3. 核心细节解析与实操要点
3.1 TCP 粘包半包问题的处理
这是 TCP 编程里最常见、也是最容易翻车的问题。TCP 是流式协议,它不保证一次 Recv 调用拿到的数据正好是一条完整的业务消息。比如设备连续发了三条消息,TCP 可能把它们合并成一个数据包给你,这就是"粘包";反过来,一条很长的消息可能被拆成两三次读取,这就是"半包"。
常见的解决方案有三种:固定长度、分隔符、长度前缀。固定长度适合协议简单的场景,但浪费带宽;分隔符适合文本协议,比如用换行符作为边界;长度前缀是通用性最强的方案,在消息头部用 4 个字节表示消息体的长度。
我在工具集里默认采用长度前缀方式,具体是这么设计的:前 4 字节是大端序的 uint32 长度值,后面跟着对应长度的消息体。读取时先读 4 字节,解析出长度,再继续读够这么多字节才算一条完整消息。
还有一个隐藏的坑:Read 可能一次只读到部分数据,需要用到缓冲区累积。我的做法是每次读到的数据先追加进 buffer,然后循环尝试从 buffer 中解析完整的帧,直到 buffer 中的数据不足以构成一帧为止。
3.2 心跳保活与断线重连
TCP 连接本质上是"假连接"——如果一端突然断电或者网络中断,另一端可能很长时间感知不到。操作系统自带的 TCP KeepAlive 默认关闭且探测周期太长(Linux 默认 2 小时),所以应用层心跳是必须的。
我的方案是双向心跳:
- 服务端每隔 30 秒检测一次连接的空闲时间,超过 60 秒没有收到任何数据的连接视为超时,主动断开。
- 客户端(或者设备端)每隔 20 秒发送一个心跳包,可以是空帧或者特定的心跳指令。
断线重连必须考虑"退避"策略。如果断开后立即重连,极端情况下几十个设备同时断开重连,服务端会被打爆。我用的是指数退避加抖动:第一次重连等 1 秒,第二次等 2 秒,第三次等 4 秒,依次类推,最大不超过 60 秒,并且每次加上一个 0 到 1 秒的随机值打散重连时间
