## 参考
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` 最近值的根本原因。