TCP是流式协议,不保证消息边界,导致粘包和半包;应采用固定长度头(如4字节小端序长度)+循环读取的拆包方案,并校验长度合理性。

为什么 TcpClient 读出来的数据总是“粘”在一起?
因为 TCP 是流式协议,不保证应用层消息边界。你发了两次 Send(),对方一次 Read() 可能拿到两段数据拼在一起;也可能第一次只读到半条消息——这就是粘包和半包。不是你代码写错了,是 TCP 本来就这样。
常见错误现象:NetworkStream.Read() 返回字节数小于预期、JSON 解析失败、XML 报文头错乱、自定义协议里长度字段对不上实际内容。
- 别依赖单次
Read()读完一条完整消息——它只按缓冲区和网络状况返回可用字节 - 服务端和客户端必须用同一套拆包逻辑,否则一方认为“到包尾了”,另一方还在等后续字节
- 不要在
Read()后直接转字符串再分割(比如用\n),二进制协议里根本没换行,UTF-8 的 BOM 或多字节字符会让切分彻底失效
用固定长度头 + 消息体怎么写最稳?
这是 C# 网络通信里最常用也最可控的方案:前 4 字节存整个消息体长度(int,小端序),后面紧跟真实数据。解包时先收够 4 字节,解析出长度 bodyLen,再循环收满 bodyLen 字节才组装成完整消息。
关键点在于「循环收」——NetworkStream.Read() 可能一次只给 1 字节,你得自己攒够数。
- 发送端必须用
BitConverter.GetBytes(len)写入长度,并确保BitConverter.IsLittleEndian一致(.NET Core/5+ 默认小端,但跨平台时建议显式指定) - 接收端解析长度后,要检查
bodyLen是否合理(比如不能 > 1MB 或 - 别用
stream.Read(buffer, 0, buffer.Length)一次性读——它不保证填满,得用 while 循环 + 偏移量累加
示例片段(接收逻辑核心):
int headerSize = sizeof(int);
byte[] headerBuf = new byte[headerSize];
int totalRead = 0;
while (totalRead < headerSize)
{
int n = stream.Read(headerBuf, totalRead, headerSize - totalRead);
if (n == 0) throw new IOException("Connection closed");
totalRead += n;
}
int bodyLen = BitConverter.ToInt32(headerBuf, 0);
byte[] bodyBuf = new byte[bodyLen];
totalRead = 0;
while (totalRead < bodyLen)
{
int n = stream.Read(bodyBuf, totalRead, bodyLen - totalRead);
if (n == 0) throw new IOException("Connection closed");
totalRead += n;
}
StreamReader 和 NetworkStream 混用会出什么问题?
会丢数据、阻塞、或者把包头当文本读走。因为 StreamReader 内部有缓冲区,它可能为了找换行符,从 NetworkStream 多读了几字节进去,而这些字节再也吐不回底层流——下一次你再想读包头,发现第一个 int 已经被吃掉了。
使用场景:仅当你确认协议是纯文本、每条消息以 \r\n 结尾、且不关心二进制兼容性时,才考虑 StreamReader.ReadLine()。绝大多数工业级协议(如自定义二进制、Protobuf、MessagePack)必须绕过它,直接操作 NetworkStream。
- 一旦用了
StreamReader,就别再碰底层NetworkStream——两者缓冲区不共享,行为不可预测 -
StreamReader默认 UTF-8,如果对方发的是 GB2312,中文直接变乱码,且无法通过参数修正(构造函数传Encoding.GetEncoding(936)也不保险,因 BOM 判断和流位置问题) - 想“方便地读字符串”?可以等整包收完后,再用
Encoding.UTF8.GetString(bodyBuf)转——主动权在你手里
Protocol Buffers 或 JSON over TCP 怎么避免粘包?
它们本身不解决粘包,只是序列化格式。你仍得在外面套一层长度前缀,或改用带帧边界的传输层(比如 gRPC over HTTP/2 自带消息边界,但裸 TCP 不行)。
性能影响:Protobuf 序列化快、体积小,但需要预生成类型;JSON 人眼可读,但解析慢、长度不可控——如果不用长度头,有人用 {} 嵌套深度攻击,导致 JsonSerializer.Deserialize 卡死或 OOM。
- 别把
JsonSerializer.Serialize的结果直接Send()——它不带长度信息,接收方不知道该读多少字节 - Protobuf 的
CodedInputStream有ReadMessage方法,但它依赖底层流已提供完整一帧;你得先按长度头收好字节,再喂给它 - 如果协议允许,优先选 length-prefixed + Protobuf,比 JSON + 长度头更省带宽、更少 GC
事情说清了就结束:粘包不是 bug,是 TCP 的出厂设置。所有“自动拆包”的封装库,背后都是帮你做了长度头 + 循环读——只是藏起来了。你自己写的那部分,才是最该盯紧的地方。


















