CS144_2020_Fall_lab1(流重组)

废话已经在lab0说完了,我们直接来看lab1

0 一些规定,废话


1 概述


在实验 0 中,你使用了一个互联网流套接字来从网站获取信息并发送电子邮件,使用了Linux内置的传输控制协议(TCP)实现。这个TCP实现成功地产生了一对可靠的按顺序的字节流(一个从你到服务器的流,另一个在相反的方向),即使底层网络只传递了尽力而为的数据报。这意味着:短数据包可能会丢失、重新排序、更改或重复。你还在一个计算机内存中自己实现了字节流抽象。在接下来的四个星期里,你将实现TCP,以在由不可靠数据报网络分隔的两台计算机之间提供字节流抽象。

为什么要这样做?在不太可靠的服务之上提供服务或抽象,涉及到网络中许多有趣的问题。在过去的40年里,研究人员和实践者已经找出了如何在互联网上传递各种各样的东西:消息和电子邮件、超链接文档、搜索引擎、声音和视频、虚拟世界、协作文件共享、数字货币等。TCP本身的作用,使用不可靠的数据报提供一对可靠的字节流,是这方面的经典例子之一。一个合理的观点认为,TCP实现是地球上最广泛使用的非平凡计算机程序之一。

一点想说的:昨天上课的时候老师提出来一个问题,因特网在数据传输的过程中是否提供纠错和检错能力,我想大家心里都有答案,因特网通过冗余的方式检查是否出现错误,其中最为典型的就是通过CRC校验码,而纠错能力呢?显然因特网并不具备这个功能,出现错误,因特网做的是什么?重传!没错,所以在接下来的实验中,我们要做的,就是实现数据在复杂环境下的可靠性传输,这样的重传,显然就是实现可靠性传输的一种方式。

这张图将会伴随我们直到所有lab做完,他也是整个TCP在创数过程中所有的所有动作的集合。
我们没必要马上看懂,只是慢慢跟着实验走,我们很快就可以理解图中信息的含义!

今天我们做的是流重组器,听起来高大上,其实原理很简单。
我们回过上面的图中观察数据报片的状态变化。

我们模拟一下整个传输的过程,首先我的发送端有想要发送的信息了,那么通过上层调用,将想要发送的信息写入到输出字节流中,暂存在输出字节流中保存为待发送字节,当输出字节流满足一些条件的时候(后面lab会讲),会将信息发送出去,注意,当发送之前,我们的输出字节流中存储的数据是否是有序的呢?答案显然,怎么进来怎么出去,但是当发送出去之后呢?我们都知道,传输信道的MTU是有上限的,为了保证我们的数据报可以完整高效发送到对端,发送方上层需要对数据报进行分片处理,分片后每个片携带原数据报对应位置的信息,但是我们都知道在链路上,可能会发现很多变故,路由变化,负载均衡,网络拥塞,等等等等一系列变故就会导致发送方顺序发送出去的数据报碎片在途中顺序发生了变化,那么到达的时候顺序也就不是正确的顺序。为了解决问题,TCP特意为此设计了重组器,来满足稳定可靠的同行。什么是重组器,说白了就是将打乱的数据报重新排列起来的地方。今天我们就要实现他。

下面就是介绍一下我们后面需要做的内容

实验任务要求你以模块化的方式构建一个TCP实现。回想一下你在Lab 0中刚刚实现的ByteStream吗?在接下来的四个实验中,你将通过网络传输两个ByteStream:一个是出站ByteStream,用于本地应用程序写入套接字并由你的TCP发送到对等方,另一个是入站ByteStream,用于来自对等方的数据,将由本地应用程序读取。下图展示了这些部分如何组合在一起。

1. 在Lab 1中,你将实现一个流重组器,这是一个模块,用于将字节流的小片段(称为子字符串或段)重新拼接成正确顺序的连续字节流。
    
2. 在Lab 2中,你将实现TCP处理入站字节流的部分:TCPReceiver。这涉及思考TCP如何表示字节流中每个字节的位置,称为序列号。TCPReceiver负责告诉发送方:(a)它已成功组装了多少入站字节流(这称为确认),以及(b)发送方现在允许发送多少字节(流量控制)。
    
3. 在Lab 3中,你将实现TCP处理出站字节流的部分:TCPSender。当发送方怀疑它发送的一个段在传输过程中丢失,从未到达接收方时,发送方应该如何反应?什么时候应该尝试重新传输丢失的段?
    
