本实验文档的部分翻译用了翻译软件,所以翻译可能有些绕口
概述
- 你已经到了TCP实验的最后一步
- 在Lab4中,你将实现顶层模块TCPConnection,组合TCPSender和TCPReceiver,并管理连接的生命周期
- 本实验已经帮我们提前处理好了将segments交付给网络层的功能
- TCP-in-UDP的意思应该是将我们的TCP实现包装到UDP中
- 或者也可以直接包装到IP datagrams中

注意:TCPConnection可以用少于100行的代码实现,如果你的TCPSender和TCPReceiver是正确且鲁棒的,那么本实验将做的很快;否则你将花很多的时间debug,解决那些错误的测试样例。我们不建议你去阅读测试样例的源文件(代码),除非实在没有别的办法了
编写代码前的准备
你应该在Lab0~3的Sponge代码库的基础上实现本实验
我们已经提前实现了将TCP segments传入网络层的代码
- 我们准备了CS144TCPSocket这个包装类,用于使你的TCPConnection的行为像一个normal stream socket,即在Lab0中用到的TCPSocket
- 在本实验的最后,你将用CS144TCPSocket替代TCPSocket改写webget
- 确保在此之前已经commit了Lab3的内容,之后就不要再修改webget.cc、libsponge顶层目录下的文件了
- 在项目目录下运行git fetch,确保代码是最新的
- 运行git merge origin/lab4-startercode
- cd build
- make -j4
- writeups/lab4.md是本实验的checkList,需要提交,请认真填写
实现TCPConnection
概述
- 你需要将TCPSender和TCPReceiver组合成TCPConnection,并在TCPConnection处理一些连接管理的顶层逻辑
接收报文段
当TCPConnection的segment_received方法被调用时,它会从Internet接收TCPSegments。发生这种情况时,TCPConnection将查看该段并:
- 如果设置了RST(重置)标志,将入站和出站的流都设置为错误状态并永久终止连接。否则……
- 将报文段提供给TCPReceiver,以便它可以检查其关注的字段:seqno、SYN、payload和FIN
- 如果ACK标志已设置,告诉TCPSender它关心的字段:ackno和接收窗口大小。
如果传入的段占用任何序列号,则TCPConnection确保至少发送一个段作为答复,以反映ackno和窗口大小的更新。
发送报文段
TCPSender在将段推入其传出队列前,需要设置字段:seqno、SYN、payload和FIN
在发送分段之前,TCPConnection会向TCPReceiver询问其在传出分段上负责的字段:ackno和窗口大小。如果有确认,它将设置ACK标志和TCPSegment中的相关字段。
When time passes
TCPConnection具有一个tick方法,该方法将由操作系统定期调用。发生这种情况时,TCPConnection需要:
TCPSegment填入的字段如下图所示
- 蓝色是TCPSender处理得到的
- 红色是TCPReceiver处理得到的

