CS144(2024 Winter)Lab Checkpoint 2: the TCP receiver

本文详细阐述了如何在Lab2实验中实现TCP接收器,涉及32位字节序号与64位绝对字节序号的转换,以及如何处理SYN、FIN和RST标志。核心算法包括流重组和字节流缓冲区,重点在于TCP报文的回传确认机制,包括计算ackno和windowsize。实验还鼓励自定义测试用例和错误检测。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0.Overview

check2.pdf

Lab2 部分要求我们实现一个 TCP receiver。

实现的 TCP 接收器需要实现以下两个功能:

  1. 使用 send() 方法将期待的下一个字节序号回传给发送方;
  2. 告知发送方接收端的缓冲容量,也称为接收窗口大小。

TCPReceiver_functions

下一字节序号和接收窗口大小共同构成了一个左闭右开的接收区间 [first_unassembled, first_unassembled + window_size),只有这个区间内的字节才会被接收端接收。

核心算法部分(流重组 Reassembler 和字节流缓冲区 ByteStream)已经在先前的两个实验中实现了,在这次实验的部分需要补充 TCP 报文传输的回传确认。

1.Getting Started

Lab2 一如既往的要求代码复用,因此就需要将 checkpoint2-startercode 分支的代码合并到已经写好的部分中。这部分就略过了。

2.Checkpoint 2: The TCP Receiver

2.1 Translating between 64-bit indexes and 32-bit seqnos

在先前的实验中,我们的代码实现是建立在字节流序号是 64 位无符号数(下称 uint64)的基础上的;但实际为了减少报文长度,TCP 报文的字节流序号的类型是 32 位无符号数(下称 uint32)。

我们都知道如果为每个字节做一个编号,那么以 uint64 为基础的字节流长度最大可以达到 2 64 2^{64} 264 Bytes = 16 EB,即使是以 100 Gbits/s 的速率传输也需要花费 50 年左右才能传输完。但是 uint32 的最大长度只有 4GB,很明显,在实际使用时报文内的字节序号会不可避免地产生“回绕”(即这个值始终在对 2 32 2^{32} 232 取模)。

uint32_seqno

因此为了实现 uint32 的字节序号(sequence numbers)到 uint64 的绝对字节序号(absolute sequence numbers)之间的转换,我们首先需要对字节序号做一个封装。

此外,为了避免被攻击、提高 TCP 的健壮性,也是为了判断当前传输的字节是否属于上一个连接,TCP 设定当前传输的字节的序号的起始计数值(ISN)并不是 0,而是一个位于 [ 0 , 2 32 − 1 ] \left[ 0, 2^{32} - 1 \right] [0,2321] 范围内的一个随机值,因此这让 seqno 到 absolute seqno 的映射更加复杂。

文档这里还提到了,TCP 在建立确认时发送的 SYN 就是 ISN + 1 的值,发送该序号也意味着传输开始。

TCP_details

文档还给出了不同字节序号之间的对应关系。可以看出 absolute seqno 和 stream index 之间只相差了 1 位的偏移量,因此我们并不关心这里的转换,而是将注意力放在上方的 seqno 和 absolute seqno 之间。

stream index 是表示数据字节的序号,不会记录 SYN 与 FIN 这两个用于控制 TCP 传输的字节。

index_transform

代码中的 Wrap32 就是负责进行两个不同字节序列之间互相转化的类。在静态方法 wrap() 中,文档要求实现从 absolute seqno 到 seqno 的转换,也就是根据给定的起始数值点 ISN,将当前的绝对字节序号转换为 TCP 报文中的 32 位字节序号。

wrap() 方法的实现相当简单。由于是从 64 位数到 32 位数的转换,并且参照上面给出的字节映射关系就知道:只需要将 n 加在 zero_point 上即可,上文提到的“回绕”会因为 uint32 的位数截断自动进行(也就是说将一个 64 位数转换为 32 位数的过程是在对 uint64 取 2 32 2^{32} 232 的模)。

Wrap32_introduction

unwrap() 方法的实现比较复杂。文档要求在这个函数中,利用给定的 checkpoint 值和起始点 zero_point,找到最接近 checkpoint 的 absolute seqno。

什么是 checkpoint

文档中指出,有大量 64 位无符号整数(下称 uint64)在   m o d   2 32 \bmod 2^{32} mod232 的情况下的值是相等的,这种现象称作“这些数对 2 32 2^{32} 232 同余”。

如果要确定一个 uint32 的余数值对应哪一个 uint64 整数,我们就需要有一个 64 位无符号整数 checkpoint 帮我们定位我们所期望的最接近某个无符号 32 位整数的 uint64 的整数的数值。

