摘要

  • DCCP 的 CCID 2 需要可靠传递接收反馈,却不负责可靠交付应用数据。“确认之确认”让接收端有依据地释放旧反馈状态,而不是让发送端补发旧数据。
  • 双向应用流量通常顺带完成这项工作;一个方向停止发送应用数据后,仍活跃的另一端需要主动安排确认。静默判定同时要求经过足够时间,以及相关接收记录已得到确认。
  • 第二层确认丢失,只会让旧状态多保留、多发送一段时间,不需要再加第三层。CCID 3 的反馈状态通常已有界限,不能把 CCID 2 的办法当成所有 DCCP 流量的统一要求。

丢失的数据与不能丢掉的历史

互联网协议里,“可靠”经常被当作一个完整套餐:有连接、有编号、有确认,似乎就应该把每一份数据最终送到。2006年3月发表的 DCCP 规范,把这几个特征拆开了。它为数据报提供拥塞控制,却不承担丢失应用数据的传输重试。应用可以自行决定是否恢复某些内容;传输层不替它认定旧内容永远值得等待。

RFC 4336 给出了这种需求的时间尺度。音频的一小段如果错过播放时刻,迟到未必还有用;游戏里的旧位置也可能不如新的位置。发送速率仍须回应网络拥塞,但下一次获准发送时放入什么内容,可以由应用选择。

问题随之转向另一种可靠性。发送端不补回旧数据,不等于它可以不知道网络丢过多少包、哪些包带着拥塞标记到达。一个不可靠的数据服务,仍可能依赖可靠的反馈服务。DCCP 的复杂之处不在于偷偷恢复了 TCP 的全部承诺,而在于必须说明:哪些信息需要持续保存,保存到谁知道为止。

确认本身也占一个号码

RFC 4340 让每个 DCCP 包都递增序号,包括没有应用数据的纯确认包。计数单位是包,不是字节。这样一来,接收端能发现的序号缺口也包括反馈包的丢失。

确认号的含义同样不同于 TCP。它指出已经收到的最大序号,并不宣布该序号之前的每个包都到了。较早的缺口由另外的选项描述。若把最大的号码直接当成一个连续交付边界,就会把原本需要报告的丢失从账面上抹去。

这也解释了 NDP Count 的用途。一个缺口里可能全是控制包,并没有应用数据。启用相应特性后,NDP Count 报告当前包之前连续出现了多少个非数据包,帮助接收端判断某段丢失是否包含数据包。它不是丢失字节计数器,也不能总是还原混合缺口中每一类包的数量。规范把 Request 和 Response 归为数据包,即使它们未必真的装有应用负载;分析时还须保留这层分类差别。

编号让反馈成为可指认的对象。只有知道“对方收到了哪一份报告”,接收端才可能决定哪一段报告历史已经不用再讲。

压缩没有取消保存义务

CCID 2 使用 Ack Vector 描述接收历史。每个字节用两位表示状态,六位表示连续长度:已收到、已收到且带拥塞标记、保留状态、尚未收到。长度值为零时代表一个包,值为63时代表64个包。向量从确认号指向的包开始,向更旧的序号展开。

连续相同的状态因而可以写得很短。但把一段历史压缩成几个字节,不等于这段历史可以被删除。接收端仍须知道哪些信息已经传给发送端,哪些信息只是自己发出过。

这里的“已收到”也有明确边界:DCCP 已处理相应选项,包可以被确认,并不要求应用已经取得数据。即使数据在应用接收缓冲区被丢弃,接收状态仍按适当的已收到状态报告;Data Dropped 选项承担另一项说明责任。否则,应用处理能力不足就可能被错误描述成网络没把包送到。

这些边界不使反馈变弱,反而使它能用于特定决策。拥塞控制需要的是相应层面的接收和标记情况,而不是一张假装覆盖所有后续行为的回执。

反向业务停下来以后

设想两端都在发送应用数据。来自对方的确认,可以自然地被本端后续的数据与确认一起回应。维护反馈历史的工作藏在双向交互之中,不一定需要读者额外看到一串控制包。

现在,B 停止发送应用数据,却仍接收 A 的数据并发回 Ack Vector。如果 A 只继续发送不带确认号的 DCCP-Data,B 就不知道自己的哪一份报告被 A 收到了。A 可能已经依据反馈调整发送,但 B 不能从 A 继续发数据这一事实,推出某份反馈必然到达。

所以,RFC 4341 要求活跃发送端偶尔确认对端的确认。例如,A 可以把某次发送改成 DCCP-DataAck,明确指出收到过 B 的哪个包。规范建议至少每个拥塞窗口这样做一次,并不是要求每份反馈都立刻触发一个独立应答。

