阅读须知:做这个实验的时候,我计网也只能算一个初学者,所以出现错误在所难免,另外这篇博客只能算是我在做实验时边做边写的笔记以及实验后的一点总结,所以难免会有一些不那么正式的语句,不过我不打算对这个笔记做出任何修改,毕竟这保留了我当时做这个实验时的心情在里面,多年以后回过头来看这篇文章还是一件有意思的事情的。另外这篇文章图比较多,图都是我一张张手动贴上去的,所以不可避免图的位置会出错或者漏掉,敬请指正。
版权所有:汪阿少二少
6/9 Test #28: t_webget ......................... Passed 1.02 sec
这一个测试点有点慢,我认为原因是服务器在国外,所以访问比较慢
7/9 Test #50: t_address_dt .....................***Failed 0.01 sec
This test requires Internet access and working DNS.
Error: getaddrinfo(www.google.com, https): Name or service not known
至于这个测试点挂掉,我想原因恐怕很简单了
首先Writingwebget这个任务我就卡住了, 因为我看见讲义前面的一部分有点简单且无聊,就是在命令行下面输几条命令而已嘛
然后这个我知道从哪里下手也就算了,讲义下面的提示我一开始也没搞懂是要暗示我什么…
转折点: 我根据讲义给定的几条讲解相关数据结构的链接, 找到了TCPSocket Class Reference
, 然后这个里面有一个code example, 看了这个之后我大概就知道连接这一块怎么写了, 也知道host参数和 http请求在哪里用了,但是这个path是啥呢?api里面没有这个啊,于是继续读讲义, 发现讲义提到了这么一点: , using the format of an HTTP(Web) request that you used earlier. 难道就是前面叫我们在命令行里面敲的东东吗? 再然后看到hint:
Please note that in HTTP, each line must be ended with “\r\n”
, Don’t forget to include the “Connection: close” line in your client’s request.
好家伙,懂了,这是要我们自己构造请求, 于是到这里,这个任务基本就很简单啦.
不过好景不长, 接下来的一个任务我又卡住了, 虽然题目就是要我们实现几个函数而已, 不过由于我编译一直报错, 因为编译选项里面规定变量必须在初始化列表里面初始化, 好家伙, 我连c++ 都不会, 给我来这一套, 我忍都傻了, 后来查了一下资料大概知道这里的初始化列表是啥了,.
不过这一题还出现了一个问题, 就是之后我在写的过程当中不断的在加字段,因为一开始没有考虑完全,到底哪些字段是必须的, 不过由于这一题不算复杂, 更改的工作量倒也不算太大, 不过最后写完之后测试发现有些数据点有问题,仔细看报错信息之后发现是eof有问题,这个地方折腾了我一会, 我一开始的写法是多加一个变量,在需要读的一开始判断是否还有可以读的,没有就设eof为true, 不过测试还是不能过,后来又小改一会发现还是8泰兴,于是上github借鉴了一下别人的,发现别人的代码确实优雅, 当buffer为空而且输入没有结束的时候就eof为true即可.秒!
bool ByteStream::eof() const {
if (buffer_empty() && input_ended()) {
return true;
} else {
return false;
}
}
这个实验做了我一个晚上, 不过还是有一点意思的, 这只是一个热身实验,后面的还没有更新,等待着斯坦福大学的课程更新再继续加油!
9/26 今天开始做lab1啦
记录一个小插曲, 其实之前用clion做lab0的时候,就出现了cmake.list加载失败, 导致没有只能提示, 做的很难受,不过lab0简单啊, 所以也就还这样凑合着做下去了, 不过lab1难度一下子提升了好多, 没有智能提示, 没有报错, 也没有寻找引用等功能着实难顶, 于是决定把这个问题解决掉:
首先找到cmake.list
然后右键它,选择, reload cmake project, 这个时候会看到一个报错
这我可懵逼了, 对cmake一窍不通, 于是开始上网搜索, 试了好几个答案, 结果都不行, 直到遇到了这篇文章
大牛的解释是这样的:
好了,当DOXYGEN_EXECUTABLE变量已经被填充但具有WRONG值时,此配置错误发生在命令find_package(Doxygen QUIET)上。
给出的解决方案是这样的:
set(DOXYGEN_EXECUTABLE "") find_package(Doxygen QUIET)
在cmake.list当中加上这两行, 然后重新reload, 等待一小会之后一切就恢复正常啦
9/27 今天做完了lab1的内容,代码没有多少,但是边界条件是真的折磨人, 所以想要好好的总结一下这一个实验的边界条件:
首先谈一谈这道题目想要我们做什么, 因为在网络传输中会将一个大包分成许多个小包, 每个小包在传输过程中可能会出现丢包,重复,重叠,乱序等现象,所以我们需要将收到的包重组为一个完整的包
举个例子:
那么为了解决这个问题, 我们势必需要设计一个好的数据结构, 首先想到的是优先队列, 这样我们可以把收到的包放进一个优先队列, 然后优先级为index, 取出最小index 的包即可,但是, 真的只有这么简单吗? NoNono
首先你对于你收到的包,并不是每一个字符都会接收的, 举个简单的例子,比如你期待next_index = 0
, 然而你现在的容量只有8, 那么你目前只能接收到的index号的范围为[0, 7]
, 是一个闭区间,至于为什么呢? 其实这个很容易想, 因为加入你可以接收大于7的index, 比如8, 那么可能出现这么一种情况, 你的缓冲区刚好是index为1~8的字符, 那么0的就没地方装了,于是你就卡死了
上面谈到了容量的问题,那么这里我就详细的聊一聊容量, 首先上一张图, 这张图是讲义里面有的, 但是你不见得理解了
注意上图中我用红框框圈出来的部分,这个都算在我们的容量里面的,什么意思呢? 思考下面这种情况:
你的容量为8, 你期待接受的下一个index = 0, 正好你下一个接受到的包就是 “abc” index = 0, 那么很好, 这三个字符可以重组, 于是放进_output里面, 也就是上图中的绿色部分,但是呢, 还没有被read, 知道我说的啥意思吗? 不知道建议复习一下lab0, ByteStream也是有缓冲区的, 所以我们现在的容量依然只有5, 那么好,现在的next_index为3, 那么我们可以接受的最大的下一个index是多少呢? 但是是还是7, 所以这里如何计算窗口可以接收的max_index我想你已经清楚了, 我当时就是这里的计算公式一开始想错了, 导致debug了一个上午.
不过后来想了一下, 好像他说的是对的,只不过我的英语理解能力不行, 我一开始怎么理解的呢? 如果一个串的长度超过了可以存储的最大容量, 那么沉默地将它扔掉, 现在回想起来应该是将多余的部分扔掉就行了.同样, 举一个实际的例子, 考虑下面的情形:
你期待的next_index = 0, 你的最大容量为1, 这时你接受到了一个串 “ab” , index = 0, 那么这时缓冲区应该只有一个字符,就是a, 而不是把这个串直接扔掉
附上我认为最好的一个测试用例:
ReassemblerTestHarness test{
8};
test.execute(SubmitSegment{
"abc", 0});
test.execute(BytesAssembled(3));
test.execute(NotAtEof{
});
test.execute(SubmitSegment{
"ghX", 6}.with_eof(true));
test.execute(BytesAssembled(3));
test.execute(NotAtEof{
});
test.execute(SubmitSegment{
"cdefg", 2});
test.execute(BytesAssembled(8));
test.execute(BytesAvailable{
"abcdefgh"});
test.execute(NotAtEof{
});
仔细分析这个测试用来, 你会找出你程序当中的许多bug.
好的, 上面介绍了一系列边界情况, 还没有介绍如何处理重复与重叠的问题, 这个当然就要用到数据结构啦, 如果是一个优先队列,里面的元素是一个结构体的话处理这个将会非常麻烦,于是我采用的数据结构是map
, 是一个(int index, char ch)的键值对
我来说一说大致的处理思路吧,首先根据上面谈到的边界处理条件判断哪些是应该加到我们的缓冲区里面的, 一旦发现next_index被加入了, 那么就说明一件事情, 有一些可以重组了, 于是我么可以用一个while循环, 如果
while map.find(next_index) != map.end() {
...
next_index++
}
这样就可以把连续的全部放到已重组的里面了, 这种数据结构处理重复和重叠很方面, 因为重复和重叠说明了一件事情,对应下标就在map里面,我们直接continue就可以了
至于这种数据结构处理其他的也很方便,具体代码细节就不讲了, 反正代码写出来还蛮短的!
最后附上PASS截图(我把DNS那个点的测试的goole换成baidu了, 原因大家都懂的QwQ)
继续加油, 冲冲冲!!!
10/4 昨天晚上做了一会lab2, 结果没想到在seqno 与 absolute seqno之间的转化这个问题上面把我直接卡死了, 然后昨天就看了一会这街3总决赛就睡了, 今天起来之后,发现对这个问题的理解更加深刻了, 于是来记录一下:
它们之间的关系看这张图就好了:
我下面重点来讲一讲unwrap
函数:
uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
WrappingInt32 wrap_checkpoint = wrap(checkpoint, isn);
uint32_t diff = n.raw_value() - wrap_checkpoint.raw_value();
if (static_cast(static_cast(diff) + checkpoint) < 0) {
return checkpoint + diff;
}
return checkpoint + static_cast(diff);
}
大概的代码如上所示, 比较精炼, 是github上面的一个大牛给我的灵感.
首先我们的目标是将seqno转化为最接近checkpoint的一个absolute seqno, 我们的想法是求出checkpont转化为seqno之后的wrap_checkpoint
与seqno: n
之间的差值diff, 那么这个差值会出现一下几种情况:
为了简单起见,我们想象一个百米冲刺的跑道, a是运动员n, b是运动员 wrap_checkpoint
所以综上情况, 我们只用将diff解释成有符号数加上checkpoint即可, 再强调一遍, 虽然超过半圈无符号数计算最高位可能是1, 所以解释成有符号数反而成负数了,但我们要的就是负数, 所以刚刚好
接下来我要来谈一种特殊情况, 就是a超过b半圈以上, 但是b却只跑了一点点, 导致如果a如果看做落后b半圈以内的某个值时,a跑的距离成负数了, 这种情况是肯定不允许的, 所以我们需要判断static_cast
的值它如果小于0, 那么就说明如果看做a落后b半圈以内的某个值时导致a已经跑步的距离为负数, 所以这种情况我们只能选择a领先b大半圈, 因为没办法, 谁叫b跑的太少了呢?
对了, 还有一点需要注意,那就是隐式类型转换, 有符号与无符号运算会隐式转换为无符号, 所以如果你不确定会不会隐式转换, 那么久static_cast显式转换喽.
然后我们打印一些信息来看一下对应的状态的值都是怎么样的,来更深刻的理解一下:
重点体会我圈出来的两个.
然后我自己加了一个测试有助于你理解:
对应的打印信息是, 重点看我圈出来的:
看见没有, 由于溢出diff减出来其实是一个最高位为0的.
最后附上wrapping_interger
这一块的单元测试的结果:
感觉这个实验代码量都好小啊, 这一部分的代码就写了不到20行, 不过就这短短20行代码, 我却写了一天, 下面我就谈一谈今天做了些什么吧:
首先昨天晚上看到实验指导书之后, 人都给看傻了, 这都什么东西啊, 只知道这个实验要我们实现一个TCPReciver, 不过完全不知道从哪里下手, 然后我先把指导书过了一遍, 发现还是不知道要干什么, 于是决定走一步看一步呗.
这个任务的具体解决方案我在上面已经写了, 那个是我刚做完这一部分就写了的, 我就不再过多的阐述.
干完第一个任务之后, 我大概就对题意有一定的了解了, 我们处理网络发来的包, 把seqno换成absseqno, 然后交给lab1写好的重组程序重组, 就是这样.
下面让我们来干第二个任务吧:
这个任务里面关于请求头的解析之类的框架代码已经帮我们做好了, 我们重点关注下面这张图:
重点关注其中非灰色的东西:
seqno: 这个就是序列号, 是一个WrappingInt32类型, 代表的就是这个包的第一个字节在整个序列当中的下标
ackno: 这个代表的就是下一个未重组的字节的下标, 这句话说的是什么意思呢? 就是lab1中的nextIndex换成了WrappingInt32类型而已, 所以它代表的滑动窗口的左边界
SYN: 这个如果为true的话就代表这个包是第一个包, 当我们的reviver处理程序收到这个包之后才会开始处理后续到来的包
FIN: 这个标志位如果为true的话就代表这个包的最后一个字节是整个序列的最后一个字节, 代表输入结束了
Window size: 代表的就是窗口的大小
Payload: 存放的就是传输过来的字符串
还有一个ISN没有出现在头中, 但是它也是我们TCP reciver中比较重要的一个变量, 代表的是Initial Sequence Number
, 详细解释看下面这张图:
我来说一下上面这张图需要注意的一点, 就是如果一个包的SYN为true, 那么ISN就初始化为这个包的seqno的值.
有了上面的知识储备, 我们可以很快的写出我们需要的私有变量以及初始化函数:
下面的convert函数就是一个unwarp的封装, 就是把a转换成一个uint64的数字而已, 因为后两个参数永远都是固定的,所以封装了一下, 方便调用.
然后接着看讲义, 发现需要我们实现的其实就只有三个函数, 其他的东西都帮我们实现好了.
这个函数就是这个lab当中最重要的一个函数,它的主要作用就是把tcp报文中的数据发送给lab1当中我们实现的重组函数进行重组,这里指导书里面说需要实现一个滑动窗口协议, 我一开始也自己实现了一下, 不过之后想到, 我的lab1好像做了窗口的判断的, 那么我这里应该就不用判断了啊. 不过事实就是, lab2确实不用判断了
那么这个函数需要我们干什么呢? 我们需要看一看tcp_segment.hh
这一个头文件, 还有tcp_header.hh
这一个头文件, 这两个头文件也不需要全部都看, 我们只需要关注下面的内容即可:
tcp_segment.hh:
tcp_header.hh:
然后再怎么编写代码就需要我们查看测试文件了, 首先需要我们关注的两个测试文件是:
一个测试连接, 一个测试关闭, 下面我举一个例子来分析一下这个测试文件是怎么测试你的代码的:
// 首先创建一个容量为4000的对象
TCPReceiverTestHarness test{4000};
// 检测容量是不是4000
test.execute(ExpectWindow{4000});
// 检测此时的ackno是不是nullopt
test.execute(ExpectAckno{std::optional{}});
// 检测此时的未重组的字节的数目是不是0
test.execute(ExpectUnassembledBytes{0});
// 检测总共的已经重组的字节的数目是不是0
test.execute(ExpectTotalAssembledBytes{0});
// 到来了一个syn=true, seqno=0的请求, 后面的是为了方便测试的一个东西
test.execute(SegmentArrives{}.with_syn().with_seqno(0).with_result(SegmentArrives::Result::OK));
// 这个时候由于接受了syn, 所以ackno应该变大了一个, 由于初始化为seqno的值为0, 所以现在为1
test.execute(ExpectAckno{WrappingInt32{1}});
test.execute(ExpectUnassembledBytes{0});
test.execute(ExpectTotalAssembledBytes{0});
再来一个:
TCPReceiverTestHarness test{5435};
test.execute(ExpectAckno{std::optional{}});
test.execute(ExpectUnassembledBytes{0});
test.execute(ExpectTotalAssembledBytes{0});
// 由于接受到这个请求之前没有收到syn=true的,所以这个包收不了,
test.execute(SegmentArrives{}.with_ack(0).with_fin().with_seqno(893475).with_result(
SegmentArrives::Result::NOT_SYN));
// 由于这个包没收, 所以ackno还是nullopt
test.execute(ExpectAckno{std::optional{}});
test.execute(ExpectUnassembledBytes{0});
test.execute(ExpectTotalAssembledBytes{0});
// 这回收到了, 并且isn初始化为89347598
test.execute(SegmentArrives{}.with_syn().with_seqno(89347598).with_result(SegmentArrives::Result::OK));
// 由于syn算作收到的第一个字节,所以ackno得加1
test.execute(ExpectAckno{WrappingInt32{89347599}});
test.execute(ExpectUnassembledBytes{0});
test.execute(ExpectTotalAssembledBytes{0});
有了上面的基本逻辑, connect的代码不难编写了, 至于fin的话, 说实话这一部分有点坑, 不过由于我在lab1已经考虑到这些并且已经编写了健壮的代码, 所以lab2我的处理就要简单不少.这个时候再说
下面说一下对于payload当中的数据的处理, 就两行代码:
uint64_t index = convert(seg.header().seqno+(seg.header().syn?1:0));
this->_reassembler.push_substring(string(seg.payload().str()), index-1, seg.header().fin);
这里需要注意的就是stream index与absolute index的区别, 后者比前者大1, 因为SYN的存在, 所以上面有一个index-1, 然后就是index的计算,如果当前的包函数syn=1,那么这个包的index得加1跳过syn, 因为syn不算我们客户要读取的数据
这个函数就是要我们返回ackno, 也就是next_index的wrappingInt32的表示, 代码很简单, 就是调用我们第一部分洗的函数即可:
if (!this->syn_) {
return nullopt;
}
return make_optional(WrappingInt32(wrap(this->stream_out().bytes_written() + \
(syn_?1:0) + (this->stream_out().input_ended()?1:0), this->isn)));
注意syn与fin也算接受到的数据, 前面不是卖了一个关子说过fin有一个坑呗, 这个坑就是收到了fin=1的包之后这个包可能就被丢了,所以我们不能简单的来判断是否收到这个字符,所以我的做法就是利用我lab1里面已经实现的判断逻辑,直接利用input_ended函数即可.其他的就没什么好说的了.
这个就更加简单了, 一行代码就搞定了, 不过这里的窗口的size的大小你们不一定容易理解, 可以看看我lab1里面踩到的这个坑, 没想到我lab2写的时候居然重蹈覆辙了一次, 虽然很快就意识到了:
最后make -j4
编译, 然后make check_lab2
运行测试, 全部PASS!!!哈哈哈哈
10.21晚上: 过了好久好久, 终于有时间做一做cs144了, 唉, 不过估计考完试之前也只有时间做一下这一个lab了
已经被发出去但是还没有接收到接受者确认的段叫做“outstanding” segments
对于empty segment, 也就是序列长度为0, 并且序列号设置正确的段不会被“outstanding” segments跟踪, 并且也不会被重传, 只是当使用者想要发送一个空的ACK报文的时候, 它会比较有用.
下面记录我做这个lab的一些尝试吧.
首先看到这个实验的时候, 还是蛮懵逼的, 因为指导书很长, 所以难免会有一些没有头绪, 所以在看完指导书之后, 我就开始看测试, 因为我觉得测试代码是读懂题意的最佳途径.
send_connect.cc
开始.TCPConfig cfg;
WrappingInt32 isn(rd());
cfg.fixed_isn = isn;
TCPSenderTestHarness test{
"SYN sent test", cfg};
test.execute(ExpectState{
TCPSenderStateSummary::SYN_SENT});
test.execute(ExpectSegment{
}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
test.execute(ExpectBytesInFlight{
1});
这是里面的第一个测试, 首先期待TCP的状态是SYN_SENT, 下面我们来看一看这是什么意思:
就是已经开始了但是还没有任何segment被接收, 也就是握手还没有建立, 但是已经准备发起握手了, 于是我写出了以下的代码:
然后运行测试, 发现第一个点就挂了:
发现是第一个就挂了, 于是我又看了一下源码, 发现了以下的东西
找到原因了, 是因为我没有更新_next_seqno
变量, 并且还没有实现bytes_in_flight
函数, 那么现在开始逐渐有方向了, 于是继续开始撸代码. 等到这个正确实现之后, 这一个测试用例就可以通过了, 也就是说我们现在可以正确的去发送syn请求了.
TCPSenderTestHarness test{
"SYN acked test", cfg};
test.execute(ExpectState{
TCPSenderStateSummary::SYN_SENT});
test.execute(ExpectSegment{
}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
test.execute(ExpectBytesInFlight{
1});
test.execute(AckReceived{
WrappingInt32{
isn + 1}});
test.execute(ExpectState{
TCPSenderStateSummary::SYN_ACKED});
test.execute(ExpectNoSegment{
});
test.execute(ExpectBytesInFlight{
0});
观看测试代码, 不难发现, 这个里面我们还没有实现ackReceived的逻辑, 所以这个测试是肯定过不去的.
查看指导书, 发现指导书里面有这么一句话:
the TCP sender only reads the fields in the segment that are written by the receiver: the ackno and the window size
于是我们更新ackno以及window size, 我更新完了这些, 然后继续运行测试用例, 发现又出错了, 错误如下所示
就是说当前应该是SYN_ACK, 但是我还是在SYN_SENDED状态, 那么肯定就是这里出问题了
我一开始还以为是我的状态更新错了, 后来发现了List of steps that executed successfully, 发现我的Action是对的, 然后我才意识到问题出在哪里: 我没有把_segments_out
里面缓存的reciever已经接收了的segmentpop掉, 所以增加一个相应的逻辑, 另外记得更新_bytes_flight
的值. 然后经过调试发现, _segments_out这个变量是会提供给外界的使用的, 也就是说, 里面的东西在发送的时候就被pop了, 所以我们需要自己维护一个指导书中提到的segment_outstanding. 上面说的这些东西实现之后, 我们的第二个测试就可以通过了.
另外我发现其实FAQ里面也提到了我上面谈到的使用者会pop的那一点:
另外发现它也描述了前面提到的备份的那一点
另外提到了在获得ack, 知道window_size之前, 默认的值为1
这里提到的一点和我想的也一样, 只有当一个seg中的所有的bytes都被接收了之后, 这个seg才被扔掉, 虽然你可以通过技术手段裁剪这个seg, 但是完全没必要, 因为TCP协议中reciver是可以处理overlap的情况的, 正如我们在lab1当中做的
最后一个很有用的提示就是我们不用将empty_seg存储到我们的备份当中, 因为这个不需要被重传
SYN -> wrong ack test
这个测试测试的是sender接收到了一个错误的ack之后应该怎么处理. 发现我这个测试挂了是因为前面的一个地方的if判断里面应该是 <
, 结果我写成了<=
, 修正过后测试通过.
这里感觉我实现的过程比较的蠢, 代码写的比较的丑陋, 思路就是在fill_window
函数里面通过窗口的大小以及上一个接受到的abs_ack来计算出期望得到的最大的下标的值, 然后再做出一下边界条件的判断, 比如_stream当中究竟有多少个字节以及一个tcp报文段最多可以携带多少个字节的数据. 从而计算出这一个段究竟传多少个字节的数据, 然后把数据复制进去, 并且放到out与outstanding里面去, 注意记得更改_bytes_flight
变量的值.
正确实现完上面的逻辑之后, 运行测试用例应该会成功通过:
这个测试用例里面有一段注释掉的测试, 我们暂时先不管它, 直接运行测试, 发现这个测试可以直接通过:
顾名思义, 这个测试文件是检测close是否正确的.
之后的不打算写的这么详细了, 大概的流程就是运行测试文件, 然后看报错, 然后找出原因做出更改, 继续运行测试, 记得更改之后前面已经通过的测试也要再运行一下. 修复之后继续运行应该会看到如下的报错:
看了上下文之后, 不难得出结论, 就是说有一个包超时了但是没有重传, 所以我们在这里加上计时器的逻辑.
这里讲一下计时器相关的一些约束:
你必须使用tick, 而不能使用time或者clock, 因为这样方便测试程序测试
每次重传之后你需要将retransmission timeout(RTO)这个时间翻倍, 这样可以避免由于网络原因造成太多次的重传
我们每次发送一个带有data的seg的时候, 我们都需要检查计时器是否已经运行, 没有的话, 启动计时器
当所有的seg都被发送并且都收到确认的时候, 我们关闭重传计时器
当计时器启动并且超时的时候
重传最早的没有被确认的seg, 也就是queue里面的队首元素
如果window_size非0, 那么我们需要跟踪重传的次数, 并且翻倍重传时间, 当重传次数太多时, 可以选择终止连接
Double the value of RTO. This is called “exponential backoff”—it slows downretransmissions on lousy networks to avoid further gumming up the works
重置计时器
当sender收到了接收者的确认包之后, 我们需要重置rto, 重置重传次数, 如果还有已经发送未被确认的包, 那么我们需要重置计时器, 否则关闭计时器.
大致完成上述之后就可以通过这个测试了, 因为这个测试不是很严. 打印信息是我自己调试的时候加上去的, 可以忽略.
这个注释部分不知道到底需不需要测试通过, 这里貌似当ack不对的时候需要我们重传第一个.
算了, 懒得管这个测试了.
这个测试文件把我整的够呛, 好多地方都没有考虑到位, 导致我调试了半天, 之前写的很多代码都重构了一下.
我发现了我代码里面一个非常严重的问题, fill_window函数当窗口没有满的时候需要一直调用直到填满窗口或者输入eof为止, 然后在接收到了ack然后移动了窗口使得窗口有空闲之后我们需要显示的调用fill_window来填充窗口. 不然是过不了测试的.
然后我们在接收到一个新的ack之后, 更新窗口大小时需要保存旧的窗口大小, 因为窗口移动时需要旧窗口大小的信息, 这里主要的疏忽就是没考虑到窗口大小在连接建立之后还会发生变化.
然后还有_fin标志位, 我们不能直接判断eof了, 然后就更新标志位, 我们需要窗口的大小可以装下这个标志位的时候才更新, 然后把含有标志位的seg放入 _seg_out当中.
然后还有一些错误可能不那么具有代表性, 纯属我个人NT, 就不写出来了.
这一块编写成功过后可以通过window的这个测试用例, 打印信息是我调试时使用的, 可以忽略:
这个测试用例直接就过了, 看来之前关于tick这一块实现的应该没什么大问题
这个测试也是直接过了
这个测试用例挂掉了
这里有一点很坑的地方, 就是当你接收到了一个window为0的ack的时候, 你要将它的window_sz视为1, 并且这个时候超时不能该表rto的值, 这个实现很简单, 加一个布尔变量, 再加一个特判就搞定了.
最后直接make check_lab3
测试一下
10/22 今天把昨天晚上剩下的一点lab3的坑填了, 然后做一做lab4, 这个做完了之后就得投入复习大业了.
大概看了一下指导书, 太长了, 先快速的浏览了一遍, 在头脑里面有一个印象之后, 再来精读然后做实验, 由于这个实验是把之前做的东西给全部整合起来, 所以下面这张图对于数据的流动应该会很有参考价值:
然后指导书上面定义了一些我们需要遵守的规则, 我们来看一下:
上面这张图说的是我们接收到一个报文时应该遵守的规则, 接收到报文, 很明显, 要么是sender接收到ack, 要么是reciver接收到一个报文段
seqno
, SYN
, payload
, FIN
等这里其实就很浅显易懂了, 不用解释了
下面这张tcp首部字段的图示有助于理解上面的规则:
指导书关于这个lab的任务描述到这里貌似就结束了, 下面就是FAQ了, 但是如果不实际遇到问题并思索一段时间的话, 直接看FAQ是感受不到它的那种字字珠玑的.
how should i get started?
, 它给出的建议是:可能最好的开始方式是将一些“普通”方法与TCPSender和TCPReceiver一些适当的函数调用关联起来。这可能包括像remaining_outbound_capacity()、bytes_in_flight()和unassembleled_bytes()这样的内容。
然后你可能选择实现"writer"的方法: connect(), write()和 end_input_stream(). 这些方法中的一些可能需要为outbound ByteStream做一些事情, 并且告诉TCPSender关于这些事情的一些信息.
然后就是运行测试, 通过测试输出的失败信息来找到你下一步应该怎么做的线索.
emmm, 然后我看了一下这次merge里面新增了哪些测试文件, 准备继续测试驱动开发
这里我给出自己的理解: inbound就是对等端传送过来的值, 这个值会在reciver部件中进行重组等操作, 然后放在stream_out当中, 同理, outbound就是自己传送给对等端的值, 需要在SENDER部件当中进行分段等操作, 然后放在stream_in当中.
其实这个测试用例可以直接看fsm_active_close.cc
里面的源码, 但是指导书里面说了这么一句话:
We’d discourage you from trying to read the test source code, unless it is as a last resort.
除非万不得已,否则我们不鼓励您尝试阅读测试源代码。
那么我们就先不读测试的源代码, 只尝试从它打印的错误信息里面找出我们的问题
这个问题很容易就找到了, 程序期望我们发送一个SYN包, 但是我们什么都没有发, 所以加一个发SYN包的逻辑就可以了, 我们在lab3中实现的fill_window在第一调用的时候会制造一个SYN包, 所以我们可以很容易的制造一个放到sender的_segment_out里面, 并且可以在lab4中取出来, 但是问题来了, 我们的TCPConnection怎么发这个seg呢? FAQ告诉了我们答案:
按照上述逻辑添加代码之后重新编译然后运行测试将结果保存到log文件中**(这句话以后省略为重新测试)**
果然又多过了一点点, 赶脚现在有方向了. 明天再冲!!!
啊这, 今天起来又做了一下之后发现前面就有不少错误, 比如我在前面几个lab压根没想到过sender发送的包也会有ack为true的情况, 也没考虑过它的ackno, 唉, 还是先把原理搞懂吧, 指导书上面说叫我们仔细阅读lab2-4中出现的几个图表, 我把这几个图表粘贴过来方便查看:
啊这, 我是废物, 我还是面向测试编程吧, 先来爬第一个测试的源码:
它的第一行测试经历了以下几个阶段:
1.
//! \brief Create an FSM which has sent a SYN.
//! \details The SYN has been consumed, but not ACK'd by the test harness
//! \param[in] tx_isn is the ISN of the FSM's outbound sequence. i.e. the
//! seqno for the SYN.
TCPTestHarness TCPTestHarness::in_syn_sent(const TCPConfig &cfg, const WrappingInt32 tx_isn) {
TCPConfig c{
cfg};
c.fixed_isn = tx_isn;
TCPTestHarness h{
c};
h.execute(Connect{
});
h.execute(ExpectOneSegment{
}.with_no_flags().with_syn(true).with_seqno(tx_isn).with_payload_size(0));
return h;
}
tx_isn 是有限状态机输出序列的isn
这一部分的代码首先调用TCPConnection的connect()函数, 这是发起第一次握手, 执行完connect之后 期待的状态是, 只有syn标记位为真, 段的序列号是tx_isn, 也就是初始的isn, 握手的一个作用就是交换双方的isn, 之后期待payload size是0, 也就是没有数据.
总结起来, 目前处于的状态是SYN_SENT
2.
//! \brief Create an FSM with an established connection
//! \details The mahine has sent and received a SYN, and both SYNs have been ACK'd
//! \param[in] tx_isn is the ISN of the FSM's outbound sequence. i.e. the
//! seqno for the SYN.
//! \param[in] rx_isn is the ISN of the FSM's inbound sequence. i.e. the
//! seqno for the SYN.
TCPTestHarness TCPTestHarness::in_established(const TCPConfig &cfg,
const WrappingInt32 tx_isn,
const WrappingInt32 rx_isn) {
TCPTestHarness h = in_syn_sent(cfg, tx_isn);
// It has sent a SYN with nothing else, and that SYN has been consumed
// We reply with ACK and SYN.
h.send_syn(rx_isn, tx_isn + 1);
h.execute(ExpectOneSegment{
}.with_no_flags().with_ack(true).with_ackno(rx_isn + 1).with_payload_size(0));
return h;
}
rx_isn就是状态机的输入序列的isn
这个函数里面send_syn消费发起连接的syn请求, 然后用一个syn以及ack的请求回应
最后我们收到这个回应之后发起第三次握手,
3.
//! \brief Create an FSM in FIN_WAIT
//! \details SYNs have been traded, then the TCP sent FIN.
//! No payload was exchanged.
//! \param[in] tx_isn is the ISN of the FSM's outbound sequence. i.e. the
//! seqno for the SYN.
//! \param[in] rx_isn is the ISN of the FSM's inbound sequence. i.e. the
//! seqno for the SYN.
TCPTestHarness TCPTestHarness::in_fin_wait_1(const TCPConfig &cfg,
const WrappingInt32 tx_isn,
const WrappingInt32 rx_isn) {
TCPTestHarness h = in_established(cfg, tx_isn, rx_isn);
h.execute(Close{
});
h.execute(
ExpectOneSegment{
}.with_no_flags().with_fin(true).with_ack(true).with_ackno(rx_isn + 1).with_seqno(tx_isn + 1));
return h;
}
然后执行close, 也就是调用end_input_stream()函数, 之后我们期待的状态如上所示, 我解释一下, tx_isn+1是因为只有syn被确认了, 同样rx_isn+1也是因为只有syn被确认了.
4.
//! \brief Create an FSM in TIME_WAIT
//! \details SYNs have been traded, then the TCP sent FIN, then received FIN/ACK, and ACK'd.
//! No payload was exchanged.
//! \param[in] tx_isn is the ISN of the FSM's outbound sequence. i.e. the
//! seqno for the SYN.
//! \param[in] rx_isn is the ISN of the FSM's inbound sequence. i.e. the
//! seqno for the SYN.
TCPTestHarness TCPTestHarness::in_time_wait(const TCPConfig &cfg,
const WrappingInt32 tx_isn,
const WrappingInt32 rx_isn) {
TCPTestHarness h = in_fin_wait_1(cfg, tx_isn, rx_isn);
h.send_fin(rx_isn + 1, tx_isn + 2);
h.execute(ExpectOneSegment{
}.with_no_flags().with_ack(true).with_ackno(rx_isn + 2));
return h;
}
这里send_fin消费之前发出的那个seg, 由于fin也被接受了, 所以ackno为rx_isn+2
5.
TCPTestHarness test_1 = TCPTestHarness::in_time_wait(cfg);
这一行调用的具体过程就如上面四步所示.
然后我还想知道上层应用是如何调用我写的TCPConnection的API的, 所以我用爬了execute的源码, 如下所示:
void TCPTestHarness::execute(const TCPTestStep &step, std::string note) {
try {
step.execute(*this);
while (not _fsm.segments_out().empty()) {
_flt.write(_fsm.segments_out().front());
_fsm.segments_out().pop();
}
_steps_executed.emplace_back(step.to_string());
} catch (const TCPExpectationViolation &e) {
cerr << "Test Failure on expectation:\n\t" << step.to_string();
cerr << "\n\nFailure message:\n\t" << e.what();
cerr << "\n\nList of steps that executed successfully:";
for (const string &s : _steps_executed) {
cerr << "\n\t" << s;
}
cerr << endl << endl;
if (note.size() > 0) {
cerr << "Note:\n\t" << note << endl << endl;
}
throw e;
}
}
看样子就是读取了我们放在_segments_out队列里面的seg
纵观这一行测试, 他要我们实现的东西应该就是三次握手和四次挥手的逻辑.
首先实现三次握手的逻辑, 实现成功之后应该会看到如下的界面, 注意里面的标志位, 我之前的lab压根就没有注意过, 结果还过了测试QwQ
看后面的挂掉的是因为到来了一个带有FIN标志位的包, 代表我们需要进行挥手了, 所以需要添加挥手的逻辑, 第四次挥手的逻辑添加成功之后, 我们会看到如下所示的结果:
接下来的报错显示我们需要实现active, linger_after_streams_finish
的逻辑, 这一块说实话, 我也没咋看懂QwQ, 然后看到讲义说这是最难的一块, 心里平衡好多
另外在解决这个问题的过程我, 我还找到了之前的一个疑问的答案, 之前几个lab里面一直没怎么管ack标志位, 自己对于这个也不是很清晰, 但是这一条FAQ解答了我的疑惑, 我也知道该怎么编码了. 对于理解下面我觉得有一点也很重要, 就是reciver里面的isn其实是对等方的isn, 所以reciver就是帮你计算期待接收对等方的下一个序列号是多少, 所以我们在编码的时候对于ackno的值可以直接调用reciver的ackno函数, 但是在调用之前记得先调用reciver的segment_received函数, 更新一下对应的值.
啊这, 我先把这个最难的留着吧, 先让这两个东西为true, 然后测试其他的测试用例, 我测试了一下fsm_connect_relaxed.cc
, 发现我没有实现收到第一次握手的回应逻辑, 第一次握手是SYN为true, 但是ACK为false, 报错如下所示:(我发现这个测试用例应该才是我们第一个应该尝试的, 因为是最简单的一个了)
简单实现了第一次握手的回应逻辑也就是第二次握手之后, 成功通过对应的测试:
这里的warning是因为我没有shutdown, 这个硬核逻辑放在后面, 太秃头了, 现在做一点养生的东东.
然后再测试fsm_active_close
, 发现倒在了状态机上面, 啊这, 算了, 我已经做好秃头的准备了.
看了一下指导书的第五节之后, 我有点懂了.有两种shutdown, 一种是clean的, 一种是unclean的
unclean shutdown, 就是通过seg中header的RST标记的值来指定的
clean shutdown需要一些前提条件:
什么意思呢? 就是前三条都满足了, 那么我们似乎可以认为对等端能够收到我们的确认, 但是由于TCP doesn’tdeliver acks reliably (it doesn’t ack acks).所以我们也并不是那么的有信心, 但是如果ack真的丢了的话, 那么对方会重传一些东西, 所以我们需要等待一会确认它是否重传了一些东西.
至于linger_after_streams_finish, 可以看下面的这段话.
以及这里, 交代了这个变量啥时候为true, 啥时候为false
然后对于active这里也给出了非常详细的步骤, 告诉我们应该怎么做
总之, 这里理解蛮困难, 但是幸好指导书给了上面图示的两段文字告诉我们什么时候这个变量应该是怎么样的, 所以难度就大大的降低了_, 说人话就是有手就行(手动狗头保命
当然, 如果我们是被动关闭的时候, 那么我们应该就不用等待了, 因为对方肯定接收到了, 不然我们不可能被动关闭
实现了active以及_linger_after_streams_finish之后, 测试fsm_connect_releax
应该是可以通过的, 不过会有warning, 我暂时不知道原因.
(有点累了, 下面的步骤可能会写的水很多QwQ)
上一阶段已经圆满完成, 现在让我们开始下一阶段的探索, 我们以fsm_active_close.cc
为突破口, 我们运行这个测试用例, 发现现在第一个测试是可以圆满通过的, 但是第二个测试挂了.
第二个测试的第一行与我们第一个测试的第一行一样, 内部也是有一堆函数调用, 但是唯一的不同就是, 第二个测试的对等端没有接收到我们的FIN, 它的ackno是tx_isn+1, 所以这里会触发超时重传, 但是我们这里并没有超时重传这个包.
现在错误已经很明显了, 所以我们需要添加相应的逻辑, 或者修改之前的错误逻辑. 添加相应逻辑之后, 正确通过测试
我们继续下一个测试用例fsm_listen_relaxed.cc
这个测试我们的机器是作为一个监听方, 等待其他客户端进行connect. 在我添加了第二次握手的逻辑之后, 测试运行成功.
我突然发现前面的connect测试用例又挂了, 找了一下原因: 考虑以下情境, 你首先发起第一次握手, 也就是 SYN=1, ACK=0, 然后可能这个丢了或是怎么的, 对面给你回的还是第一次握手, 也就是SYN=1, ACK=0, 这个时候你收到了这个, 那么按照我的处理逻辑就得发送第二次握手请求, 但是由于我最开始发送第一次握手请求的时候我的sender模块的实现里面把一个变量_syn置为true了, 所以第二次握手请求的发送是失败的, 所以这里导致什么都没发送, 但是抛开这些不谈, 测试用例期待的输出也是蛮离谱的, 期待你发送一个报文头为ACK=1, SYN=0, 我反正是想不通, 这里的bug我是通过加了一个if特判解决的.
update: 后来找到了这么一张图,按照这张图的状态机走, 很多东西都可以解释得通了
下一个测试用例fsm_passive_close.cc
, 看这个名字就知道, 是用来测试被动关闭的.
我这里出了一个bug, 后来排查很久之后发现是lab3里面的一个bug, 我的计时器没有及时关闭, 然后导致出现了一些重发的奇怪的bug. 解决完计时器的问题之后就奇迹般的过了.
然后附上这个测试点考察的知识点:
接下来完成rst字段的内容, 根据测试文件fsm_ack_rst_relaxed.cc
来完成
运行测试之后发现我只处理了三次握手四次挥手, 还没有处理平常的通信, 于是加了相应的逻辑之后又出错了, 因为我回复了只有ack但是payload什么都没有的回复包, 所以我又加了一个逻辑跳过这种包的回复, 之后运行测试用例才得到了我们想要的结果, 在RST标志位这里挂了.
我下面总结一下rst相关的知识点:
于是我们开始编写代码. 正确编写代码之后应该会看到如下所示结果:
其他测试测试结果:
loopback测试挂了, 我看了一下打印的调试信息, 结果就特么离谱, 这特么前面的测试怎么过得?
啊这, 我知道为什么窗口一直不动了, 数据太毒瘤了: 两边互相发数据, 但是对方回复的ackno一直没变, 导致我窗口移动不了, 后来变化了, 但是窗口大小成1了, 按照我之前写的判断逻辑, 窗口也移不了, 然后窗口满了之后就挂了
上面的问题解决了, 是因为我之前写的有问题, 现在我又遇到一个新问题, 就是windowsize=0的情况, 我之前按照讲义里面说的, 把它当1处理
结果测试用例玩我呢? 想要得到这个测试明显不能按照窗口大小为1处理啊.
我之后发现我的窗口的fill也出现了问题
调试发现是因为我在fsm_winsize.cc
里面的这一行执行结束后也进行了fillwindow操作, 但测试用例其实是不期待的
上图中shouldn’t 标记的填充过程应该在方框所示的执行之后才应该执行. 后来我在sender里面把fill_window的调用取消了, 然后在tcpconnection里面调用fill_window就奇迹般的修复了这个bug, 至此本地的测试都通过了, 现在就是像这种与远程主机之间通信还有概率挂掉(前80个还好, 后面的全部挂了):
我们要先搞懂这些测试文件的命名规则:
我发现我挂的点的DEBUG信息基本上都是, waiting for clean shutdown或者tcp connection finished uncleanlyError in _rt_connect
唉, 不想做了, 就这样吧, 最后实在不知道为什么会超时了
update on 12/05: 其实当时是准备考完试之后再把这个实验做一遍,争取跑过所有的测试点的,不过考完试之后人变懒了,而且实验室那边还有更重要的事情要做,最重要的是因为,考试周计网的一波极限复习我已经彻底学会计网了,感jio没必要再在这个实验上面花时间了,所以咕咕咕啦,另外之前做的时候其实是把那个随机测试的测试代码研究过一遍的,不过当时复习时间比较紧,没有时间记录下来,有点可惜