TCP 为什么会粘包?4 种拆包方案 49 字节实测对比

0 阅读7分钟

两条 JSON 粘成一条,问题不在 TCP。

TCP 只负责把字节按序送到,从不保留“消息”边界。

应用层没规定一帧多长,接收端就只能靠猜。

我把四种常见拆包方案做成了一个能点的实验室。

图 1:默认长度前缀,49 字节流切成 6 块,3 条消息全部还原

公开仓库在 Demo Park,单个 index.html,双击就能开。

本地复跑自检 10/10 PASS,失败路径也逐条点过。

仓库地址:gitcode.com/OPCTeam/dem…

一、为什么“粘包”不是 TCP 的错

很多人以为是网络把包粘住了。

其实 TCP 根本没有“包”这个概念,它只提供字节流。

发送端 write 两次,接收端 read 一次拿到 30 字节,完全合法。

反过来,一次 write 也可能被拆成两次 read,这就是半包。

我在页面上把 49 字节拆成 9 字节一块,轨道变成 6 个分块。

顺序没变,边界全丢。

接收端看到的只是一条连续的十六进制带,哪几个字节属于哪条消息,协议不说就没人知道。

所以“粘包”和“半包”是同一个问题的两面:应用层没有帧边界。

解决方向只有一类:让每条消息自带可识别的结束条件。

常见做法就是定长、分隔符、长度前缀、自描述格式。

二、四种拆包方案,差在哪

方案编码规则一眼看懂什么时候会翻车本次实测
定长帧 16B每条固定 16 字节,不足补 0x00切固定长度就行消息长度不等就整体错位1/3 帧,错误偏移 0x0010
分隔符 0x0A正文后追加换行读到换行为止正文自带换行会被误拆4 帧,比真值多 1 条
长度前缀 2B2 字节大端长度加正文先读帧头再读正文帧头本身也可能半包3/3 帧,半包时正确等待
JSON 行一行一个 JSON,换行结尾自描述,字段可扩展体积膨胀,解析更慢183 字节,3/3 帧

定长帧的实现最简单。

每条消息固定占 16 字节,不够补 0x00,解析器每次切 16 字节。

问题是消息长度一变就全乱。

我切到“变量长度”预设,11/21/15 B 进流,第一帧后游标停在 0x0010。

页面直接报错:“定长帧要求每条消息等长,当前消息 21 字节 ≠ 16 字节,解析游标已错位”。

图 2:定长帧遇到不等长消息,解析 1 帧后错位

分隔符方案在文本协议里很常见。

正文后面追加 0x0A,接收端一直读到 0x0A 为止。

可读性好,调试也方便。

代价是正文不能随便出现分隔符,要么转义,要么换一个不会出现的字节。

“内容含分隔符”预设里,登录消息正文是 login:alice 加换行再加 v2。

解析器切出 4 帧,比真值多 1 条,多出来的那条显示为“未知分片 v2”。

图 3:分隔符把一条正文劈成两半,偏移 0x000B

长度前缀是二进制协议最常见的选择。

帧头两字节大端写正文长度,后面跟 UTF-8 正文。

接收端先读 2 字节,知道后面该收多少,再收正文。

缺点是帧头本身也可能半包。

“半包到达”预设把 49 字节切成 49 个 1 字节分块,只到达第 1 块。

页面显示“等待剩余 1 字节帧头”,0/3 帧,0 错误。

这个状态很重要:字节不够时应该等,不是报错。

图 4:1 字节分块到达,解析器停在半包等待

JSON 行是自描述格式,前端调试最舒服。

每条消息是一行 JSON,用 0x0A 结尾,字段可以随时加。

代价是体积。

同样三条消息,长度前缀 49 字节,JSON 行 183 字节,膨胀 3.7 倍。

中文按 UTF-8 三字节算,键名和引号也要占位。

图 5:JSON 行模式,183 字节、21 个分块、3/3 帧

三、实测:我踩到的三个坑与验证方法

坑一,半包不是错误。

如果看到 1 字节帧头就报“帧头不完整”,网络正常分片也会红。

正确做法是缓冲区加游标:不够就 return,等下一块到了再继续。

页面自检第 7 条专门盯这个状态,期望“等待剩余 1 字节帧头”。

坑二,定长帧的“正常路径”要单独造数据。

界面上三条消息长度不同,定长帧必然失败。

自检第 3 条用三条 16 字节的等长消息,才能验证“能解析”。

第 5 条再用不等长数据验证“会错位”。

一个策略要同时有成功和失败两条断言,否则测试全绿也说明不了边界。

坑三,分隔符冲突不能靠多试几次。

正文里的 0x0A 就是 0x0A,解析器没有上下文,无法分辨它是正文还是帧尾。

要么在编码时转义,要么选一个正文不会出现的分隔符。

这个实验室只演示冲突,没有实现转义,这是它的边界。

验证方式在页面底部。

10 条断言直接调用编码器和解析器,不依赖界面数字。

结果 10/10 PASS,总耗时 3.600 ms。

四条正常路径、定长失败、分隔符多拆、半包等待、空输入、超长拦截、逐字节恢复,各一条。

我还在表格里对了输入字节:49 / 53 / 47 / 183,和字节流的十六进制行数能对上。

四、结果与下一步:使用码道体会

现在它是一个 1600×1000 的单页,四块布局不用滚动。

左边字节流,右边解析器,底部实测表和自检。

键盘空格暂停,R 重置,1~4 切策略。

四个预设分别对应正常、错位、误拆、半包。

亲手点三下就能验收。

第一,看尺子:默认预设下确认 3/3 帧、49/49 B、0 错误。

第二,换输入:拖“每块字节”到 1,看解析器从 3 帧变成等待。

第三,看失败路径:切“变量长度”再点定长帧,必须看到偏移 0x0010 的中文报错。

三步都过,页面才算真能跑。

边界也要说清楚。

这是浏览器里的字节流模拟,没有真的开 socket。

耗时是 JS 循环 100 次的平均值,不是网络 RTT。

单帧正文上限 255 B,长度前缀理论上限 65535 B。

定长帧只适合等长消息,分隔符没有做转义。

使用码道体会,我记了五条:

  • 把失败路径写进需求,页面才会把错位和误拆当成一等公民。
  • 要求自检直接调用编解码函数,界面数字才不会自说自话。
  • 预设比参数面板更好验收,一个按钮就能把状态切到边界。
  • 先定数据真值,再让页面算,49 字节、3 条消息、偏移 0x0010 都能手算复核。
  • 让生成结果落到单文件,双击可开,读者复现成本最低。

五、准备环境:进入码道 Web

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。

本文用 WebUI 版生成。

浏览器打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source…,登录后直接在对话窗口输入需求,不需要装软件。

六、仓库与复现

仓库地址:gitcode.com/OPCTeam/dem…

git clone https://gitcode.com/OPCTeam/demo_park.git
cd demo_park\codearts-tcp-sticky-packet-lab
start index.html

也可以只下载 index.html 双击打开。

如果只想看效果,先点“默认数据”和“半包到达”,再看“变量长度 + 定长帧”的报错。

粘包不是玄学,是协议少写了一行边界。

把这四种方案各跑一遍,再遇到线上“两条 JSON 粘一起”,就能直接定位到帧头、分隔符或长度字段,而不是先怀疑网络。