2024-CS144-lab3 TCP_Sended的分析与实现

lab3-TCP sender

在上个实验中,我们实现了TCP的接收方,在这次实验中我们将实现TCP发生方。由于TCP协议是全双工通信,要求通信实体两端都可以各自发送数据报或者接收数据报:发送方负责发送带有数据报的分组,收到接收方的ack确认报文以及超时重传以确保TCP协议的可靠性,接收方负责接收可能不按序到达的分组并且将其推入重组器重组,同时,接收方收到信号还需要向发送方发送一个确认分组。

通过阅读文档,我们可以知道TCP-sender需要满足的核心功能:

  • 记录接收端发送给它的接收窗口大小_(从TCPReceiverMessages)_中获取,用于做拥塞控制
  • 在窗口大小的允许下持续从ByteStream中读取数据,在必要的时候添加SYNFIN标记(在连接开始时与连接结束时携带)
  • 追踪哪些以及发送过的数据报是没有收到接收方的确认的,这部分报文可能需要超时重传
  • 没有收到确认的报文在足够长的时间后依然没有得到确认,发送方有责任将其重传

文档中将TCPsend需要处理的事情和特殊情况总结的很到位了,我们先慢慢理清楚思路以及讨论特殊情况

请注意,TCP发送方在发送第一个数据报时SYN一定为true,如果发送方一直没有收到接收方对这个携带SYN的特殊报文的确认,那么发送方发送的后续报文要一直携带SYN标志,知道收到接收方的确认,同理,FIN标志也是一样的。

请注意TCPSendertick 方法每隔几毫秒会被调用一次,参数表示自上次调用以来经过了多少毫秒。你需要用它来维护 TCPSender 存活的总时间。不能调用任何操作系统或 CPU 的时间函数 ,所有时间相关的信息只能通过 tick 方法获取,这样可以保证程序是可测试且确定性的 。所以在构造TCPSender 时,会传入一个初始重传超时时间(RTO)参数,表示重传未确认 TCP 段之前等待的毫秒数。这个 RTO 会随时间变化,但初始值始终不变,代码中会将其保存在一个名为 initial_RTO_ms 的成员变量中。

每当发送一个包含数据的段(即序列空间长度非零的段,不论是首次发送还是重传),如果定时器未在运行 ,就启动定时器,使其在 RTO 毫秒后过期。一旦所有未确认的数据都被确认 (即没有 outstanding segment),就停止定时器

如果在调用tick方法时,重传定时器已过期,那么我们需要重新发送最早的,尚未被完全确认的分组,(即序列号最小的未确认段),我们需要设计一个数据结构,以便快速找到它,这里我们需要利用先进先出的特性,而且只需要找到最早的未确认分组,很容易想到使用队列进行存储,内部元素类型就是发送出去的报文数据 TCPSenderMessage。如果窗口大小不为0,那么我们需要重传报文并且将RTO的值设置为原来的两倍,这里设置为原来的两倍是课程要求,然后重置计数器,使得记录的时间复位为0。

当发送方收到接收方的确认时,我们需要将计时器的RTO重置为默认值,如果sender还有未完成的数据,重启计时器使其工作,重置重传计数器。

下面我们根据框架代码对要求实现的函数进行分析。

push方法中,我们需要不断从流中读取字节,直到填满发送窗口、或是填充的数据使得当前报文的负载长度达到了 TCPConfig 中规定的上限。所以push必须尽快将所有缓存的数据全部发送出去,利用滑动窗口机制,可以同时发送多个分组。也就是说我们需要不断地从流中读取字节并组装报文,当读取的数据达到了报文段长度限制后马上把这个报文发送出去,再继续执行读取-组装工作 ,直到流中没有更多数据、或者累计发送出去的字节数达到了接收方的窗口大小。请注意 SYN 和 FIN 字节也要被计入字节序号当中。故窗口大小不足、并且还需要发出 FIN 时,必须把 FIN 字节的发送推迟到下次报文发送。

注意下面的 FAQ 中提及了一个特殊行为:当接收方告知其窗口大小为 0 时,我们需要假装窗口大小为 1,并依然正常发送报文过去,避免陷入死锁。

