第一个事实:
UDP 有一个 16 位校验和字段,从数据包头的第 40 位开始。这有(至少)两个弱点:
- 校验和不是强制性的,所有设置为 0 的位都定义为“无校验和”
- 它是严格意义上的 16 位校验和,因此容易受到未检测到的损坏。
这意味着,UDP 的内置校验和可能足够可靠,也可能不够可靠,具体取决于您的环境。
第二个事实:
比传输过程中的数据损坏更现实的威胁是丢包重新排序:USP 不保证
- 所有到(最终)的数据包都到达
- 数据包以与发送相同的顺序到达
事实上,UDP 根本没有内置机制来处理大于单个数据包的有效负载,这是因为它不是为此而构建的。
结论:
在没有额外措施的情况下在接收到的数据包后附加数据包必然会产生与发送流不同的接收流,但在最有利的环境中除外。这使得它不是直接文件传输的最佳协议。
如果您确实想要或必须使用 UDP 来传输文件,您需要构建这些部分,它们是 TCP 不可或缺的部分,而不是 UDP 到应用程序中的一部分。不过有一种说法,这很可能会导致 TCP 的重新实现效果不佳。
成功的实现包括许多对等文件共享协议,其中保护连接中断和数据包丢失或重新排序无论如何都需要成为应用程序功能的一部分,以击败或减轻过滤器。
实施建议:
对我们有用的是分块窗口实现:有效负载被分成固定且方便长度的块,(我们使用 1023 字节)在发送端和接收端保存 N 个这样的块的状态数组。
在发送方:
- 发起一个 UDP 消息,其中包含这样的块、其在流中的序列号(不止一次)以及校验和或哈希。
- 状态数组将此块标记为“已发送/待处理”并带有时间戳
- 发送停止,如果完整的状态数组(发送窗口)被消耗
在接收方:
- 接收到的数据包根据它们的校验和进行检查,
- 如果序列号的所有副本都同意,则损坏的数据包被否定确认,否则丢弃
- OK 数据包在状态数组中标记为“已接收/待处理”并带有时间戳
- 如果已接收到足够的块来填充 ack 数据包,或者最旧的“接收/待处理”的时间戳变得太旧(从几毫秒到大约 100 毫秒),则确认通过发送一个 ack 数据包来工作。
- Ack 数据包需要校验和,但不需要排序。
- 已发送 ack 的块在状态数组中被标记为“ack/pending”并带有时间戳
在发送方:
- 接收并检查确认数据包,丢弃损坏的数据包
- 收到 ack 的块在状态数组中被标记为“ack/done”
- 如果状态数组中的第一个块被标记为“确认/完成”,则状态数组向上滑动,直到它的第一个块再次没有完成。
- 这可能会释放一个或多个要发送的未发送块。
- 对于处于“已发送/待处理”状态的块,时间戳超时会触发该块的新发送,因为原始块可能已丢失。
在接收方:
- 接收块 i+N(N 是窗口宽度)将块 i 标记为 ack/done,向上滑动接收窗口。如果不是所有滑出接收窗口的块都被标记为“ack/pending”,这构成了一个不可恢复的错误。
- 对于状态为“ack/pending”的块,时间戳超时会触发该块的新 ack,因为原始 ack 消息可能已丢失。
显然,发送端需要一个特殊的消息类型,如果发送窗口滑出文件的末尾,来表示接收到一个 ack 而不发送块 N+i,我们通过简单地发送 N 个多于的块来实现它存在,但没有有效载荷。