实现时,我们所跟踪的绝对字节序号(absolute seqno)本身就是一个最合适的 checkpoint

从数学角度上看,这一操作过程实质上是在求同余方程 f ( x ) ≡ k ( m o d 2 32 ) f(x) \equiv k \pmod{2^{32}} f(x)k(mod232) 的解集中最靠近 checkpoint 的数,其中 k ∈ Z ∩ [ 0 , 2 32 ) , x ∈ Z ∩ [ 0 , 2 64 ) k \in \mathbb{Z} \cap \left[ 0, 2^{32} \right),x \in \mathbb{Z} \cap \left[ 0, 2^{64} \right) kZ[0,232)xZ[0,264).

既然解空间是有限范围内的同余方程解集,其实打表嗯搜也不是不行。 ← \leftarrow 实际上是真的不行(

我们知道,seqno 是一个模 2 32 2^{32} 232 数,并且这个数的起始计数点是 ISN 而非 0。因此要找到给定最接近 checkpoint 的数的最好办法就是计算 checkpointcheckpoint 是一个 absolute seqno)在变为 seqno 的情况下到当前 seqno 的偏移量,然后在 checkpoint 中加上这个偏移量;也就是: c h e c k p o i n t + ( c k p t _ m o d − r a w _ v a l u e _ ) checkpoint + \left( ckpt\_mod - raw\_value\_ \right) checkpoint+(ckpt_modraw_value_)

当然也可以使用另一种思路:计算要加上多少个 2 32 2^{32} 232 才最靠近 checkpoint,这个方法需要做到以下几点:

  1. 因为 ISN 不一定从零开始,所以还要额外计算一个偏移量;
  2. 无法利用 checkpoint 的位置信息,因此需要试探三个点: r a w _ v a l u e _ + o f f s e t − 2 32 raw\_value\_ + offset - 2^{32} raw_value_+offset232 r a w _ v a l u e _ + o f f s e t raw\_value\_ + offset raw_value_+offset r a w _ v a l u e _ + o f f s e t + 2 32 raw\_value\_ + offset + 2^{32} raw_value_+offset+232
  3. 需要使用乘法。

当然第 2 点可以使用 ckpt_mod 的信息,但是都用了 ckpt_mod 为什么不直接计算 checkpoint 的偏移量?

[ 0 , 2 32 ] \left[ 0, 2^{32} \right] [0,232] 范围内,一个数可以加上任意多的 2 32 2^{32} 232 而仍停留在原位,这会导致对应的 seqno 从 uint32 提升到 uint64、并且要找出其距离某个数的最小值时出现两种可能的取值:要么在下图区间 0 点左侧(减去一个 2 32 2^{32} 232),要么在区间 0 点的右侧。并且当最小值在左侧时,我们只需要将右侧的值减去一个 2 32 2^{32} 232 就可以得到结果,因此实际上我们只需要计算右侧的值(并且右侧的值更好求,原因下文会提及)。

这里本质上是在试探 a b s o l u t e _ s e q n o − 2 32 absolute\_seqno - 2^{32} absolute_seqno232 a b s o l u t e _ s e q n o absolute\_seqno absolute_seqno 哪个更靠近 checkpoint

并且因为我们在围绕 ckpt_mod 确定最终值,所以我们只用试探两个点: c h e c k p o i n t + ( c k p t _ m o d − r a w _ v a l u e _ ) − 2 32 checkpoint + \left( ckpt\_mod - raw\_value\_ \right) - 2^{32} checkpoint+(ckpt_modraw_value_)232 c h e c k p o i n t + ( c k p t _ m o d − r a w _ v a l u e _ ) checkpoint + \left( ckpt\_mod - raw\_value\_ \right) checkpoint+(ckpt_modraw_value_)

下图给出的就是“最近的数在区间 0 点左侧的情况”。

示意图

从这张图中我们还可以知道一点:当图中的 ckpt_mod 小于 raw_value_ 时,无符号减法: c k p t _ m o d − r a w _ v a l u e _ ckpt\_mod - raw\_value\_ ckpt_modraw_value_ 的值会变得非常大;但是这只是将目标结果“搬到”了区间段 [ 2 32 , 2 33 ) \left[ 2^{32}, 2^{33} \right) [232,233) 中,结果值在 [ 0 , 2 32 ) \left[ 0, 2^{32} \right) [0,232) 数值范围内的“绝对位置”是始终保持不变的,我们只需要减去一个 2 32 2^{32} 232 就可以在 uint64 的数值范围下得到最靠近 checkpoint 的值,即我们所期望的正确结果。

