TCP本身不保证“一次Read对应一次Send”,故直接用NetworkStream.Read读原始字节流必遇粘包或分包;首选Length-Prefix协议,即固定4字节大端header存body长度,再按长读body,边界清晰、无转义依赖、不浪费带宽,需设超时与最大缓冲限制防OOM。

直接用 NetworkStream.Read 读取原始字节流,不加协议解析逻辑,一定会遇到粘包或分包——这不是你代码写错了,而是 TCP 本身就不保证“一次 Read 对应一次 Send”。
为什么 Length-Prefix 协议是首选方案
长度前缀(Header+Body)是 C# 中处理 TCP 粘包最通用、最可控的方式。它把每个消息拆成两部分:header(固定长度,如 4 字节 int32 存 body 长度) + body(变长业务数据)。接收端先读够 header,再按长度读 body,边界清晰、无歧义、无需转义。
相比分隔符法(如 \n),它不依赖数据内容;相比固定长度法,它不浪费带宽、适配任意大小消息。
-
header推荐用int32(4 字节),兼顾表达范围(最大约 2GB)与跨平台字节序一致性 - 必须统一使用
BitConverter.GetBytes(len).Reverse().ToArray()或BinaryPrimitives.WriteInt32BigEndian显式指定网络字节序(大端),避免 Windows(小端)与嵌入式设备(常大端)通信错乱 - 如果业务消息普遍很小(int16(2 字节)节省开销,但需在协议文档里明确约束最大长度
手写解析器:如何安全读取不完整数据
核心陷阱在于:TCP 的 NetworkStream.Read 可能返回任意字节数(0 到 buffer.Length),绝不能假设“一次调用就能读满”。必须用循环 + 偏移量 + 剩余长度状态机来处理。
以下逻辑必须封装进独立方法,不可裸写在事件回调里:
- 维护一个
List<byte></byte>或MemoryStream作为接收缓冲区,持续追加新读到的字节 - 每次追加后,检查缓冲区是否 >= 4 字节(header 长度):不够就继续等;够了就读 header,解析出
bodyLength - 再检查缓冲区总长度是否 >=
4 + bodyLength:不够就继续等;够了就截取完整包,从缓冲区头部移除已处理字节 - 务必设超时和最大缓冲区限制(如 10MB),防止恶意客户端发超长 header 导致 OOM
TouchSocket 的 FixedHeaderPackageAdapter 怎么用才不踩坑
TouchSocket 封装了成熟的长度前缀解析,但默认行为容易被忽略细节:
- 构造时传入的
headerSize必须严格等于你协议中 header 的字节数(如 4),否则解析失败 -
headerSize和bodySize是分离的:它只负责读 header,body 长度由你从 header 解析得出,不是硬编码值 - 若服务端用的是小端 header,需继承
FixedHeaderPackageAdapter并重写GetBodyLength方法,用BitConverter.ToInt32(bytes, 0)替代默认的大端解析 - 它不会自动处理“header 本身被分包”的情况(例如 header 被拆成两次
Read),但内部已用缓冲区管理,只要配置正确就不会暴露这个问题
实际调试中最容易漏掉的三件事
粘包问题往往在压测或弱网下才爆发,上线前必须验证:
- 发送端连续快速发 5 条短消息(每条 20 字节),接收端是否稳定解析出 5 条,而非合并成 1 条或丢包
- 手动模拟分包:用 Wireshark 抓包,确认发送端发出的确实是“4 字节 header + N 字节 body”,而不是 header 和 body 被 TCP 层拆开(那是正常现象,你的解析器必须扛住)
- 故意发一个超大消息(如 5MB),观察接收端内存增长是否平滑、有无 GC 峰值,以及超时机制是否触发(比如 30 秒内没收完就断连)
协议解析不是一次性写完就结束的事——它藏在每一次 stream.Read 调用背后,也藏在你没写的那行边界判断里。


















