12.5. 可靠性——序列、ACK 与重传¶
成帧层通过其 CRC 检测数据损坏。可靠性层把“检测到损坏”转化为“应用程序永远不会看到损坏的数据”,方法是在数据包未能完整到达时协商重传。
12.5.1. 序列号¶
每个数据包头都携带一个一字节的序列号,每个传输方向各自独立。发送方在发送前递增该计数器;接收方检查每个收到的数据包的序列号是否为前一个加一(对 256 取模)。
接收方收到的内容除了一个干净的有序数据包之外,还可能是以下三种情况:
序列号符合预期,且 CRC 有效。该数据包被向上递交给下一层。
序列号符合预期,但 CRC 错误。接收方丢弃该数据包,并(如果协商启用了 ACK)发送一个 NAK 请求重传。
序列号比预期高一,且 CRC 有效。接收方知道前一个数据包丢失了;它发送一个引用所缺序列号的 NAK,并暂存这个新数据包。
重复的情况(原始数据包最终送达后,重传的数据包才到达)通过与预期计数器比对来处理:如果序列号落后于预期值,则该数据包是重复的,接收方在发送一个发送方显然第一次没收到的 ACK 后将其丢弃。
12.5.2. ACK 与 NAK¶
数据包头中的两个标志位承载可靠性流量本身:
在发出的数据包上设置
ACK_REQ表示“我想收到一个确认回复”。数据包通常会设置此位;状态心跳和一次性事件则可能不设置。在数据包上设置
ACK表示“此数据包是对包头中序列号的确认”。它本身不携带任何有效负载。设置
NAK表示“此数据包拒绝某个先前的数据包”——通常是因为 CRC 错误或序列号出现缺口。包头会向发送方指明需要重传哪个序列号。
发送方运行一个停止-等待循环:它发送一个需要确认的数据包,然后等待匹配的 ACK(或 NAK)后才发送下一个。这种单包在途模型使发送方的状态保持有界——在最小的摄像头上仅需几百字节——并且与该协议作为两个端点之间控制通道(而非吞吐量优化管道)的角色相匹配。收到 NAK 时,发送方重传同一数据包并设置 RTX 标志,以便接收方知道这是一次重试。
12.5.3. 重传计时¶
如果在重传超时时间内 ACK 和 NAK 都没有到达,发送方会自行重传在途的数据包。该超时默认为 500 ms,并在每次连续重试时翻倍(1 s、2 s、……)。在配置的重试次数——默认为三次——之后,发送方放弃并向应用程序报告一个传输错误。
超时翻倍是标准的指数退避模式。较短的首次超时能快速捕获丢失的数据包;翻倍则意味着一个忙碌了几百毫秒的主机不会触发一阵加重负载的重复数据包风暴。
12.5.4. 配置可靠性¶
在双方同意的情况下,当应用程序能够承受数据丢失时,两端都可以关闭可靠性层的部分功能:
protocol.init(ack=False)禁用逐包 ACK。发送方发出后即不再理会;接收方递交收到的任何内容。适用于陈旧样本可接受的流式传感器数据。protocol.init(seq=False)关闭序列号跟踪,这也意味着关闭 ACK。仅在完全可靠的传输上有用。protocol.init(crc=False)关闭 CRC 验证,但保留成帧的其余部分。仅当传输本身足够稳健、不会发生 CRC 错误时才值得这样做。
默认设置——全部启用——对于任何主机到摄像头的调试会话都是正确的起点。一旦应用程序进入生产环境,权衡取舍就会变得取决于其数据和传输的具体情况。
12.5.5. 状态码¶
当传输错误确实向上传播到应用程序代码时,它会以状态码的形式到达。协议库定义了十种:
SUCCESS——操作完成。FAILED——命令因未指明的原因而失败。INVALID——接收方拒绝了该命令或其某个参数。TIMEOUT——重试计时器超时。BUSY——摄像头忙(通常是通道被锁定)。CHECKSUM——包头或有效负载的 CRC 不匹配。SEQUENCE——序列号失序,超出了该层所能恢复的范围。OVERFLOW——有效负载超过了协商的最大值。FRAGMENT——一条多片段消息到达时缺少了部分片段。UNKNOWN——针对真正意外情况的防御性兜底。
调用 channel_read() 的主机代码会将这些视为 Python 异常;选择使用自定义错误处理的摄像头端应用程序代码则将它们视为后端回调的返回值。大多数摄像头应用根本不需要查看状态码——库会处理重试,只有真正无法恢复的故障(例如传输本身已消失)才会到达应用程序。
有成帧来检测损坏、有可靠性来从中恢复,线路级的工作就完成了。应用程序代码看到的是经过成帧、有序、完整的数据包;其中的字节可以自由表示上层通道希望它们表示的任何含义。