计算右侧的 seqno 对应的值时,我们可以假定 raw_value_ 总比 ckpt_mod 大,进而计算: c h e c k p o i n t + ( r a w _ v a l u e _ − c k p t _ m o d ) checkpoint + \left( raw\_value\_ - ckpt\_mod \right) checkpoint+(raw_value_ckpt_mod);这里的 c k p t _ m o d ckpt\_mod ckpt_mod 是 absolute seqno 基于 ISN 时的 32 位值,这个值可以由上文的 Wrap32::wrap() 直接求得。

那么我们该如何判断最小值是出现在左侧还是右侧?观察上图可以知道:当 raw_value_ckpt_mod 的差值小于一半区间长度( 2 31 2^{31} 231)时,意味着最小值在区间 0 点的右侧;否则就需要多减去一个 2 31 2^{31} 231 使得计算结果最小。

但是要注意一个边界条件:当 c h e c k p o i n t < 2 32 checkpoint < 2^{32} checkpoint<232 时,区间 0 点左侧是不存在的,这个时候的最小值只能取在区间 0 点右侧。因此我们要把这个条件筛去:在 uint64 的数值计算下, c h e c k p o i n t + ( r a w _ v a l u e _ − c k p t _ m o d ) < 2 32 checkpoint + \left( raw\_value\_ - ckpt\_mod \right) < 2^{32} checkpoint+(raw_value_ckpt_mod)<232。(所以说右侧的值会更好求)

根据以上分析结果,unwrap() 的实现方式就已经呼之欲出了。并且文档还提及:合理的 Wrap32::wrap() 实现应该只有一行,Wrap32::unwrap() 则应该在 10 行以下。

如果你对数学比较敏锐,可以把上面的问题用更加数学化的方式表达出来:

已知整数 a a a k k k,其中 a ∈ Z ∩ [ 0 , 2 64 ) 且 k ∈ Z ∩ [ 0 , 2 32 ) a \in \mathbb{Z} \cap \left[0, 2^{64} \right) 且 k \in \mathbb{Z} \cap \left[ 0, 2^{32} \right) aZ[0,264)kZ[0,232),现要求同余方程 f ( x ) ≡ k ( m o d 2 32 ) f(x) \equiv k \pmod{2^{32}} f(x)k(mod232) 的解集中最靠近 a a a 的整数,其中 , x x x 为未知数且 x ∈ Z ∩ [ 0 , 2 64 ) x \in \mathbb{Z} \cap \left[ 0, 2^{64} \right) xZ[0,264).

例如,当 k = 1 , a = 0 k = 1,a = 0 k=1a=0 时,解 x = 1 x = 1 x=1;当 k = 1 , a = 4294967295 k = 1,a = 4294967295 k=1a=4294967295 时,解 x = 4294967297 x = 4294967297 x=4294967297

wrapping_integers

2.2 Implementing the TCP receiver

在这部分,我们要根据实现好的 Wrap32 完成 TCP 的接收器。最终的实现如果是合理的,行数应该不会超过 15 行。

我们要做的事一共两个:

  1. 根据接收到的信息将字节序列提交给 Reassembler 处理、并由它推入流中;
  2. 向发送方回传期盼的下一个字节序号 ackno、以及窗口大小 window size。

receiver_requires

接下来文档介绍了接收和回传时承载报文信息的结构体 TCPSenderMessageTCPReceiverMessage:当接收器接受信息时,接受的是 TCPSenderMessage,而回传信息时需要使用 TCPReceiverMessage

在前者中,三个符号位 SYNFINRST 共同控制 TCP 连接的行为,具体描述如下:

  1. SYN 是第一次为 true,seqno 中的值是 ISN,否则 seqno 中的值就是 payload 的第一个字节的序号(SYN 为 false 是无效数据);
  2. FIN 为 true,表示这是最后一段数据;
  3. RST 为 true,表示链路出了问题,需要中断传输。

SenderMessage

TCPReceiverMessage 要比前者简单很多,具体描述如下:

  1. ackno 使用了 std::optional,因为当接收方没有接收到 SYN 为 true 的报文时不应该返回任何应答序号;
  2. windows_size 指定当前窗口大小,可以看出这里的类型被限定为 uint16_t,所以如果窗口大小超过了 uint16 的最大值(65535)就会溢出,并且如果发生溢出就取最大值;
  3. RST 用于告知发送方我的流缓冲是否出现问题,若有则要中断传输。

ReceiverMessage