4. 在Lab 4中,你将结合前两个实验的工作,创建一个工作的TCP实现:一个包含TCPSender和TCPReceiver的TCPConnection。你将使用这个TCPConnection与世界各地的真实服务器进行通信。

没什么说的


2 Getting started


你的TCP实现将使用Lab 0中使用的相同Sponge库,其中包含额外的类和测试。为了开始工作:

确保你已经提交了Lab 0的所有解决方案。请不要修改libsponge目录顶层以外的任何文件,或者webget.cc文件。否则,你可能会在合并Lab 1的起始代码时遇到问题。

在实验分配的存储库中,运行git fetch以检索实验分配的最新版本。

通过运行git merge origin/lab1-startercode下载Lab 1的起始代码。

在构建目录中,编译源代码:make make-j4(你可以在编译时使用例如make-j4来使用四个处理器)。

在构建目录之外,打开并开始编辑writeups/lab1.md文件。这是你实验报告的模板,并将包含在你的提交中。

这里面就是教你怎么构建lab1的代码,fetch什么的是给他们自己学生说的,咱们拿到的已经是完整的demo,所以不用管,切换到lab1分支然后merge一下把之前lab0写的代码merge进来以便后续使用就可以了。


# 3 Putting substrings in sequence

到这开始我们就要开始实现了,

  
在这个实验和下一个实验中,你将实现一个TCP接收器:这个模块接收数据包并将它们转换成可靠的字节流,可以通过套接字被应用程序读取,就像在Lab 0中你的webget程序从web服务器读取字节流一样。TCP发送方正在将它的字节流分割成短的段(每个子字符串不超过约1460字节),以便它们每个都适应一个数据包内。但是网络可能会重新排序这些数据包,或者丢弃它们,或者传递它们多次。接收器必须将这些段重新组装成它们最初的连续字节流。

在这个实验中,你将编写负责这个重组的数据结构:一个StreamReassembler。它将接收子字符串,包含一串字节和该字符串在更大流中的第一个字节的索引。流的每个字节都有其自己唯一的索引,从零开始逐个计数。StreamReassembler将拥有一个输出的ByteStream:一旦重组器知道流的下一个字节,它将写入到ByteStream中。所有者可以随时访问和从ByteStream中读取。
// 构造一个StreamReassembler,最多存储capacity字节。
StreamReassembler(const size_t capacity);

// 接收一个子字符串并将任何新的连续字节写入流中,同时保持在容量内存限制内。超过容量的字节将被静默丢弃。
// data: 子字符串
// index: 表示data中第一个字节的索引(在序列中的位置)
// eof: 该子字符串的最后一个字节将是整个流的最后一个字节
void push_substring(const string &data, const uint64_t index, const bool eof);

// 访问重组后的ByteStream(来自Lab 0的你的代码)
ByteStream &stream_out();

// 存储但尚未重组的子字符串中的字节数
size_t unassembled_bytes() const;

// 内部状态是否为空(除了输出流以外)?
bool empty() const;

为什么要这样做呢?TCP针对重新排序和重复的鲁棒性来自于它将字节流的任意部分重新拼接成原始流的能力。在一个离散可测试的模块中实现这一点将使处理传入的段变得更加容易。 StreamReassembler类在stream reassembler.hh头文件中描述了重组器的完整(公共)接口。你的任务是实现这个类。你可以向StreamReassembler类添加任何私有成员和成员函数,但不能更改其公共接口。

这里就是就是告诉你你的任务,简单介绍了一下你需要实现的各个函数。

至于难点,我们继续往下看

3.1 capacity 是什么?

你的push_substring方法将忽略任何使StreamReassembler超出其容量的部分:这是对内存使用的限制,即它允许存储的最大字节数。这防止了重组器使用无限量的内存,无论TCP发送方决定执行什么操作。我们在下面的图片中进行了说明。容量是两者的上限:

  1. 重新组装的ByteStream中的字节数(在下图中以绿色显示)。
  2. 未组装的子字符串可以使用的最大字节数(在下图中以红色显示)。 在实现StreamReassembler并通过测试时,你可能会发现这张图片很有用。有时候,正确的行为并不总是很明显。

其实这个图还是有点抽象的,我们分析一下,当我们接收到乱序数据报的时候,我们应该做什么呢?没错,我们要将他重组,但是在何时何地重组呢?这就是我在理解这部分的时候出现的问题。仔细研究后就会发现,这个图已经给出了答案。