receive()方法负责根据接收方发送来的确认报文,如果ackno的值大于缓冲区队首报文段中的所有字节序号,也就是说只有队首的报文被全部确认后,才能把这个报文弹出缓冲区。在这个使用中我们不需要根据ackno的值截断队首的报文段,统一设计为该ackno大于该分组末尾的字节序号时才pop出去。如果新的确认报文将缓冲区全部清空了(全部数据都被确认),那么就要停止计时器。

tick()方法主要的点就是不要使用现实世界的时间,在这个方法中根据计时器是否过期判断是否需要重传数据。文档推荐我们将超时计时器设计为一个独立的类,所以我们应该在tick类中调用这个计时器类。

make_empty_message() 正如其名,创建并返回一个零长的报文。在TCPSenderMessage::sequence_length()方法中,我们可以知道一个零长报文有以下特征:SYNFIN都是false,数据报的负载payload是空的。

FAQS中还有一些额外的信息:

  1. TCPSenderMessage 中的 seqno 必须是当前 TCPSender 要发送的下一个字节序号。
  2. 刚开始连接时,假定接收方的窗口大小为 1。
  3. 不要裁剪缓冲区的字节数据,即使它们中的部分已经得到了确认。
  4. 同样的,即使缓冲区中有多个完整且互相可以连接在一起的字节数据,也没有必要将它们拼在一起。
  5. 如果发送了零长报文(make_empty_message() ),不要重传它,也不要启动计时器。

接下来我们总结一下我们需要在各个待完成的函数中要做的事情

push方法

不断的组装报文,并且需要满足每个报文的负载payload长度不大于TCPConfig::MAX_PAYLOAD_SIZE,没有收到确认的字节数量不能超过接收方告知的窗口大小。需要将已经发送但是还未收到确认的字节计入待确认的字节中,每次从 ByteStream 中读取字节后都要检查读端是否已经结束,因为这是发送FIN的标志。当 FIN 已发出、或者 ByteStream 中没有数据、亦或者发出的数据已经填满了滑动窗口,拒绝发出任何报文。还要记得在发送长度非0的报文时启动超时计时器(如果没有启动的话)

receive()方法

要根据接收方发送的信息更新当前发送窗口的大小,这是为了与接收端协调,做拥塞控制。报文中的 ackno 不大于缓冲区队首的首字节序号 seqno + payload.size() 的值时,跳过更新缓冲区。检查 SYN 连接请求是否被确认,并根据检查结果设置以后发出的报文中 SYN 的值(测试用例要求)。只要缓冲区有报文被弹出,就将重传计时器置零。

tick方法

ms_since_last_tick 的值加到计时器上。每次更新计时器时,要检查计时器是否已经过期。如果过期的话就重传缓冲区队首元素,递增重传计数器,并将计时器的 RTO 增加为原来的两倍。

下面开始讲解代码的实现:

首先我们按照文档的要求,先设计一个代表超时重传计时器的类出来,然后在TCPsender方法中嵌入这个类的字段表示计时器,再在tick方法调用这个类的方法实现。

C++ 复制代码
class ReTreansmitTimer
{
public:
  explicit ReTreansmitTimer( uint64_t init_rto_time ) : RTO_( init_rto_time ) {};
  bool is_expired() const { return is_open_ && allTime_passed_ >= RTO_; }
  bool is_open() const { return is_open_; }
  // 激活一个分组的计时器,返回引用可以支持链式调用
  ReTreansmitTimer& open();
  // 将超时重传时间变为两倍
  ReTreansmitTimer& timeout();
  // 重置当前计时器
  ReTreansmitTimer& reset();
  // 当前计时器经过了多少时间
  ReTreansmitTimer& tick( uint64_t ms_since_last_tick );

private:
  uint64_t RTO_ {};            // 超时重传时间
  uint64_t allTime_passed_ {}; // 计时器启动之后所经过的总时间
  bool is_open_ { false };     // 定时器是否打开
};

在设计这个类时我们需要实现的计时器操作有,打开计时器、将超时重传时间设置未原来的两倍、重置当前计时器以及当前计时器从激活到现在经过了多少时间。注意这里的函数返回引用是为了进行链式调用,例如timer_.tick( ms_since_last_tick ).is_expired()

接下来是我额外在TCPsender中补充的字段:

