CS144_2020_Fall_lab3(流发送)
1 概述
在实验0中,您实现了一个流控制的字节流(ByteStream)的抽象。在实验1和实验2中,您实现了将携带在不可靠数据报中的段翻译成传入字节流的工具:StreamReassembler和TCPReceiver。现在,在实验3中,您将实现连接的另一侧。TCPSender是一个工具,将传出字节流翻译为将成为不可靠数据报负载的段。最后,在实验4中,您将结合之前实验的工作,创建一个完整的TCP实现:一个包含TCPSender和TCPReceiver的TCPConnection。您将使用这个实现与互联网上的真实服务器进行通信。
回想lab2,我们实现了流接收的API,还记得流接收所需要的参数吗?
我们在接收函数中,接收了一个seg数据片段,将数据段塞入重排器,那么数据报从何而来呢,显然就是由发送端而来,本lab就是来实现发送端发送seg的行为。
3 The TCP Sender
TCP是一种可靠地在不可靠数据报上传输一对流控制字节流(每个方向一个流)的协议。TCP连接中有两个参与方,每个参与方同时充当发送方(发送自己的传出字节流)和接收方(接收传入字节流)。这两个参与方被称为连接的端点或对等方。
本周,您将实现TCP的发送方部分,负责从一个ByteStream中读取(由某个发送方应用程序创建和写入),并将流转换为一系列传出的TCP段。在远程一侧,TCP接收方将这些段(可能不是全部都到达)转换回原始字节流,并向发送方发送确认和窗口广告。
TCP发送方和接收方各自负责TCP段的一部分。TCP发送方写入了对于Lab 2中的TCPReceiver相关的TCPSegment的所有字段,即序列号、SYN标志、有效载荷和FIN标志。然而,TCP发送方仅读取接收方写入的段中的字段,即确认号和窗口大小。
以下是TCP段的结构,仅突出显示发送方将读取的字段:TCPSender的责任是:
跟踪接收方的窗口(处理传入的确认号和窗口大小)
在可能的情况下填充窗口,从ByteStream中读取数据,创建新的TCP段(包括如果需要的SYN和FIN标志),并发送它们。发送方应该保持发送段,直到窗口满或ByteStream为空。
跟踪已发送但尚未被接收方确认的段,我们称之为未确认段
如果足够的时间过去而它们尚未被确认,则重新发送未确认的段
为什么要这样做?基本原则是发送接收方允许我们发送的内容(填充窗口),并保持重传,直到接收方确认每个段。这称为自动重复请求(ARQ)。发送方将字节流分成段并发送它们,只要接收方的窗口允许。
由于上周的工作,我们知道远程TCP接收方只要至少一次收到每个索引标记的字节就可以重建字节流,无论顺序如何。发送方的任务是确保接收方至少收到每个字节一次。
到这,我们就要开始接触TCP保证可靠性连接的基础,我们就要开始了解TCP实现可靠性连接的相关发送规则了。
我们看他介绍的发送端需要做出的行为:
- 跟踪接收方的窗口(处理传入的确认号和窗口大小)
- 在可能的情况下填充窗口,从ByteStream中读取数据,创建新的TCP段(包括如果需要的SYN和FIN标志),并发送它们。发送方应该保持发送段,直到窗口满或ByteStream为空。
- 跟踪已发送但尚未被接收方确认的段,我们称之为未确认段
- 如果足够的时间过去而它们尚未被确认,则重新发送未确认的段
跟踪
正如我在lab1中提出的问题,如果我们塞入的数据报片填满了整个capacity,再有数据报seg传入会怎样?答案是不会出现这种情况。TCP实现的可靠性传输专门维护了一个窗口大小window_size,来告诉发送方接收方的capacity还剩多少空间(接收方在回复确认报文的时候会携带这个window_size在回复的ack头部中,后面会做到),发送方会根据这个大小发送合适数量的seg避免超出这个大小造成seg丢失。这,也叫做TCP的流量控制。
当然,我们在发送时候需要将某些段规定他特殊的部分,例如
在三次握手的时候,也就是连接的开始,我们需要对某些段标志连接开始的段的头部syn设为true(默认都是false),如果你还是不太了解TCP头部的信息,重新看一遍tcp_header.hh这个函数吧。与之相对应的,结束的时候需要对结束标志进行更改以及在传输中某些信息进行添加,更改。另外要知道我们的发送端的职责是尽可能将发送端内的出向字节流的seg全部发送出去,所以这里一定要注意。
第三点他让我们跟踪已发送但尚未被接收方确认的段,函数中体现位bytes_in_flight
然后最后一句表达了重传的思想。
我们都知道,在因特网传输过程中,信息是不具备纠错能力的,但是具备检错能力,但是作为可靠性传输,检查出错误我们应该怎么办呢?答案是重传。至于重传是怎么回事,我们继续往下看文档。
3.1 TCPSender是怎么知道数据报是否丢失的呢?
您的TCPSender将发送一系列TCPSegments。每个段都包含一个从传出的ByteStream中提取的(可能为空的)子字符串,使用序列号表示在流中的位置,并在流的开头标有SYN标志,在结尾标有FIN标志。
除了发送这些段之外,TCPSender还必须跟踪其未确认的段,直到它们占用的序列号完全被确认。定期地,TCPSender的所有者将调用TCPSender的tick方法,指示时间的流逝。TCPSender负责查看其未确认的TCPSegments集合,并判断最早发送的段是否在没有得到确认的情况下已经过了太长时间(即没有全部序列号被确认)。如果是这样,它需要被重传(再次发送)。
以下是未确认时间过长的规则。我们将要实现这个逻辑,尽管它有点详细,但我们不希望您担心隐藏的测试用例试图使您失败,也不要将其视为SAT上的词汇问题。我们将在本周提供一些合理的单元测试,并在完成整个TCP实现后在Lab 4中提供更完整的集成测试。只要您百分之百通过这些测试并且您的实现是合理的,您就会通过。
为什么要这么做?总体目标是让发送方在段丢失并需要重新发送时能够及时检测到。等待重新发送的时间非常重要:您不希望发送方等待太长时间才重新发送一个段(因为这会延迟到达接收应用程序的字节流),但您也不希望重新发送一个如果发送方稍微等待一下就会被确认的段,这会浪费互联网的宝贵带宽。
1. 每隔几毫秒,将使用一个参数调用您的TCPSender的tick方法,该参数告诉它自上次调用该方法以来经过了多少毫秒。使用这个参数来维护TCPSender的总存活毫秒数。请不要尝试从操作系统或CPU调用任何时间或时钟函数,tick方法是您唯一访问时间流逝的方法。这使事情保持可预测性和可测试性。
2. 当构造TCPSender时,它会得到一个参数,该参数告诉它重新传输超时(RTO)的初始值。RTO是在重新发送未确认的TCP段之前等待的毫秒数。RTO的值会随时间变化,但初始值保持不变。起始代码将初始RTO的值保存在一个名为initial retransmission timeout的成员变量中。
3. 您将实现重新传输计时器:在某个时间开始的警报,一旦经过了RTO,警报就会触发(或过期)。我们强调这种时间流逝的概念来自于调用tick方法而不是获取实际的时刻。
4. 每次发送包含数据的段(序列空间中长度非零的段)时(无论是第一次还是重新发送),如果计时器尚未运行,则启动它,以便在RTO毫秒后触发。通过触发,我们的意思是时间将在未来的一定毫秒数内耗尽。
5. 当所有未确认的数据都被确认时,停止重新传输计时器。
6. 如果调用tick且重新传输计时器已经过期: (a) 重新传输未被TCP接收方完全确认的最早(最低序列号)段。您需要在某种内部数据结构中存储未确认的段,以便能够执行此操作。 (b) 如果窗口大小不为零: i. 跟踪连续重传的次数,并增加它,因为您刚刚重新传输了某些内容。您的TCPConnection将使用此信息来决定连接是否无望(连续重传次数太多)并且需要中止。 ii. 将RTO的值加倍。这被称为指数退避,它减缓了在差劲网络上的重新传输,以避免进一步阻塞网络。 (c) 重置重新传输计时器,并启动它,以便在RTO毫秒后触发(考虑到您可能刚刚加倍了RTO的值!)。
7. 当接收方向发送方提供了确认以确认成功接收的新数据时(确认号反映了比任何先前确认的绝对序列号都要大的绝对序列号): (a) 将RTO设置回其初始值。 (b) 如果发送方有任何未确认的数据,则重新启动重新传输计时器,以便在RTO毫秒后触发(对于当前RTO的值)。 (c) 将连续重传的次数重置为零。
我们建议在一个单独的类中实现重新传输计时器的功能,但这取决于您。如果您这样做,请将其添加到现有文件中((tcp_sender.hh tcp_sender.cc)。
这一大段极其极其重要,他介绍了数据报是怎么判断丢失的,以及什么时候重传,建议配合代码看。
他这里给出了定时器的概念,定期器会被周期调用,每次调用会返回一个距离上次调用的时间,当距离上次接收到数据报的时间超过一个初始值最大重传时间的时候,那么就会发生重传,我们认为第一个没有被接收的数据段需要重传(这个很好理解,最早传出去但是还没被收到的数据报肯定是最前面有问题那个),然后同时维护一个连续重传次数。另外,为了保证我们的传输过程不会被各种因重传出现的数据报过多而性能下降,TCP特地设计了拥塞控制,每发生重传的时候,他紧跟着的下次重传时间翻倍,来避免因为网络差的原因滞留太多数据报。后面就是一些细节了,比如最大传输时间和连续重传次数的更新等等。下面就是你自己去尝试理解了。
3.2 实现发送端
好的!我们已经讨论了TCP发送方的基本思想(给定一个传出的ByteStream,将其分割成段,发送到接收方,如果它们在足够快的时间内没有被确认,则继续重新发送)。我们已经讨论了何时可以得出结论,以及何时认为一个未确认的段丢失并需要重新发送。
现在是时候介绍您的TCPSender将提供的具体接口了。有四个重要的事件需要处理,每个事件可能最终会发送一个TCPSegment:
1. **void fill_window()** TCPSender被要求填充窗口:它从其输入的ByteStream中读取并尽可能多地发送字节,形成TCPSegments,只要有新的字节可以读取并且窗口中有空间。 确保每个发送的TCPSegment完全位于接收方的窗口内。使每个单独的TCPSegment尽可能大,但不要超过由TCPConfig::MAX_PAYLOAD_SIZE(1452字节)给定的值。 您可以使用TCPSegment::length_in_sequence_space()方法来计算段占用的序列号的总数。请记住,SYN和FIN标志也各自占用一个序列号,这意味着它们在窗口中占用空间。 如果窗口大小为零怎么办?如果接收方宣布窗口大小为零,则ll_window方法应该像窗口大小为一样。发送方可能最终发送一个单字节,被接收方拒绝(并且没有被确认),但这也可能促使接收方发送一个新的确认段,其中它透露出其窗口中已经打开的更多空间。否则,发送方将永远无法了解到底可以开始重新发送。
2. **void ack_received(const WrappingInt32 ackno, const uint16_t window_size)** 从接收方接收到一个段,传达窗口的新左边(= ackno)和右边(= ackno + window_size)边缘。TCPSender应该查看其未确认的段的集合,并删除任何现在已完全被确认的段(ackno大于段中的所有序列号)。如果有新的空间打开,TCPSender应该再次填充窗口。
3. **void tick(const size_t ms_since_last_tick):** 距离上次调用此方法已经过去了一定数量的毫秒。发送方可能需要重新传输一个未确认的段。
4. **void send_empty_segment():** TCPSender应该生成并发送在序列空间中长度为零的TCPSegment,并正确设置序列号。如果所有者(您将在下周实现的TCPConnection)希望发送一个空的ACK段,这将非常有用。 注意:像这样不占用序列号的段不需要被追踪为未确认,也永远不会被重新发送。
要完成Lab 3,请查阅文档中的完整接口,网址为 https://cs144.github.io/doc/lab3/class tcp sender.html,并在tcp_sender.hh和tcp_sender.cc文件中实现完整的TCPSender公共接口。我们期望您可能需要添加私有方法和成员变量,以及可能需要辅助类。
下面就是实现了,具体细节我这里不想讲,因为这里的细节太多了,很多问题需要你自己debug发现才有意思,印象深刻,大体思路理解了后面都不是问题。
对了,TCP有一个很有意思的点
这里,当窗口是0的话,我们没必要对初始超时时间翻倍,这样做是无意义且浪费带宽的。
参考自p165 自顶向下
这是一个需要注意且文中没有提到的地方,而且测试点还有,如果你面向样例编程也可以通过。
tips:一定要仔细读接口设计注释来理解各个函数!
3.3 测试
测试您的代码时,测试套件将期望它通过一系列情况的演变,从发送第一个SYN段,到发送所有数据,再到发送FIN段,最终直到FIN段被确认。我们认为您可能不想添加更多的状态变量来跟踪这些状态——这些状态已经由您的TCPSender类的公共接口定义。但为了帮助您理解测试输出,这里是TCPSender在流的生命周期中的预期演变图表。(在Lab 4之前,您无需担心错误状态或RST标志。)
这里其实我个人是没有用到的,因为我这里对于状态的判断是挺精准的,但是如果你对TCP什么情况应该处在什么状态有疑问,一定要看这张图,你会受益匪浅。
3.4 Q&A
喜闻乐道的放水环节,Q&A他的核心目的就是提醒你需要注意的一些事情,所以一定要看。
• 如何发送一个段? 将其推送到segments_out队列上。就您的TCPSender而言,只要将其推送到该队列上,它就被视为已发送。很快,所有者将会过来并弹出它(使用公共的segments_out()访问器方法),然后真正地发送它。
• 等等,如何既发送一个段又跟踪同一个段作为未确认的,以便稍后知道要重传什么?我难道不必复制每个段吗?这会浪费资源吗? 当您发送包含数据的段时,您可能希望将其推送到segments_out队列,并在内部保留一个副本,以便在可能需要重传时跟踪未确认的段。这实际上并不浪费太多,因为段的负载被存储为引用计数的只读字符串(Buffer对象)。所以别担心,实际上并没有复制负载数据。
• 在我从接收方得到ACK之前,我的TCPSender应该假设接收方的窗口大小是多少? 一个字节。
• 如果确认仅部分确认了某个未确认的段,我该怎么办?我应该尝试剪切已确认的字节吗? 一个TCP发送方可以这样做,但是对于这个课程来说,没有必要搞得太复杂。将每个段视为完全未确认,直到它已完全被确认,即它占用的所有序列号都小于ackno。
• 如果我发送包含a、b和c的三个单独的段,并且它们从未被确认,我能否稍后在一个包含abc的大段中重新传输它们?或者我必须逐个重新传输每个段? 同样:一个TCP发送方可以这样做,但是对于这个课程来说,没有必要搞得太复杂。只需单独跟踪每个未确认的段,当重传计时器超时时,再次发送最早的未确认段。
• 我应该在我的未确认数据结构中存储空段并在必要时重新传输它们吗? 不需要,唯一应该作为未确认并可能重新传输的段是那些传递了一些数据的段,即在序列空间中占用一些长度的段。不需要记住或重新传输占用零序列号的段(没有负载、SYN或FIN)。
• 我在这个PDF之后还能找到更多的常见问题解答吗? 请定期查看网站(https://cs144.github.io/lab_faq.html)和Piazza。
最后同样,make check_lab4
就可以测试用例了。
做到这,lab3就结束了。
我想做到这你应该有点想法了,我们现在已经实现了几个基础部分,还有发送端,接收端的角色,TCP三次握手,当发送方发送数据seg的时候,会考虑对方是否塞得下,同时还要定期重传数据报,传输标准就是没有确认的第一个seg,这里就是根据reveive方所发来的ack中的ackno来确认,同时维护握手挥手中头部特殊标记的变化,总之,lab3是一个非常考验你debug能力的lab,其中的细节会让你对于TCP的传输印象深刻。