protocol: 更正上一个提交的归因——坏帧不在这里

上一个提交(1c65eaa)说"34 张 JPEG 不完整是重复分片凑数导致的",并说修完从
34 降到 1。**这个结论是错的**,是我归因到了错误的地方。

实测复查:一到两个订阅者、20 秒 2000+ 个分片,重复投递数为 0(固定端口和
临时端口两种订阅方式都试过)。这条链路上根本不产生重复。

34 张坏帧的真实来源是探测脚本自己:scripts/udp_probe.py 的重组状态只按
sender_id 存,不按 frame_id 存。于是某帧丢了分片之后,下一帧的分片会接着往
同一个计数器里凑,凑够了就交出去一张混着两帧的 JPEG。

FrameAssembler 一直按 frame_id 隔离(换个 frame_id 就重置),所以它没这个
问题——脚本原先跟它不一致,而我又把脚本的输出当成了 crate 的行为。

本次改动:

  protocol/src/lib.rs
    位图去重保留,理由改为正确性而非正在发生的故障,并在注释里写明实测重复
    为 0、以及真正的坏帧来源。删掉一个死变量 `last`。行为不变。

  protocol/README.md
    "重复分片必须去重" 一节改写:说清防护的定位是"哪天真的重复了"。
    新增"按 frame_id 隔离,换帧即放弃"一节,讲清不隔离的具体后果。
    记下探到坏帧时先怀疑测量工具。

值得记的不是位图本身,是那个错误归因:34 张坏帧配上"帧率正常、无报错线索"
这个症状,太像一个真的丢包/重复 bug 了,于是一路写到了注释、文档和提交说明
里。以后探到链路异常,先确认测量工具和被测对象说的是同一件事。
This commit is contained in:
Horldsence
2026-10-05 19:13:24 +08:00
parent 1c65eaabc6
commit 7be8d7d2ba
2 changed files with 174 additions and 55 deletions
+13 -9
View File
@@ -617,12 +617,17 @@ impl FrameAssembler {
let index = frag.header.frag_index as usize;
// 重复分片直接跳过,且**不**增加 seen。
// 重复分片直接跳过,且**不**增加计数。
//
// 这里曾经用 `seen >= expected` 判断收齐,那是错的:UDP 会重复 delivered
// 重复包,一个丢失的分片可以被另一个包的副本凑数,于是 seen 达标而缓冲区
// 里留着空洞,交出去一张 JPEG 尾部是 0 的坏图。这个 bug 很隐蔽——它不会让
// 帧率下降,只会让偶尔几张图解不出来,而接收端没有任何线索知道原因。
// 用 `seen` 一个计数判断收齐是不够健壮的:一个丢失的分片若能被某个分片的
// 副本凑数,计数就达标了,而缓冲区里留着空洞,交出去一张尾部是 0 的 JPEG。
// 位图让"收到过"这件事是显式的,不用靠计数的算术去推断。
//
// 要说明的是:在本项目实测的链路上(2.4GHz AP,一到两个订阅者,20 秒 2000+
// 个分片)**一次重复投递都没出现过**,所以这层防护是为了正确性而不是为了
// 修一个正在发生的故障。之前观察到的 34 张坏 JPEG 另有原因:探测脚本自己
// 的重组状态只按 sender_id 存、不按 frame_id 存,于是丢片之后下一帧的分片
// 会接着凑上一帧的计数。那个脚本已修,见 scripts/udp_probe.py。
if self.have[index] {
return Err(DropReason::Superseded);
}
@@ -638,10 +643,9 @@ impl FrameAssembler {
self.seen += 1;
// 计数达标还不够:最后一个分片必须真的到过,否则中间可能缺一块。
// 显式检查尾块,比依赖 seen 的算术更直接,也更容易看出意图。
let last = self.expected - 1;
let tail_expected = frag.header.total_len as usize;
let tail_index = tail_expected.saturating_sub(1) / MAX_FRAGMENT_PAYLOAD;
// 显式检查尾块,比依赖计数的算术更直接,也更容易看出意图。
let tail_index =
(frag.header.total_len as usize).saturating_sub(1) / MAX_FRAGMENT_PAYLOAD;
if self.seen >= self.expected && self.have[tail_index] {
let total = frag.header.total_len as usize;