然后是 receiver 的 receive() 方法,这个方法在每次收到新信息时都会被调用,并且我们需要使其满足两种功能:

  1. SYN 第一次为 true 时设置 ISN 的值;
  2. FIN 为 true 时告知 Reassembler 这是最后一个分组(别忘了 Lab1 的 is_last_substring)。

receiver_function

查看 starter-code 可以发现,实验只要求我们补充 receive()send() 方法,后者就是用于回传信息的。这里需要注意的是:我们不需要自己在 receive() 中调用 send(),这种事是交给上层应用的。

因为每条连接的初始值都是随机值,所以我们必然要维护一个 ISN 值;再考虑到当连接建立请求 SYN 尚未发来时 ISN 的值是不能确认的,所以我们可以模仿 TCPReceiverMessage,将我们维护的 ISN 设置为 std::optional<Wrap32> ISN。这样做有个好处:可以通过判断 ISN 是否有值(使用 has_value())判断是否已经收到了连接确认请求,而不必额外维护一个 bool 标志。

另外我们都知道为了计算绝对序列号 absolute seqno,我们还需要维护一个正在期待的下一个字节的序号作为 checkpoint,而且这个值也需要回传给发送方。不过这里我们也不需要额外设置一个 uint64_t 的私有成员,我们可以使用 Writer::bytes_pushed() 方法检查当前有多少个字节已经进入流中,并且这个值恰好就是一个表示正在期盼的下一个字节的 absolute seqno 序号。

最后还有三点要注意的:

  1. SYN 字节本身是需要被计入 absolute seqno 中的,所以上面提到的 absolute seqno 还需要加上 ISN.has_value() 的值(这里利用了 bool 隐式转换为数值类型加减的性质);
  2. FIN 关闭请求要在写端方法 Writer::is_closed() 为 true 时才能响应,否则会导致错误,并且在 TCP 中 FIN 字节也要计入 absolute seqno;
  3. 要拦截一些错误的报文段(例如刚接收了第一个 SYN 报文段,下一个报文段的 seqno 还是等于 ISN),这个错误检测似乎是今年的新内容。

speed_test

tcp_receiver

3.Extra Credit

在最后呢,课程还鼓励大家查看 test/ 中的测试用例(包括但不限于 Lab2),并补充一些有可能出错但是 CS144 提供的测试代码中没有的部分,然后在课程结束前向仓库发起一个 pull request。

可惜我没有这个学分拿(笑)。

总体上来讲,从 Lab0 到现在的进程都是一种“自顶向下”的方向,并且由于核心算法已经在前两个实验中编写完了,所以后续的实验一般都会更加底层、并且代码量不太大(我猜的)。

### DeepLab v3+ 语义分割模型概述 DeepLab v3+ 是一种用于图像语义分割的强大工具,其核心在于通过改进的 Atrous 空间金字塔池化(ASPP)模块以及编码解码器结构来提升特征提取能力[^1]。此架构不仅能够处理多尺度信息,还能有效捕捉上下文依赖关系。 #### 架构特点 - **Atrous Convolution**: 扩张卷积允许在不增加参数量的情况下扩大感受野。 - **ASPP Module**: 利用不同采样率下的扩张卷积并行操作,从而更好地适应多种尺寸的目标对象。 - **Decoder Structure**: 解码部分帮助恢复高分辨率细节,在最终输出前融合低级特征图与高级语义表示。 ```python import torch.nn as nn from torchvision.models.segmentation import deeplabv3_resnet101 model = deeplabv3_resnet101(pretrained=True, progress=True) ``` 上述代码展示了如何加载预训练好的 ResNet-101 版本的 DeepLab v3+ 模型实例[^3]。 #### 实战案例:基于 Jittor 的实现 对于希望动手实践的朋友来说,可以尝试使用开源框架如 Jittor 来构建自己的 DeepLabV3+ 应用程序[^5]。下面是一个简单的例子: ```python # 导入必要的库 import jittor as jt from models.deeplab_v3_plus import DeepLabV3Plus # 初始化模型配置 num_classes = 21 # 对于PASCAL VOC数据集而言 input_size = (513, 513) deeplab_model = DeepLabV3Plus(num_classes=num_classes, backbone='resnet', output_stride=16) # 加载权重文件 checkpoint_path = 'path_to_your_checkpoint' state_dict = jt.load(checkpoint_path)['model'] deeplab_model.load_state_dict(state_dict) # 设置评估模式 deeplab_model.eval() ``` 这段脚本说明了怎样定义一个带有ResNet骨干网的DeepLabV3+实例,并从中读取已保存的状态字典以继续之前的工作或进行推理预测。
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值