## 参考 https://doraemonzzz.com/2021/12/27/2021-12-27-CS144-Lab2%E7%BF%BB%E8%AF%91/ https://doraemonzzz.com/2021/12/27/2021-12-27-CS144-Lab2/ ## TCPReceiver 实现`TCPReceiver`。它将 - (1) 从它的对等方接收段, - (2) 使用你的`StreamReassembler`重新组装ByteStream,并计算 - (3) 确认号(ackno),和 - (4) 窗口大小。 在TCP中,**确认(acknowledgment)**意味着,"接收方需要的下一个字节的索引是什么,这样它才能重新组装更多的ByteStream?"这告诉发送方它需要发送或重新发送哪些字节。 **流量控制**意味着,"接收方感兴趣并愿意接收哪些索引范围"(通常作为其剩余容量的函数)。这会告诉发送方允许发送多少。 ## wrap, unwarp // 为了知道当前真正的数据在整条传输过程中的绝对位置, // 并且要选取最合理的那一个圈数,尽量贴近我们已有的 checkpoint // 就像当我们看到时钟显示"9点",并且知道现在是"下午2点"时,我们更可能认为这 "9点"是指同一天的上午,而不是第二天的上午。 下面给出的解释希望能帮助你用"5 位"和"10 位"这样更小的数字范围来直观地理解 CS144 Lab2 中 `wrap` 和 `unwrap` 这两个函数在做什么。实际代码里用的是 32 位无符号整数进行"环绕",但把位数缩小后更容易想象那个"环绕"的过程。 --- ## 一、什么是"环绕"? ### 1. 场景类比 - **5 位场景** - 5 位无符号整数可以表示的范围是 0~31,共 32 个值。 - 如果我们把一个比较大的"绝对序号"压缩到这 5 位里,就相当于取模 32(因为 2^5 = 32)。 - 当数值超过 31 再往上加时,就会从 0 重新开始,形成"环绕"(wrap around)。比如说: - 31+1≡0 (mod 32)31 + 1 \equiv 0 \ (\text{mod } 32) - 31+2≡1 (mod 32)31 + 2 \equiv 1 \ (\text{mod } 32) - 这就像一个表盘只有 32 个刻度,指针走满 32 格又回到起点。 - **10 位场景** - 10 位无符号整数可以表示的范围是 0~1023,共 1024 个值。 - 同理,如果我们有一个较大的数,把它"压缩"到 10 位,就是对 1024 取模。超出 1023 的数值也会再次环绕,从 0 继续。 ### 2. 在 Lab2 中的角色 在 Lab2 里,`WrappingInt32` 就是一个 **32 位** 的环绕计数器。但为了便于理解,我们先用"小规模"的 5 位或 10 位来做类比演示。 - **`wrap`**:将一个 64 位的"绝对序号"nn 映射到一个环绕的 32 位(这里举例用 5 位/10 位)。 - **`unwrap`**:反过来,根据一个环绕的 32 位数值 nn,推算出它所对应的、**最接近给定检查点(checkpoint)** 的 64 位"绝对序号"。 --- ## 二、`wrap` 函数:把大数"压缩"进环绕计数 代码: ```cpp WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) { return isn + uint32_t(n); } ``` 用 5 位或 10 位去理解: 1. `isn`(Initial Sequence Number)相当于初始偏移量。 2. `n` 是一个很大的 64 位数(绝对序号)。 3. 把 `n` 转成和 `WrappingInt32` 同样大小(在真实场景下是 32 位),然后再加上 `isn` 进行环绕。 ### 1. 用 5 位举例 - 设 `isn = 7` (当然也必须在 0~31 范围)。 - `n = 37`(这时相当于一个 64 位的大数,但为了演示用小数即可)。 - 如果仅仅对 5 位取模,`n % 32 = 37 % 32 = 5`。 - 我们再加上初始偏移量 `isn = 7`:7+5=12 (mod 32) 7 + 5 = 12 \ (\text{mod } 32) - 结果就是 `12`,表示这个环绕后的值最终落在 5 位整数里的 `12` 这个位置。 ### 2. 用 10 位举例 - 设 `isn = 100` (必须在 0~1023)。 - `n = 1500`(还是为了演示)。 - 先做 nmod  1024=1500mod  1024=476n \mod 1024 = 1500 \mod 1024 = 476。 - 再加上 `isn = 100`:100+476=576 (mod 1024) 100 + 476 = 576 \ (\text{mod } 1024) - 结果 `576` 就是 10 位环绕后的数值。 --- ## 三、`unwrap` 函数:找回最接近的"绝对序号" 代码(简化注释过的思路): ```cpp uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) { // 1) 先把 n 和 isn 的差转换到 uint64_t 上 // 2) 再在可能的几个“区间”里,挑一个最接近 checkpoint 的值 uint64_t tmp; if (n - isn < 0) { tmp = uint64_t(n - isn + (1ULL << 32)); } else { tmp = uint64_t(n - isn); } // 如果 tmp 本身已经 >= checkpoint,就返回 tmp // 否则在其高位(每隔 2^32 一个区间)上加相应偏移,找到最接近 checkpoint 的那一个 if (tmp >= checkpoint) return tmp; tmp |= ((checkpoint >> 32) << 32); // 对齐到 checkpoint 相同的高位 while (tmp <= checkpoint) tmp += (1ULL << 32); uint64_t tmp1 = tmp - (1ULL << 32); return (checkpoint - tmp1 < tmp - checkpoint) ? tmp1 : tmp; } ``` 这段逻辑听上去比较拗口,我们用 5 位或 10 位的思路来做比喻,就会更好理解。 ### 1. 用 5 位场景"unwrap"举例 - **背景**:现在我们的"环绕计数器"只有 5 位能表示,也就是只能表示 0~31。 - **参数**: - `n`:已经过环绕的序号(0~31 之间),比如这里是 `n=12`; - `isn`:还是那个初始偏移量,比如我们仍然用 `isn=7`; - `checkpoint`:一个最近用到的"绝对序号"(64 位的大数),比如我们现在假设是 `100`(当然 100 已经大于 31 了,它是"绝对编号")。 具体过程: 1. **先算出最基础的差值** tmp=(n−isn)mod  32 \text{tmp} = (n - isn) \mod 32 - 因为是 5 位的,所以实际操作会像对 32 取模那样。如果 `(n - isn)` 是负数,我们就加上一圈(即 +32)让它变成一个非负值。 - 假设 `n=12`, `isn=7`,则 `n - isn = 5`,这时是正数,就不用加 32。 - 所以 `tmp = 5`。 2. **结合 checkpoint 的高位,拼出一个 64 位候选值** - 在 5 位里,`tmp = 5` 仅仅表示在 0~31 这个小范围里是 5。 - 但在"绝对世界"里,这个 5 可能是: - 5 - 5 + 32 - 5 + 64 - 5 + 96 - … 甚至一直加 32 (因为 2^5 = 32),直到我们能"追上"或"超过" `checkpoint=100`。 - 代码里 `tmp |= ((checkpoint >> 32) << 32);` 这个操作对应的是"先将 `tmp` 的高 32 位对齐到跟 `checkpoint` 相似的区域"。不过在 5 位的类比里,其实对应的是"尝试对齐到跟 checkpoint 相同的某个大区间,然后每次加 32 才能跳到下一个区间"。 3. **找出最接近 checkpoint 的那个值** - 一旦我们找到了一个"候选的 tmp"比 `checkpoint` 大,那么再往回退一圈(减去 32)就能得到另一个候选值 `tmp1`,这样我们就有两个候选值: - `tmp`(稍微超出或刚好等于 checkpoint) - `tmp1`(比 checkpoint 小一点或很接近) - 对比这两个候选值和 checkpoint 的距离,选取差值更小的那个,就是最终返回的"绝对序号"。 拿我们举的 `checkpoint=100` 来说: - 先得到 `tmp = 5`; - 然后一步步加 32: - `5 + 32 = 37`(还小于 100),继续加: - `37 + 32 = 69`(还小于 100),继续加: - `69 + 32 = 101`(终于大于 100) - 此时可以拿到两个候选值:`101` 和 `101 - 32 = 69`。 - `|100 - 69| = 31` - `|101 - 100| = 1` - 明显 101 离 100 更近,所以就返回 101 这个结果。 这样,"unwrap" 就把原先只有 5 位的 `n=12` 转回了在绝对序号里离 100 最近的那个真实值 **101**。 ### 2. 用 10 位场景"unwrap"举例 - **背景**:现在我们的环绕计数器是 10 位,也就是 0~1023。 - **参数**: - `n`:环绕后的序号(0~1023),比如是 `n = 576`; - `isn`:初始偏移量,比如 `100`; - `checkpoint`:可能是一个较大的"绝对序号",比如 `5000`。 1. 先计算 `tmp = (n - isn) mod 1024`。若为负,就加 1024。 2. 将 `tmp` 贴合到与 `checkpoint` 相同或相近的高位区间(因为 10 位取模只是底下 10 位的值,前面高位可以是许多倍的 1024)。 3. 找到"最贴近 checkpoint" 的那个绝对值。 假设最后发现最接近 `checkpoint=5000` 的值是 `4984`,那这个就是 `unwrap` 返回的结果。 --- ## 四、总结 1. **`wrap`**:做的事就像"模运算 + 初始偏移量",把一个大数折叠进小小的环状空间(在实际代码中是 32 位,但我们用 5 位或 10 位来类比更易理解)。 2. **`unwrap`**:则是根据你给定的环绕后的序号,加上 `isn` 这个偏移,再去"对齐"到最靠近 `checkpoint` 的那个绝对值。因为环绕会有无数个"间隔(加或减 2^32)"可能都对应相同的环绕值,所以要挑一个和 `checkpoint` 最近的。 在 5 位/10 位的小例子里,可以用"加或减 2^5 / 2^{10}"的方式去找到距离最近的那个绝对序号。在代码层面,就是利用了移位或循环加减来模拟这种"找正确圈数"的过程。 这样一来,你就能形象地理解: - 为什么要有"环绕"(因为 TCP 的序号是 32 位,数据多了以后会溢出回到 0)。 - 为什么还要把 32 位的环绕数"还原"到 64 位的大数(为了知道当前真正的数据在整条传输过程中的绝对位置,并且要选取最合理的那一个圈数,尽量贴近我们已有的 `checkpoint`)。 希望这个用 5 位、10 位作类比的说明能让你对 CS144 Lab2 中的 `wrap` / `unwrap` 核心逻辑有更直观的理解。祝学习顺利! ---- ## 绝对序号 下面给出一个形象化(直观)的解释,帮助大家理解为什么要在计算绝对序号时,对比 `± (1ul << 32)` 后选择与最近一次 `checkpoint` 最接近的那个。 --- ### 1. "32 bits 的 seqno 会循环"这个特性 #### 场景比喻:12 小时制的时钟 - **传统的 TCP sequence number 是 32 bits,会发生"回绕(wrap around)"。** 就像一个只有 12 小时刻度的时钟,会在指针转满一圈后,从 `11:59` 跳回到 `0:00`,继续循环使用。 - 所以你看到的"时钟显示"**不是唯一**的,它每 12 小时就会从头开始,无法直接区分今天的 10:00 和昨天的 10:00。 #### 问题:如何确定"真正的时间"? - 如果只知道当前时间是 "10:00",并不知道现在是上午还是晚上,单看时针本身并不能确定具体是哪一天或哪一次循环。 - **但我们有一个"基准点(checkpoint)"**:比如我们知道"最近一次我们确认的时间是 9:58 AM",那么我们就可以猜测:现在这 10:00 很可能就是在"9:58 AM 之后的 2 分钟",而**不会**是隔了 12 个小时以后的 10:00 PM。 --- ### 2. "绝对序号"就是不回绕的时间线 - **对比"12 小时制时钟"**:你可能用一个 24 小时制或干脆用一个"Unix 时间戳"来记录不同时刻,它不会循环回绕,也不会重复。 - 在 TCP 协议里,**我们自己维护一个"64 bits 不回绕"的 absolute sequence number**,对应真实的"从数据流开始到现在"的字节位置计数。 - 当对方发来一个 32 bits 的 seqno(就是"12 小时制的时间"),要把它折算到我们自己的 64 bits 时间轴(就是"Unix 时间戳")。 --- ### 3. 为什么要选"与 checkpoint 最接近的那个"? #### 原理 / 直觉 - "seqno" 可能是 "时钟"上的一个读数,例如 "10:00"。 - 我们当前维护的 "checkpoint" 表示我们上一次计算出的 **absolute seqno**(相当于"上次确认好的 Unix 时间戳")。 - 当我们再看到一个新的 "10:00" 时,为了转换到"Unix 时间戳"上,就要判断: - 它是"9:58 AM" 后 2 分钟吗? - 还是其实已经过了 12 个小时,变成了"10:00 PM"? - 甚至可能过了 24 小时、36 小时...(只是极少见/几乎不可能发生那么大延迟) - **正常情况下**,两个分段(segment)相邻到达的序列号不会相差一个 **2^31**(=INT32_MAX)以上的量,除非真的隔了很多小时、甚至几天才到达,这在常规网络里非常罕见。 #### 数学实现 - 对于给定的 `seqno_32`(32 bits seqno),我们先用已知的 `checkpoint`(64 bits absolute seqno)来猜测: - 先把 `seqno_32` 转成一个基础的 `abs_seqno_candidate`,比如直接 `(seqno_32 - ISN) + 0`; - 再分别尝试加/减 `1 << 32`,形成多个可能的候选值: 1. candidate0=base\text{candidate}_0 = \text{base} 2. candidate1=base+232\text{candidate}_1 = \text{base} + 2^{32} 3. candidate−1=base−232\text{candidate}_{-1} = \text{base} - 2^{32} - 选出其中与 `checkpoint` **差的绝对值最小**的那一个,就代表"最有可能的绝对时间(绝对序号)"。 **直观理解**: - 就像"现在是 10:00",可能对应今天上午 10:00、今晚 10:00、明天 10:00... - 但根据你"刚才是 9:58"的记录,肯定选"上午 10:00"(只差 2 分钟)最合理,而不是"隔 12 小时"或更久以后。 --- ### 4. 结论 在实现时: 1. **存储一个"checkpoint"(上一次确认好的 64 bits absolute seqno)**。 2. 每次接收新的 32 bits `seqno` 时,算出几个可能的 64 bits 候选值。 3. **选择距离 `checkpoint` 最接近的候选值**。 4. 这样就可以将一个会回绕的 32 bits 序号,映射到一个不回绕的 64 bits **absolute seqno** 上。 **形象总结**: - 32 bits 的序号就像一个"12 小时制时钟"不停地转圈; - `checkpoint` 则是上一次你确认了"确切的时间"。 - 当你再看到一个钟面读数时,就把它对应到最接近你上次确认时间的那一点上,这样就能**唯一确定**它在时间轴上的位置。 这就是我们为何要在计算出的绝对序号(absolute seqno)上下 **±1×232\pm 1 \times 2^{32}** 做比较,选择离 `checkpoint` 最近值的根本原因。