C++ 复制代码
TCPSenderMessage make_message( uint64_t seq, bool syn, std::string payload, bool fin ) const;

  ByteStream input_;
  Wrap32 isn_;
  uint64_t initial_RTO_ms_;

  uint16_t window_size_ { 1 };
  bool zero_window_ {}; // 表示发送方拥塞窗口大小是不是为0,如果为0的话就不加倍超时重传时间
  uint64_t acked_seq_ { 1 };                // 表示发送方收到接收方想要的下一个数据的序列号
  uint64_t seq_num_in_flight_ {};           // 表示已经发送但是未被确认的数据的数目
  uint64_t consecutive_retransmissions_ {}; // 表示超时重发的次数
  uint64_t seq_num_ {}; // 表示发送方将要发送的下一个字符,也可使用queue.front()
  // 用于存储发送数据报的队列,利用先进先出的特性
  std::queue<TCPSenderMessage> outstanding_segment_ {};

  ReTreansmitTimer timer_ { initial_RTO_ms_ };
  /*设置四个标志位,前两个是表示在连接过程中已经确定的状态位,后两个表示是否发送过SYN_和FIN_
  用与应对特殊情况*/
  bool SYN_ { false }, FIN_ { false }, send_FIN_ { false }, send_SYN_ { false };

其中最基本的成员有std::queue<TCPSenderMessage> outstanding_segment_ {};,这个队列存放了我们发送出去的TCP数据报,这些数据报还未收到确认,在push方法中向队列推入消息,在receive方法中根据接收到的确认号不断的从队列中弹出消息。seq_num_in_flight_consecutive_retransmissions_变量是为了在方法

C++ 复制代码
uint64_t sequence_numbers_in_flight() const;  // For testing: how many sequence numbers are outstanding?
  uint64_t consecutive_retransmissions() const; // For testing: how many consecutive retransmissions have happened?

中调用返回的变量,在pushreceive方法中进行更新。timer_变量就是在上一个类中编写的超时计时器,在push方法中我们发送对应信息时要开启一个总的超时计时器,每次调用tick方法时,我们会根据定时器是否过期来选择重传队首的数据,将重传次数++,然后加倍计时器的超时时间。window_size_表示发送窗口的大小,可以使用这个变量协调拥塞控制,在push方法中,发送数据时要检测窗口大小是否允许,在receive方法中,在接收数据时还要根据接收方的窗口大小调整自己的窗口大小,来做拥塞控制。

ack_no_seq_num_变量就是发送方和接收方用来协调接收与发送进度的工具。注意我还添加了一个方法TCPSenderMessage make_message( uint64_t seq, bool syn, std::string payload, bool fin ) const;,这提供了一层抽象,让我们更方便地构造要发送的数据分组,还可以更快捷的实现make_empty_message()方法,代码可读性与可维护性更强。

剩下的一些变量是我在进行测试时发现有一些特殊情况添加的,这里也鼓励大家多看测试代码,因为测试样例很多,可以面向测试用例编程......

这里是我的代码的完整实现,下面是测试用例通过情况:

相关推荐
zkmall15 分钟前
HikariCP 源码核心设计解析与 ZKmall开源商城场景调优实践
spring boot·开源
FIT2CLOUD飞致云44 分钟前
全面支持MCP协议,开启便捷连接之旅,MaxKB知识库问答系统v1.10.3 LTS版本发布
人工智能·开源
奋斗者1号3 小时前
嵌入式AI开源生态指南:从框架到应用的全面解析
人工智能·开源
OpenTiny社区4 小时前
TinyPro 中后台管理系统使用指南——让页面搭建变得如此简单!
前端·vue.js·开源
蚝油菜花6 小时前
清华联合DeepSeek推出奖励模型新标杆!DeepSeek-GRM:让AI学会自我批评,推理性能越跑越强
人工智能·开源
猿小猴子8 小时前
在 Ubuntu24.04 LTS 上 Docker Compose 部署基于 Dify 重构二开的开源项目 Dify-Plus
docker·重构·开源
小华同学ai9 小时前
25.9K star!AI一键生成高清短视频,这个开源神器让内容创作起飞!
人工智能·开源·github
LaughingZhu9 小时前
PH热榜 | 2025-04-03
前端·数据库·人工智能·经验分享·mysql·开源·产品运营