摘要

  • TCP 提供可靠、有序的字节流,却不保留调用与回复的边界;ONC RPC 因此把每条 RPC 消息放进一条由一个或多个片段组成的记录。
  • 每个片段前有四字节大端标记:低 31 位是该片段的数据长度,最高位只表示读完这些字节后记录结束。
  • 记录完整不等于动作完成;XDR 解码、XID 对应、认证、授权、过程结果与持久效果仍需各自的证据。

一次读取可以装下三条消息,也可以装不下一枚标记

应用向操作系统读取数据,返回长度为二,并不说明对端只发送了两个字节。它可能只是得到四字节标记的前半。解析器必须保存它,等待余下部分,不能把这次系统调用的末尾当成协议边界。

下一次读取又可能很大:补齐标记、装下整个片段、接着出现第二个片段,甚至还带上下一条 RPC 消息的开头。发送端的 write、TCP 分段、IP 数据报与接收端的 read 都可以采用不同边界,而字节顺序仍然完全正确。

RFC 793 已把 TCP 描述成有序字节服务,并明确 PUSH 不能提供记录边界。RFC 9293 延续了可靠、有序字节流的定义。报文段是运输包装,不是远程过程的语法。

1988 年的四个字节回答了两个问题

1988 年 4 月发布的 RFC 1050 把这个办法称为 record marking。每条 RPC 消息对应一条 RM 记录,记录由一个或多个片段组成。两个月后的 RFC 1057 取代前者,保留同一线上格式。

每个片段从一个四字节无符号大端数开始。低 31 位给出紧随其后的数据长度,可以从零到 2^31 - 1。最高位是一项布尔判断:零表示同一记录后面还有片段;一表示这是最后一个片段,读完所声明的数据后,记录才结束。

这两个字段授予不同而狭窄的权限。长度允许接收端跨过当前片段;终止位允许它在那个位置关闭记录。若最高位为一,仍须收齐该片段的数据。若长度已经满足而最高位为零,仍不能把记录交给上层当作完整消息。

解析器靠状态,不靠包边界

接收端首先积累四个标记字节,再拆出最高位和长度。随后它准确消费指定数量的流字节,无论这些字节分散在多少次读取里。片段数据被并入当前记录,最后才检查先前保存的终止位。

终止位为零,下一组四字节仍属于同一记录的下一片段。终止位为一,当前累计结果才形成一条完整 RPC 消息,随后四字节是下一条记录的标记。这个状态机只依赖顺序、精确计数和本地保存的上下文。

规范允许零长度片段。它没有数据,却仍有片段身份和是否终止的选择。相反,连接在片段正文中途关闭,不会自动得到一条较短的合法记录;那是未完成片段。解析器可以证明边界没有到来,不能据此证明远端过程从未执行。

这里的片段不是 IP 分片

record fragment、TCP segment、IP fragment、一次套接字写入和一个 XDR 值是五种不同对象。一条 RM 片段可以横跨多个 TCP 报文段;同一个 TCP 报文段也可以承载多个标记、片段或完整记录。重传可能改变抓包表面,却不改变重组后的字节流。

最高位也不是文件结束、连接结束、批处理结束或成功标志。CALL 与 REPLY 分别是一条 RPC 消息,在 TCP 上各有自己的 RM 记录。记录终点只说明编码消息到哪里为止,不说明程序号存在、凭据通过、参数正确、过程已提交或回复已持久保存。

这正好补上 PUSH 不愿承担的职责。PUSH 可以请求尽快移动已有字节;record marking 则给 RPC 解析器可计算的语法边界。一个影响时机,一个定义结构。

标记故意不属于 XDR

RPC 消息内部使用 External Data Representation。RFC 4506 规定 XDR 的数值、长度、填充与类型结构,使不同机器能解释同一个 CALL 或 REPLY。

可是各版 RPC 规范都特意说明:四字节记录头不是 XDR 标准形式。它与 XDR 无符号整数采用相同字节顺序,却在完整 RPC 值被解码之前工作。若必须先读懂整条消息才能知道消息终点,流解析会陷入循环依赖。

分层还保留两类不同失败。接收端可以在解释过程以前,因不合理的 framing 声明而拒绝;也可以得到边界完全正确、内部 XDR 或 RPC 内容却无效的记录。“消息在此结束”和“消息正确”从来不是同一事实。

三十一位不是远程内存配额

长度只约束当前片段,不是建议分配量,也不是整条记录的总长度。记录可以拥有多个片段,基础规范没有给出统一累计上限。若实现看到长度便无条件一次分配,就把线上坐标变成了发送端支配接收内存的权力。

因此运营者必须在规范之外建立本地资源边界:累计记录字节、片段数量、缓冲字节与持续时间都要有限;具体程序适合流式处理就流式处理,不适合就提前拒绝。这些数值是本地策略,四字节标记提供的是执行策略所需的可审计事实。

错误恢复同样要克制。RFC 说分隔有助于发现并可能恢复协议错误,却没有定义“扫描下一枚看起来合理的标记”。正文可以包含任意四字节图样。遇到 framing 违规时关闭连接,往往比把偶然图样认作新消息更诚实。

线上格式长期不变,适用范围却可以被替换

RFC 1831 在 1995 年把这套格式带入标准轨道。RFC 5531 于 2009 年取代它,并明确没有线上协议变化。最高位与低 31 位能够延续,因为它们只补足 TCP 缺少的边界,没有占领其他职责。

但它始终是传输相关适配器。RFC 8166 为 RPC-over-RDMA 定义 transport stream 与 payload stream,并明确替代包括 TCP record marking 在内的其他 RPC framing。即使底层 RDMA 又构建在 TCP 之上,也不能叠用旧标记。动态切换必须发生在两条独立 RPC 消息之间,并与传输层共同完成。

这一后续设计反过来说明原机制的边界:record marking 不是某个远程过程永恒的属性,而是消息模型适配字节流的一种办法。

来源与证据边界

闭集来源为 RFC 793、RFC 1050、RFC 1057、RFC 1831、RFC 4506、RFC 5531、RFC 8166 与 RFC 9293。它们证明格式、修订延续与分层,不统计今天的使用量,不认证某个库,也不证明真实连接中的身份、授权、内容正确性或过程结果。

四个字节的成就很具体:即使两端对每个包、每次写入和每次读取的切分都不同,仍能同意一条 RPC 消息在哪里结束。最后一位之所以可靠,恰恰因为它从未声称消息里的工作也已结束。