SYSTEMS NOTES阅读地图
7个主要章节
4张图片与图示
2组对照与手算表

从一个具体请求或状态变化出发,追踪数据流与方法成立的条件。

章节速览7 节 · 展开选择

网络基础解释连接和可靠传输,但连接建立以后,接收程序怎样知道“一条消息结束了”?发送端调用两次写入,并不意味着接收端恰好读到两次、每次一条。

这里把问题缩小为一个可手算的应用协议:两字节大端无符号长度,后接指定数量的正文原始字节。这是本文自定义的教学格式,不是 TCP 自带的头部,也不是 HTTP 报文格式。

TCP 保留字节顺序,不保留消息边界

RFC 9293 §2.2将 TCP 描述为可靠、有序的字节流服务;§3.7明确指出,TCP 段边界与应用读写缓冲区边界没有对应保证。

应用写入 ABC 和 DE,接收程序可能读到 A / BCDE,也可能读到 ABCD / E。只要连接正常交付,重组后的字节顺序仍是 ABCDE。读取切分、TCP 分段和抓包显示的分段是不同的观察层,下面图中的分块只表示应用读取结果,不表示网线上真实报文。

消息ABC和DE编码为00 03 41 42 43 00 02 44 45,相同九个字节可按不同读取边界交付
原创九字节示意;应用分帧边界由长度字段解释,读取边界不承担这个职责。查看原图

所谓“粘包、半包”常是应用对读取结果的描述,不是 TCP 随意破坏了消息。关闭 Nagle、设置更大的接收缓冲区、等待一小段时间,都不能替代协议分帧。PSH 也不是应用消息结束标记,不能据此设计读一条消息的循环。

三种常见分帧方案

方案 如何确定结束 必须先约定的条件
固定长度 每累计 K 字节形成一帧 K 固定,填充和版本变化规则明确
分隔符 扫描到约定标记 正文含标记时怎样转义;标记可跨读取块;限制最大累计长度
长度前缀 先读头部,再读指定字节数 字段宽度、字节序、长度是否含头部、上限、零长度和错误策略

用连接关闭表示结束也可构成一种协议约定,但不适合在同一方向继续串接多条独立消息。HTTP/1.1 的正文长度有自己的优先级规则,不能把本文格式套进去;可从 RFC 9112 §6.3继续阅读。

两字节头部怎样解释

本文长度 L=256×第一个头字节+第二个头字节,只计正文,不含两字节头部。线上格式能表达 0~65535,但本例接收上限设置为 1024 字节;超过上限立即拒绝,不先按声明长度分配正文缓冲区。零长度正文合法,立即输出一条空消息,随后继续读下一帧。

ABC 的 ASCII 正文为十六进制 41 42 43,长度为 3,因此整帧是 00 03 41 42 43;DE 是 00 02 44 45。两帧串接共 9 字节。

长度必须按编码后的字节计算。比如 UTF-8 的“海”是 E6 B5 B7,共三字节,头部写 00 03,而不是按一个汉字写 00 01。正文完整后再按约定编码解码;一个字符的编码也可以跨读取块。Python Socket HOWTO强调 send/recv 不保证一次完成所期望的数据量,长度字段本身也可能要多次读取。

四次读取,仍然恢复两条消息

设四次成功读取依次返回 00、03 41 42、43 00、02 44 45。下面的“余量”是解析器尚未解释的后缀,不是下一次系统调用必定返回的内容。

读取后 新增字节 本次解析动作 留下的状态 本次输出
1 00 头部不足两字节 头部前缀 00 无
2 03 41 42 读出 L=3,正文只有两字节 等待 ABC 的最后一字节;已有 41 42 无
3 43 00 正文凑齐,输出 ABC;剩下 00 下一帧头部仅一字节 ABC
4 02 44 45 凑齐头部 L=2,正文齐全 回到等头部,余量为空 DE
解析器在等两字节头部与等L字节正文之间转换,完整帧输出后继续解析余量,超长或截断进入错误
持续保留状态与余量;一次读取可能输出零条、一条或多条完整消息。查看原图

解析器应保持什么不变量

每次处理新块后,已经输出的消息必须恰好对应已消费字节流中的完整帧前缀,既不能漏掉,也不能重复。未输出的部分只能是下一帧的头部前缀,或已知长度的正文前缀;它们不能被当作独立消息。

推理从空流开始:空前缀满足条件。头部不完整时保留,完整时唯一确定 L;检查上限后,不足 L 字节继续等待,达到 L 后恰好消费该正文并输出。随后对后缀重复同一过程。因此只要编码合法,输出只依赖串接后的字节序列,不依赖成功读取的切分方式。错误输入应按协议关闭或报错,不随意扫描一个“看起来合理”的长度重新同步。

实现可以采用带游标的缓冲区或分段队列,不必反复删除数组头部;只要每字节搬移次数有界,总解析工作量可为 O(B+M),B 是字节数、M 是消息数。上限 L_max 限制的是单帧正文,不会自动限制一次读取块、完整消息队列或总连接数的内存。大流量时还要流式消费、限制队列并施加背压。

自检:一次读取包含两个完整帧,应只返回第一条吗?

应按接口约定输出两条,或输出第一条后保留余量供下一次解析。不能丢弃后缀,也不能把两帧正文拼成一条。解析循环必须在出帧后继续检查现有余量,不必等待新的网络读取。

EOF、超时和业务成功不是同一件事

对 Python 阻塞流 socket 的正长度 recv 请求,返回空字节串表示这一方向已到 EOF;非阻塞时暂时没有数据通常以 would-block 异常报告,而不是空正文消息。本文的空正文消息在线上仍带 00 00 两字节头部,和 EOF 不同。

EOF 到来时,解析器处于等头部且无剩余字节,才是帧边界上的正常结束;若只剩一个头字节,或正文尚未达到 L,则是截断。TCP 允许半关闭,读到 EOF 不等价于本端发送方向已经关闭。连接复位、读取超时也应分别处理;超时只说明等候预算耗尽,不能把残缺正文强行作为完整帧。

TCP ACK 表示传输层接收确认,不等于接收端业务已校验、落盘或执行。若操作需要业务确认,应用协议必须定义响应、请求标识及重复处理规则;不能因客户端未收到响应就断言服务器没有执行,可衔接重试与安全重放。

自检:读到 00 03 41 42 后收到 EOF,是否输出 AB?

不输出。长度声明为三字节,而正文只有两字节;这是截断帧。之前已经完整输出的帧保持有效,本帧不能通过修改长度或补零伪装为完成。

从局部模型到真实协议

  • 为头部和正文设置长度、时间及连接级资源预算;仅有单帧上限不足以防止缓慢发送与排队累积。
  • 发送也要处理部分写入;成功将字节交给 socket 不代表对端业务成功。
  • 先按字节分帧,再解释正文;文本编码、压缩、校验与认证由各自协议定义。
  • TLS 记录边界也不应被直接当成应用消息边界;HTTP/3 使用 QUIC,不能套用“所有 HTTPS 都先建立 TCP”的流程。

本篇只核算合成字节序列的分帧,不测量真实网络分段、吞吐量或操作系统时序。继续阅读:网络基础、HTTP 缓存与资源路线。