- TCPConnection的完整接口是在课程文档中。请花一些时间阅读此内容
- 您的大部分实现将涉及“wiring up”TCPConnection的公共API到TCPSender和TCPReceiver中的适当例程
- 您要尽可能地将所有繁重的工作推迟到已经实现的发送方和接收方
- 就是说,并非所有事情都那么简单,并且有些微妙之处涉及整个连接的“global”行为
最困难的部分是决定何时完全终止TCPConnection并声明它不再“active”
FAQs和特殊情况
期望的代码行数(在tcp_connection.cc中):100~150行
- 测试套件将广泛测试您与自己的实现以及Linux内核的TCP实现的互操作性
- 应该如何开始?
- 最好先将一些“常规”方法连接到TCPSender和TCPReceiver中的相应调用。这可能包括remaining_outbound_capacity(), bytes_in_flight(), unassembled_bytes()
- 然后可以实现”writer”方法:connect(),write()和end_input_stream()。某些方法可能需要对outbound ByteStream(TCPSender持有的)进行一些处理,并告知TCPSender
- 在你完成部分实现后,可以先运行make check,执行测试套件,通过输出的错误信息了解到下一步该干什么
- 应用程序如何从inbound stream中读取数据?
- TCPConnection: : inbound_stream () 已经在头文件中实现,你不需要自己实现
- TCPConnection是否需要任何巧妙复杂的数据结构或算法?
- 不需要,所有繁重的工作都已经在TCPSender和TCPReceiver完成
- 此处的工作实际上只是将所有内容连接起来,并处理一些连接层面的微妙问题
- TCPConnection实际如何发送段?
- 和TCPSender类似
- Push it on to the
_segments_outqueue - 你只要认为一旦你将segment放入这个队列,这个队列的owner就会过来取走这个segment (using the public segments_out () accessor method) 然后发送到网络上
- TCPConnection如何了解时间的流逝?
- 与TCPSender相似
- tick()方法将被定期调用。请不要使用任何其他方式来表示时间
- tick方法是您唯一可以访问时间的方法。这使事情具有确定性和可测试性。
- 如果传入段具有RST标志,则TCPConnection应该做什么?
- RST标志(“重置”)表示连接立即死亡
- 您应该在入站和出站ByteStream上设置错误标志
- 保证随后对TCPConnection::active()都应该返回false
- 什么时候应该发送设置了RST标志的段?
- 在两种情况下,您需要中止整个连接:
- 1.如果发送方发送了太多的连续重传而没有成功(超过TCPConfig::MAX_RETX_ATTEMPTS)
- 2.如果在连接仍处于active状态时(active()返回true)调用了TCPConnection析构函数
- 发送RST标志的段和接收到RST标志的段有相同的副作用
- 连接立即死亡
- 在入站和出站ByteStream上设置错误标志
- 保证随后对TCPConnection::active()都应该返回false
- 在两种情况下,您需要中止整个连接:
- 应该如何创建1个带有RST标志的段?
- 任何一个outgoing segment都需要带有合适的sequence number
- 你可以强制TCPSender生成一个带有合适sequence number的空报文段,通过调用**send_empty_segment ()方法**
- Or you can make it fill the window (generating segments if it has outstanding information to send, e.g. bytes from the stream or SYN/FIN) by calling its fill_window() method(意思应该是在数据报文段中携带RST标志?)
- ACK标志的目的是什么?每个报文段都需要有ackno吗?
- 几乎所有的报文段都会带有ACK标志和ackno,除了在连接的最开始阶段,接收方没有任何发送方的数据需要确认
- 在发送报文段时,只要调用TCPReceiver::ackno()的返回值std::optional
的has_value()是true,就设置ACK标志和ackno - 在接收报文段时,当ACK标志被设置,就需要取出ackno和窗口大小给TCPSender
- 如何理解这些状态的名称,例如“stream started”、“stream ongoing”
- 请参阅实验2和实验3讲义中的图表。
- 我们要再次强调,“状态”对于测试和调试很有用,但我们并不是要您在代码中实现这些状态。您无需制作更多状态变量来跟踪此情况。“状态”隐式的表示为您的模块已经公开的公共接口的功能。
- What window size should I send if the TCPReceiver wants to advertise a window size that’s bigger than will fit in the TCPSegment: : header(). win field?
- Send the biggest value you can. You might find the std: :numericlimits class helpful.
- 这里的意思是TCP首部的windows size字段是32位的,如果接收方实际能提供的窗口大小比这个还大怎么办?那也只能填32位无符号数的最大值吧?
TCP连接何时最终“done”?active()什么时候可以返回false?
- 请参阅下一节。
- 更多的FAQs:https://cs144.github.io/lab_faq.html
TCP连接的结束
TCPConnection的一项重要功能是确定何时TCP连接完全“done”
- 发生这种情况时,将释放绑定的本地端口号,停止发送确认以响应传入的段,将连接视为历史记录,并使其active()方法返回false
- 连接可以通过两种方式结束。
- 在不正常关闭中,TCPConnection发送或接收带RST标志的报文段
- 在入站和出站ByteStream上设置错误标志
- 保证随后对TCPConnection::active()都应该返回false
- 正常的关闭
- 使得TCPConnection::active()返回false,且不带错误
- 这比较复杂,因为需要尽可能确保两端的outgoing ByteStream中的每一个已可靠地完全交付给接收对等方(参考后面的总结,你可以跳过下面总结前的部分)
- 在不正常关闭中,TCPConnection发送或接收带RST标志的报文段
- 如两军问题的描述,无法完全保证两个对等方都可以实现正常的关闭,但是TCP可以使其非常接近这种理想状态。
- 从其中一个对等点(一个TCPConnection,我们称为“local”peer)的角度来看,与“remote”peer的连接完全关闭有四个先决条件:
- 先决条件#1:入站流已完全组装并结束。
- 先决条件#2:出站流已由本地应用程序结束并已完全发送(包括它结束的说明,即a segment with FIN)到远程对等方。
- 先决条件3:出站流已被远程对等方完全确认。
- 先决条件#4:本地TCPConnection确信远程对等方可以满足先决条件#3。(也就是说我发送的ACK已经完全被对方接收,对方不会再重传报文段)
- 有两种情况
- A:lingering after both streams end(这里指的是接收方主动发起关闭连接)
- 先决条件1至3是正确的,并且远程对等方似乎已经获得了本地对等方对整个流的确认。本地对等方不确定这一点(TCP不会传送确认的确认)。但是,本地对等方非常有信心远程对等方得到了确认,因为远程对等方似乎没有在重传任何内容,并且本地对等方已经等待了一段时间才能确定。
- 具体而言,当先决条件#1至#3满足,且离本地对等方从远程对等方接收到任意段的最近时间已经过去了初始重传超时(-cfg.rt_timeout)的10倍时,视为连接“done”
- 在两个流都结束之后,这被称为“lingering”,以确保远程对等方不会尝试重新传输我们需要确认的任何内容。
- 这确实意味着TCPConnection需要保持一段时间【1】,即使在TCPSender和TCPReceiver的工作完全完成并且两个流都已结束之后,仍保留对本地端口号的排他性要求,并可能响应进入的段而发送ack。
- B:被动关闭(passive close)(这里指的是发送方主动发起关闭连接)
- 先决条件1至3是正确的,并且本地对等方100%确定远程对等方可以满足先决条件3。
- TCP并不会传送确认的确认,这是怎么做到的?因为TCP连接的关闭是远程对等方发起的
- A:lingering after both streams end(这里指的是接收方主动发起关闭连接)
【1】
在现实的TCP实现中,the linger timer(也称为TIME-WAIT timer或者说是最大段生存时间(Maximum Segment LifeTime,MSL)的两倍)通常约为60或120秒。有效地完成连接后,保留端口号可能会很长时间,尤其是如果您要启动绑定到相同端口号的新服务器时,没人会等待两分钟。SO_REUSEADDR socket选项可以使Linux忽略这个保留设置,以方便进行调试或测试
- TCP关闭连接的实践总结
- 您的TCPConnection具有一个名为_linger_after_streams_finish的成员变量,并通过state()方法暴露给测试程序。
- 这个变量初始化为true。如果入站流在**TCPConnection的**出站流读到EOF之前结束(意思就是对方先发送FIN),则此变量需要设置为false
- 在满足先决条件#1到#3的任何时候,如果**_linger_after_streams_finish*为false,则连接“done”(并且active()应返回false)。否则,您需要linger:仅在自收到最后一个段以来经过足够的时间(10_cfg.rt_timeout)之后连接才“done”
- 您的TCPConnection具有一个名为_linger_after_streams_finish的成员变量,并通过state()方法暴露给测试程序。
- 为什么这种规则是有效的?这是一个脑筋急转弯,你无需继续阅读下去就能完成本实验
测试
除了我们已经准备好的自动测试样例,我们鼓励你玩一玩自己的TCP实现
- 打开一个终端,运行
./apps/tcp_udp -l 127.0.0.1 9090- 这将运行你的TCPConnection,通过UDP监听9090端口,报文段将传送进来
- 你可以看到下图

