感觉只看文档做可能还是会有些勉强,不知道是不是需要去看课程录像(虽然文档已经写的很详细了)
实验准备
git fetch && git merge origin/lab2-startcode
make -j4
遇到的问题:
- 合并后,运行
make -j4
出错:
这里需要我们安装缺少的环境 Doxygen
(因为我不是用人家准备好的环境,所以会出现这个错误)
解决方法:
安装后即可运行 make -j4
更新
用了 Ubuntu 22.04
然后装上 clang-14
之后,对于此错误似乎只需要装 doxygen
即可(当然还需要装上 clang-format
,否则不会高亮)
实验过程
一些对 TCP Receiver
的介绍
TCP Receiver
需要做的事情,负责告诉发送端两样东西
first unassembled
的索引,这被称之为ackno
(acknowledgment number
),因为这是接收端需要从发送端拿到的第一个字符windows_size
也就是capacity - first_unassembled
的长度
这两样东西负责决定了接收端的窗口,也就是发送端允许发送字符索引的范围
这里补充一些 TCP
的三次握手:
TCP
三次握手是指建立一个TCP
连接时,需要客户端和服务器总共发送 个包。三次握手的目的是连接服务器指定端口,建立 TCP
连接,并同步连接双方的序列号和确认号并交换 TCP
窗口大小信息。第一次握手:客户端发送 syn
包到服务器,并进入SYN_SEND
状态,等待服务器确认;第二次握手:服务器收到 syn
包,必须确认客户端的 SYN
,同时自己也发送一个 SYN
包,即SYN+ACK
包,此时服务器进入SYN_RECV
状态;第三次握手:客户端收到服务器的SYN+ACK
包,向服务器发送确认包ACK
,此包发送完毕,客户端和服务器进入ESTABLISHED
状态,完成三次握手。
上述过程可能有些复杂,随着阅读后续的文档会逐渐理解这个过程。下面先介绍我们在 StreamReassembler
中写到的 index
seqno
与 index
TCP
的报文头如下所示:
限于 TCP
报文头的长度限制,我们拿到的 seqno
(另一种意义上的 index
) 和我们需要的 index
是不一样的,在传输过程中的seq
是 32 位的,但我们本地的seq
是 64 位系统下的,所以我们需要将对其做一个转化。32 位最大值为 , 超过这个数字就从 0 开始。
而这个 seqno
的含义为:当前 payload
中的第一个字符在整个报文中的位置模 (当然可能不止 payload
,这在后面会提到),而这个 seqno
的选择并不是和 index
一样从 开始的。
- 第一次握手时,
seqno
以一个 位随机值初始化。目的是为了防止被猜到,以及网络中较早的数据报造成干扰。一端连接中第一个seqno
就以一个 位的数字初始化,叫做Initial Sequence Number(ISN)
,之后每个 - 连接开始和结束每个占用一个序列号:除了确保收到所有字节的数据,TCP 确保流的开始和结束同样是是可靠的。因此,在 TCP 中,SYN(流开始)和 FIN(流终端)控制标志被分配
seqno
,都占据一个字节(SYN 标志占用的序列号就是 ISN)。流中的每个数据字节还占用一个字节。注意,标志位虽然占据一个字节但是并不算在需要读出的数据里。
为了转化 seqno
和 stream index
,我们提出了 absolute seqno
。absolute seqno
始终以零开始并且为 64 位,stream index
为StreamReassEmbler
流中的每个字节的索引,从零开始,64 位,具体见下图:
要求我们完成 seqno
与 absolute seqno
之间的转化。
-
absolute seqno
2seqno
是显然的,我们只需要将isn
加上n
模 即可,而由于uint32_t
对 自然溢出,因此: -
seqno
2absolute seqno
稍微会有一些麻烦,由于这种转换不是唯一的。seqno
每次增加 值都不变,但是absolute seqno
变化。为了确定唯一的结果,我们需要checkpoint
,将可能的结果中距离checkpoint
最近的作为最终结果。checkpoint
表示最近一次转换求得的absolute seqno
,而本次转换出的absolute seqno
应该选择与上次值最为接近的那一个。原理是虽然segment
不一定按序到达,但几乎不可能出现相邻到达的两个absolute seqno
差值超过INT32_MAX
= 的情况因此,这里我们先将
checkpoint
转化为seqno
,然后计算出当前与上一次seqno
的偏移量,如果当前checkpoint
加上offset
大于等于 ,那么显然我们直接返回即可,否则,checkpoint
是小于INT32_MAX
而我们当前的seqno
已经超过了INT32_MAX
,因此需要加上 再返回。
完成后,运行 make -j4 && ctest -R wrap
,结果如下:
TCP Receiver
明确接收端的工作流程为:
- 检查是否已经受到了
syn
,如果没有那么直接丢弃包 - 如果收到了,记录下
isn
, 随后通过seqno
和checkpoint
计算absolute seqno
- 通过
absolute seqno
计算stream index
,注意syn
占一个字节(如果存在的话) - 扔进
_reassembler
,然后计算下一个ackno
和window size
,包装成TCP Segment
然后发送给发送端
当然在这里,我们并不需要发送什么东西给发送端,做好前面的部分即可。这里唯一的问题是 checkpoint
怎么计算,以及接收到受到的报文为什么也会有 ackno
。
我的想法是这个 ack
实际上就是上一次收到报文后,接收端计算出的 ack
值,那么在下一次收到报文后,这个值就变成了一个检查点。因为上一次的 ackno
是期望发送端发出这个值为开始索引的报文,那么在下一次确定 absolute seqno
时显然这个值就是 checkpoint
(思考一下 checkpoint
的定义)
那么我们需要增加以下定义:
随后,对于 segment_received
:
这里有一个坑点就是
payload
的存储结构用的是string_view
(一种效率更高性能更好的string
替代品),我们不能直接调用payload().str()
,注意看方法的返回值
对于剩下的函数:
最需要注意的就是在计算 ack
时,需要查看是否关闭连接,如果关闭了那么需要算上 FIN
标志位所占的一个字节。
实验结果
运行 make -j4 && make check_lab2
(一样的错误一样的不用管)