当两端应用都不再发送数据时,情况又不同。发送端可以等很久再发确认。系统不必为了证明自己正在安静,就持续制造新的交互。流量方向的变化之所以重要,是因为它改变了已有业务包能否顺带完成反馈清理,而不是把连接简单分成“有流量”和“没流量”两种状态。

一个没有无限回音的回路

若没有确认之确认,CCID 2 接收端可能在每次反馈中重复一路积累下来的历史。RFC 4340 第11.1节把这个问题说得很直接:极端情况下,重述的范围可以追溯到连接开始。

第二层确认给出一个退出条件。发送端已经收到某份反馈,接收端便可以释放相应旧状态。它结束的是一项信息保存义务,不是追回某个丢失的数据报。

那第二层确认也丢了怎么办?接收端暂时不知道自己可以清理,就继续保存和重复旧信息。代价是更多保留时间和反馈负担,而不是必须再建立一个可靠交付第二层确认的机制。于是也不需要“确认之确认之确认”。

这并非宣称丢包没有成本。持续丢失仍会影响状态、带宽和拥塞反应。设计的关键是让一种失败落入可延长的保留义务,而不是递归地产生必须可靠交付的新义务。回路能够结束,是因为两层信息承担的后果不同。

静默不是计时器单独裁决的

CCID 2 对静默的判断还有一个容易遗漏的条件。等待时间 T 取0.2秒与两倍往返时间的较大值。但仅仅经过 T、没有收到新的数据,还不够。

接收端还要确认:发送端已经确认了覆盖所有已收数据的接收向量。时间条件与反馈条件共同满足,才构成规范描述的静默判断。若往返时间未知,采用的默认 RTT 为0.2秒,两倍默认值就是0.4秒,不能把它随手写成固定200毫秒。

不同方向还可能采用不同的拥塞控制配置。哪个方向被判为静默,由该方向的 CCID 定义;静默之后另一方向怎样确认反馈,则由另一方向的 CCID 决定。一个连接里的两个方向,并不是由一个模糊的“连接空闲”开关统一管理。

太早清理,也可能擦掉刚得到的消息

规范的附录 A 给出一个非强制的实现示例。接收端保存发出的确认包序号,以及那份包代表了哪些接收历史。得到对应的确认后,它据此移动清理边界。

但一个曾经缺失的包,可能在旧报告发出后才到达。旧报告说“尚未收到”,新的本地状态已经变成“收到”。如果这时对方确认了旧报告,接收端就把相关记录全部删掉,刚刚得到、尚未传出的消息也会被擦掉。

附录展示了如何收紧可清理的边界,使发送端看见新的到达事实之后才继续释放。实现可以选择保留一些旧信息,应对迟到和乱序;这不意味着所有实现必须采用附录同一套缓冲区。向量合并规则也不允许一份乱序到达的旧缺失报告轻易覆盖已知的接收事实。

保存义务的终点因此不是“出现过一个更大的数字”,而是“相关信息已被哪一份报告带走,而那份报告又是否被确认”。

不是每一种反馈都需要同一套收尾

RFC 4342 定义的 CCID 3 使用 TFRC,追求较平滑的速率变化,并接受对可用带宽变化反应较慢的取舍。RFC 4340 指出,这种配置的确认状态通常已有界限,因此不需要照搬 CCID 2 的确认之确认要求。这不等于没有状态或没有反馈,只是需要结束的保存义务不同。

历史文本本身也需要认真收尾。RFC 4340 的勘误 修正了一个误把 Ack Ratio 零值写成无效值的例子,还纠正了附录里的变量数。RFC 4342 的已核实勘误 则把接收速率的计算区间改正为最近一个 RTT,而非自上次反馈以来的全部时间。

其中一项修正尤其能说明反馈为何不能只数包:若所涉及区间没有数据包,发送端可以按规定忽略第二份反馈的 Receive Rate,但同时不得据此重置无反馈计时器。收到控制包,并不自动等于收到了足以续期的有效信息。

2012年的 RFC 6773 增加 UDP 封装,处理部分中间设备支持 UDP、却不支持原生 DCCP 的兼容问题。2018年的 RFC 8311 又移除了几个 DCCP 拥塞控制配置中的 ECN Nonce 讨论。这些变化提醒读者,2006年的文本不是可以原封不动执行的当代操作清单。

IANA 的登记表 保留了各项编号及其规范依据,但没有告诉我们当下有多少网络在用、某个应用是否支持,或一种配置能提高多少性能。DCCP 留下的这个设计切面也不需要靠夸大的普及率才成立:可靠反馈和可靠数据可以分开,而一个协议想要安全地遗忘,必须先准确说明它已经让谁知道了什么。