- 打开另一个终端,运行
./apps/tcp_udp 127.0.0.1 9090- 这将运行你的TCPConnection,作为客户端连接指定的服务器
- 此时在前一个服务器终端可以看到

- 在当前客户终端可以看到

- 此时在任意一个窗口输入字符串并回车,在另一个窗口可以看到一样的字符串
- 在客户端,CTRL+D,结束客户端的outbound stream,可以看到下图

- 在服务终端可以看到下图

- 在服务端,CTRL+D,服务器会马上输出如下内容(without lingering),并且退回到CMD

- 在客户端可以看到如下输出(with lingering)

- 10秒的lingering后,客户端会有如下输出,并且退回到CMD

- 如果这些步骤之一出错了,那么说明termination逻辑有问题:the decision about when to stop reporting active()=true
用一个很小的窗口进行测试
在你完成TCP的实现,并运行make check通过全部测试样例后,请确保使用git commit
- 然后测试你的实现的性能,确保至少达到100Mbps
- 在build目录下,运行
./apps/tcp_benchmark,如果一切正常,你将看到如下输出

要求两行上的性能至少应为“0.10 Gbit/s”(每秒100兆比特)。您可能需要分析代码或速度缓慢的原因,并且可能必须改善某些关键模块(例如Bytestream或StreamReassembler)的实现。
重写webget
Lab0使用Linux内核提供的TCPSocket编写了webget.cc,现在使用你的TCP实现做出如下改动

- 为什么要添加socket.wait_until_closed()?
- 通常,即使在用户进程退出后,Linux内核仍会等待TCP连接达到“干净关闭”状态(随后才放弃其端口保留)
- 但是,因为您的TCP实现全部在用户空间中,所以除了您的程序外,没有其他任何方法可以跟踪连接状态。
- 添加此调用会使套接字等待,直到TCPConnection报告active()=false
- 重新编译,运行
make check_webget确保测试通过 - 如果你碰到一些问题,试着运行
./app/webget cs144.keithw.org /hasher/xyzzy,你将得到一些可能有用的debugging输出开发和调试建议
- 在tcp_connection.cc中实现TCPConnection的公有接口,你可以在tcp_connection.hh中添加任何需要的私有成员
- 大约需要100~150行代码,不需要任何精妙的数据结构或算法
- 你可以在编译代码后运行指令make check进行测试
- 将运行全部的测试套件(159个cases)

- 注意git的使用

- 注意代码的可读性
- 使用现代化的C++语法风格
- 如果产生一个segmentation fault
提交注意事项
- 仅修改libsponge顶级目录下的.hh和.cc文件,可以为对象添加私有成员,但不要修改公有接口
- 在提交前,请按顺序运行
- make format
- git status(确保commit了所有的改动)
- make
- make check(应该是make check_lab4吧?)
- 编辑writeups/lab4.md,文档中需要包含
- 程序设计结构
- 实现过程中遇到的挑战
- 未解决的BUG
- 提交指南:https://cs144.github.io/submit


