在实现了接收端的 TCPReceiver 类和发送端的 TCPSender 类后,本节实验实现掌管这两个类,向用户提供一个完整的 TCP 连接抽象的 TCPConnection 类。正如讲义标题所写,完成本节的实验后,我们是真正攀登上了一个高峰(summit),也是完成了本门课程最重要精华的部分。
事前提醒,本节的测试量非常大,其中会用你的 TCP 实现与 Linux 的 TCP 实现进行各种条件的交互,之前的 Sender 和 Receiver 类在实现上如果有潜在问题也都会被暴露出来。所以,如果你在尝试自己实现,请做好耐心 Debug 的准备。虽然直接看其它人的 blog 可能感觉逻辑都很自然,但自己动手是完全不一样的。我当时也花了 2 整天左右才通过了所有测试。
TCP 是全双工通信,即双方可以同时发送/接收信息。如图所示,TCPConnection 类应该是架设在 TCPSender 和 TCPReceiver 之上的。当 Sender 需要发送数据包时,从其发送队列 _segments_out
中取出数据包并实际发送出去;当接收到数据包时,做必要处理后交给 Receiver 处理。另外,之前我们一直没有关注 TCP 包头中很重要的一个标识 RST,这也是 TCPConnection 要负责的,即异常状态的处理。最后,TCPConnection 还应该处理好连接的终止,包括正常终止和异常终止两种情况。以下我们分别来讨论这几个问题以及实现思路。
注:这里发送的实现其实还是将数据包从 Sender 取出转存到 TCPConnection 的一个发送队列中,之后会由更上层的所有者实际负责发送
接收数据包
对应函数:void segment_received(const TCPSegment &seg)
。
- 如果 RST 标识存在,立刻将出入向的数据流均设置为错误状态,并结束连接。可以将结束连接提炼为一个
_shutdown(bool)
函数,参数代表是正常还是异常结束,这里即为异常结束。否则: - 将数据包交给 Receiver 处理,即调用 Receiver 的
segment_received
。 - 如果 ACK 标识存在,通知 Sender 其关注的信息(ackno 和 window size),即调用 Sender 的
ack_received
。 - 如果数据包有序列号,至少保证有一个回复数据包被回复,即应该检查 Sender 的发送队列是否为空,如果为空则造一个空包。
- “keep-alive” 包的回复:对方可能发送一个包含无效序列号的数据包以探测连接是否仍有效,此时应回复一个空包。实现方法讲义已给出。
发送数据包
- Sender 构造的待发送包还不完整,ackno 和 window size 信息应该由 Receiver 给出(如果存在 ackno,还要设置 ACK 标识)。
- 任何时刻 Sender 向发送队列中 push 了一个包,TCPConnection 都应该立刻将其发送。因此,TCPConnection 应该实现一个清空 Sender 队列,填充 Receiver 信息并发送的函数(我的实现中命名为
_clear_sendbuf
),在任何可能使 Sender 发送数据包的语句之后都调用该函数。
时间的感知
与 Sender 相同,TCPConnection 也是通过其 tick
函数被调用而被动的感知时间的经过。当 tick
被调用时:
- 使 Sender
tick
。 - 如果 Sender 的重传次数超过某阈值,终止连接并发送一个带 RST 标识的空包。终止连接与上面收到对方 RST 包的处理一样,即调用
_shutdown(false)
。 - 如果时机合适,正常结束连接(见下一节)。
连接的终止
以上已经讨论过连接异常终止的两种情况,即主动检测到重传次数过多或被动收到对方的 RST 包,这里主要讨论的是连接的正常终止。
理论上来说,通过不可信连接通信的双方无法绝对确保达成共识(参考:两军问题),不过 TCP 设计了一种可信度较高的方案。以一方(local 方)的观点来看,至少有四件事成立才能正常终止连接:
- 入向流(inbound)的数据已经完全接收重组完毕并结束。
- 出向流(outbound)的数据已经完全发送并且带 FIN 标识的包也已被发送。
- 出向流被对方完全确认接收。
- local 方相信 remote 方能够满足第三条。这是最难解决的,因为 TCP 不会对 ack 信息做 ack,否则就没有尽头了。于是有两种选项:
- 延迟关闭(lingering)。当前三条都成立且经过了一定时间后 local 方都没有收到 remote 方重传任何数据,则 local 方能够比较确信双方的所有数据已经正确传输完毕并关闭连接。具体来说,这个时间设置为初始重传超时的 10 倍。
- 被动关闭。如果入向流的结束早于出向流含 FIN 标识包的发送,则 local 方在出向流也结束后立刻关闭连接。其中的原因讲义中给出了解释,这里就不展开了。
测试
除了运行自动化测试外,本节完成后已经可以用两个自己的 TCP 端进行通信,以及用自己的 TCP 代替 Lab0 中的 TCPSocket 与真实的服务器进行通信,还可以测试自己 TCP 实现的性能,操作方法讲义中都有描述。
完整代码链接:
tcp_connection.hh
tcp_connection.cc
通关截图 😎