我们可以很容易脑补出,我们的重组行为是发生在接收端的行为,当然,在很上面那个传输图中已经体现出来了。我们可以看到,在发送的时候,发送端维护了一个发送流,接收的时候,接收端维护了一个接收流。最开始我在做lab1的时候,认为这个重组肯定是在某个特殊的位置吧,但是不然,我们可以看到reassembler的方框是包含inbound stream的,同时在上面三色图中,我们可以看到capacity是已经重组和未重组的数据报的总和(分析一下可以发现,红色是发进来但是没被重组的数据报,绿色是已经被重组但是没被上层读取的数据报,蓝色是已经被读取的部分。)我们可以发现,我们的重组工作其实就是在流里面,我们需要维护的就是这个窗口。

回过头来再来看这个图,是不是茅塞顿开呢?如果实在看不懂他这个有点随意的图,我们来看一下这个

其实是一样的,画的好看点。

你可能会问,那我的数据报发多了会怎么样呢?超过了capacity的值了。这个答案我们在后续实验中就会明白,TCP为了保证稳定传输,同时还维护了这个能够传输的最大值,也叫窗口大小。

3.2 一些提问解答

• 整个流的第一个字节的索引是什么?是零。

• 我的实现应该有多有效率?请不要将这看作是构建一个极度低效的空间或时间数据结构的挑战,因为这个数据结构将是你的TCP实现的基础。一个合理的期望是,每个新的Lab 1测试应该在半秒内完成。

• 不一致的子字符串应该如何处理?你可以假设它们不存在。也就是说,你可以假设有一个唯一的基础字节流,并且所有的子字符串都是它的(准确的)切片。

• 我可以使用什么?你可以使用标准库的任何部分,你觉得有帮助的。特别地,我们希望你使用至少一个数据结构。

• 何时应该将字节写入流中?尽早。唯一不应该在流中的情况是在它之前有一个尚未推送的字节。

• 提供给push_substring()函数的子字符串可以重叠吗?可以。

• 我是否需要向StreamReassembler添加私有成员?是的。子字符串可以以任何顺序到达,因此你的数据结构必须记住子字符串,直到它们准备好被放入流中,也就是说,直到它们之前的所有索引都已经被写入。

• 我们的重组数据结构是否可以存储重叠的子字符串?不行。虽然可以实现一个正确接口的重组器来存储重叠的子字符串,但允许重组器这样做会破坏容量作为内存限制的概念。当评分时,我们将考虑存储重叠子字符串为样式违规。

• 更多常见问题解答:欲了解更多,请参阅 https://cs144.github.io/lab_faq.html

这里就是帮助你在实现的时候完善一下细节,具体你一定要仔细看而且尽可能去看英文文档,我这里就不多说了,他解释的挺好的。
tip:先把stream_reassembler.cc和stream_reassembler.hh两个文件好好看看,把注释翻译理解了,再往下走。

接下来我们就要实现了。

说了这么多,怎么做呢?我们可以看到他的push_substring函数,也就是把数据报塞到入流的函数,有三个传参,data,没什么说的,就是我们数据报片携带的信息,index,正确的位置,eof,是否结束。这里面index是最重要的信息了,我们知道数据报碎片并不是拼图一样,所有块完美契合每个空缺,由于重传等保证可靠性规则的原因,数据报碎片可能出现交集,覆盖等问题,我们就需要在这些问题的基础上,完成我们的流重组器。

基本就这几种情况,这道题的关键还是控制好边界问题,抽象出来其实就是一个简单的算法问题,并不是很难,我用结构体和set进行维护的,当然有很多种办法,我队友甚至用了并查集哈哈,当然也可以。

总之选择一种合适的就行,复杂度控制在n方之内就没什么问题。

下面贴出代码。
具体细节我就不讲了,没什么好说的,代码实现一定要自己动手来。
reassembler.hh
reassembler.cc

写完之后make一下然后make check_lab1就可以测试用例了。

这里讲一下我们怎么debug吧,有两种方式,第一种就是最简单的,输出调试,我们可以逐行输出来定位出现问题的位置,可以在/build/Testing/Temporary这个路径下的三个文件来定位错误。在Lasttest.log文件我们也可以看到出现问题的测试点是哪个,然后点到生成这个测试点所在的文件,在里面定位其中的哪一步出了问题。当然,更直观的肯定是用GDB。那么到这,我们的整个lab1就完结了。

posted @ 2024-03-07 08:30  AtongM  阅读(166)  评论(0编辑  收藏  举报