关于本文
本文是通过利用生成式 AI 的自动化生成流程创建的。在查阅了 RFC 9293 和 Microsoft Learn 的 TcpClient 官方资料的同时,通过在 localhost 上观察 TCP 的字节流,梳理了实际协议所需的“分隔”设计。验证状态:📘 已确认官方规范・运行未验证
在 TCP 中,即使发送方调用了 Write 两次,也不能保证接收方能分两次将其接收。这是因为 TCP 处理的不是消息序列,而是有序的字节流。
如果不了解这种差异,就会导致诸如“JSON 偶尔在中途截断”、“两件数据黏连在一起”等故障。
首先尝试
打开两个 PowerShell。
接收端:
$l = [Net.Sockets.TcpListener]::new(
[Net.IPAddress]::Loopback,
50555
)
$l.Start()
$c = $l.AcceptTcpClient()
$s = $c.GetStream()
$b = New-Object byte[] 1024
$n = $s.Read($b, 0, $b.Length)
[Text.Encoding]::UTF8.GetString($b, 0, $n)
$s.Dispose()
$c.Dispose()
$l.Stop()
发送端:
$c = [Net.Sockets.TcpClient]::new('127.0.0.1', 50555)
$s = $c.GetStream()
foreach ($x in 'HELLO', 'WORLD') {
$b = [Text.Encoding]::UTF8.GetBytes($x)
$s.Write($b, 0, $b.Length)
}
$s.Dispose()
$c.Dispose()
有时接收端会显示 HELLOWORLD,有时单次 Read 只能获取到中途的一部分。
发送次数与接收次数不对应
sequenceDiagram participant C as Client participant T as TCP byte stream participant S as Server C->>T: Write "HELLO" C->>T: Write "WORLD" T-->>S: Read "HELLOWORLD" のこともある T-->>S: Read "HEL" + 次のReadで残り のこともある
TCP 保证的是“顺序”和“可靠性”,而不是应用程序执行的 Write 边界。
尝试将缓冲区(buffer)设为 3 字节
将接收缓冲区缩减为 3 字节后,可以肉眼看出单次 Read 无法获取全部内容。
$b = New-Object byte[] 3
通过这个实验,我们会明白“只要 Read 一下就能完成一个消息”这种设计是危险的。
实际协议中如何进行分隔
有三种典型的方法。
| 方法 | 示例 | 特点 |
|---|---|---|
| 分隔符 | 直到换行为止算 1 件 | 适合文本 |
| 固定长度 | 始终为 128 字节 | 简单但灵活性较低 |
| 长度前缀(Length prefix) | 头部 4 字节为正文长度 | 适合二进制 / API |
例如,如果是换行分隔,则不是每次 Read 就将其字符串化并结束,而是将其积蓄在接收缓冲区中,把“到达换行符为止”的内容作为 1 件数据提取出来。
如何在工作中应用
TCP 套接字通信
自研协议
固定长度电文
设备通信
在 TCP 上传输的 JSON 或 CSV
与旧业务系统的对接
仅凭“在测试环境中每次都能一次性接收成功”是不能作为正确实现的依据的。当通信量、操作系统、网络状况、发送时机发生变化时,分割单位也可能会发生变化。
注意事项
Read()的返回值n是实际接收到的字节数并不一定能完全填满缓冲区大小
Read() = 0是表示连接结束的重要状态UTF-8 字符占用多个字节,因此也需要考虑字符边界
在生产协议中确定最大消息长度
总结
TCP 是字节流,不保持消息边界
Write 次数和 Read 次数不一致
应用层需要有分隔规则
使用换行、固定长度、长度前缀等方式
正确处理 Read 的返回值和连接结束
