计网实验(三)CS144 Lab3

文章目录

0. 任务书说明

目标: 实现 TCPSender,TCP 连接的另一半,负责将出站字节流转换为报文段。

概述 (0 Overview):

  • 回顾:
    • 检查点 0: ByteStream
    • 检查点 1 & 2: ReassemblerTCPReceiver,用于将不可靠数据报中的报文段转换为入站字节流。
  • 本次任务: 实现 TCPSender
  • 最终目标 (检查点 4): 将之前的工作组合成一个完整的 TCP 实现 (TCPPeer),包含 TCPSenderTCPReceiver

开始 (1 Getting started):

  • 与之前检查点类似,获取启动代码。

检查点 3: TCP 发送方 (2 Checkpoint 3: The TCP Sender):

  • TCPSender 的职责:
    • 跟踪接收方的窗口(通过接收传入的 TCPReceiverMessage 中的 acknowindow sizes)。
    • 尽可能填充窗口:从 ByteStream 读取数据,创建新的 TCP 报文段(如果需要,包括 SYN 和 FIN 标志),并发送它们。应持续发送报文段,直到窗口已满或出站 ByteStream 没有更多数据。
    • 跟踪已发送但尚未被接收方确认的报文段(称为“未完成”outstanding 报文段)。
    • 如果自发送以来经过了足够长的时间且未完成报文段仍未被确认,则重新发送它们。
  • 基本原理 (ARQ - Automatic Repeat Request): 发送接收方允许发送的内容(填充窗口),并持续重传直到接收方确认每个报文段。

2.1 TCPSender 如何知道报文段丢失了? (How does the TCPSender know if a segment was lost?)

  • TCPSender 发送 TCPSenderMessage
  • TCPSender 必须跟踪其未完成的报文段,直到它们占用的序号被完全确认。
  • tick 方法会被周期性调用,指示时间的流逝。TCPSender 负责检查其未完成报文段的集合,并判断最早发送的报文段是否“未完成时间过长”而未被确认。如果是,则需要重传。
  • “未完成时间过长”的规则:
    1. tick 方法会带一个参数,告知自上次调用以来经过了多少毫秒。用此维护 TCPSender 已存活的总毫秒数。不要调用操作系统的时间函数。
    2. 构造 TCPSender 时,会给定一个重传超时 (RTO) 的“初始值”。RTO 是重发未完成 TCP 报文段前等待的毫秒数。RTO 的值会随时间变化,但“初始值”不变 (保存在 initial_RTO_ms_)。
    3. 实现重传计时器 (retransmission timer): 一个可以在特定时间启动的警报,当 RTO 经过后会“到期”。时间流逝的概念来自 tick 方法的调用。
    4. 每次发送包含数据(序号空间长度非零)的报文段时(无论是首次发送还是重传),如果计时器未运行,则启动它,使其在 RTO 毫秒后到期(使用当前的 RTO 值)。
    5. 当所有未完成数据都被确认后,停止重传计时器。
    6. 如果调用 tick 且重传计时器已到期:
      • (a) 重传最早(最低序号)未被 TCP 接收方完全确认的报文段。
      • (b) 如果窗口大小非零:
        • i. 跟踪连续重传次数,并递增它。TCPConnection 将使用此信息判断连接是否无望。
        • ii. 将 RTO 的值加倍 (指数退避 exponential backoff),以减缓在糟糕网络上的重传。
      • © 重置重传计时器并启动它,使其在 RTO 毫秒后到期(考虑到 RTO 可能刚刚翻倍)。
    7. 当接收方给发送方一个 ackno,确认了新数据的成功接收时:
      • (a) 将 RTO 重置为其“初始值”。
      • (b) 如果发送方仍有未完成数据,则重新启动重传计时器,使其在 RTO 毫秒后到期(使用当前的 RTO 值)。
      • © 将“连续重传次数”重置为零。

2.2 实现 TCP 发送方 (Implementing the TCP sender):

  • 四个需要处理的重要事件/方法:
    1. void push(const TransmitFunction& transmit):
      • 被要求从出站字节流填充窗口。
      • 尽可能多地读取流并发送 TCPSenderMessage,只要有新字节可读且窗口中有可用空间。
      • 通过调用提供的 transmit() 函数发送。
      • 确保每个发送的 TCPSenderMessage 完全适合接收方窗口。
      • 使每个单独的报文尽可能大,但不超过 TCPConfig::MAX_PAYLOAD_SIZE
      • 使用 TCPSenderMessage::sequence_length() 计算报文段占用的序号总数(SYN 和 FIN 标志各占一个序号)。
      • 窗口大小为零的特殊情况: 如果接收方通告窗口大小为零,push 方法应假装窗口大小为一。发送方可能最终发送一个被拒绝的单字节,但这也可以促使接收方发送新的确认报文段,显示其窗口中已打开更多空间。这是唯一的特殊情况处理。
    2. void receive(const TCPReceiverMessage& msg):
      • 从接收方收到消息,传达窗口的新左边缘 (ackno) 和右边缘 (ackno + window size)。
      • TCPSender 应检查其未完成报文段的集合,并移除任何已被完全确认的报文段(即 ackno 大于报文段中的所有序号)。
    3. void tick(uint64_t ms_since_last_tick, const TransmitFunction& transmit):
      • 时间流逝。可能需要重传未完成报文段。
    4. TCPSenderMessage make_empty_message() const:
      • 生成并发送一个序号设置正确的零长度报文。
      • 用于对等方需要发送 TCPReceiverMessage(例如确认)但需要附带一个 TCPSenderMessage 的情况。
      • 这样的空报文段不占用序号,不需要作为“未完成”进行跟踪,也不会被重传。

2.3 常见问题和特殊情况 (FAQs and special cases):

  • TCPSenderreceive 方法通知前应假设接收方窗口大小为多少? 一。
  • 如果确认仅部分确认了某个未完成报文段怎么办? 简单处理:将每个报文段视为完全未完成,直到它被完全确认。
  • 如果发送了三个包含 “a”, “b”, “c” 的独立报文段且它们都未被确认,之后能用一个包含 “abc” 的大报文段重传吗? 简单处理:单独跟踪每个未完成报文段,当重传计时器到期时,再次发送最早的未完成报文段。
  • 是否应在“未完成”数据结构中存储空报文段并重传? 否。只有那些传递数据(即在序号空间中消耗一定长度)的报文段才应被跟踪并可能重传。

1. 前置知识

1.1 TCP Segment Structure (概念层面)

  • Sequence Numbers (序列号): 理解字节流如何被分割成段,并且每个字节在流中都有一个唯一的序列号。SYN 和 FIN 标志各自消耗一个序列号。
  • Acknowledgment Numbers (确认号, ackno): 接收方用 ackno 来告知发送方它期望接收的下一个字节的序列号。它隐含地确认了所有在 ackno 之前的字节都已成功接收。
  • Window Size (窗口大小): 接收方通告给发送方的可用缓冲区大小,表示发送方在收到下一个确认前可以发送多少字节的数据。这是流量控制的核心。
  • SYN Flag: 用于建立连接的第一个包,消耗一个序列号。
  • FIN Flag: 用于关闭连接的一个方向,消耗一个序列号。
  • Payload: 段中实际携带的应用数据。

1.2 TCP Sender Responsibilities (发送方职责)

  • Segmentation (分段): 将来自应用层的 ByteStream 切分成适合网络传输的 TCP 段。
  • Sequence Number Assignment: 为每个段正确分配序列号。
  • Sending Data: 将段发送给网络层。
  • Managing the Send Window:
    • 跟踪已发送但未确认的数据量 (bytes in flight)。
    • 确保发送的数据量不超过接收方通告的窗口大小。
    • “Filling the window”: 在窗口允许且有数据可发的情况下,尽可能多地发送数据。
  • Retransmission Timeout (RTO) Management:
    • 为已发送的段启动计时器。
    • 如果超时,重传丢失的段。
  • Reliable Data Transfer: 通过序列号、确认和重传确保所有数据最终按序、无差错地到达接收方。

1.3 Retransmission Timer and RTO Calculation (Simplified Version for this Lab)

  • Initial RTO: 构造时给定的初始超时值。
  • Timer Start: 当发送包含数据的段(包括SYN/FIN)时,如果计时器未运行,则启动。
  • Timer Stop: 当所有已发送数据都被确认时。
  • Timeout Event:
    • 重传最早的未确认段。
    • Exponential Backoff (指数退避): 如果接收方窗口非零,将 RTO 加倍。
    • 增加连续重传计数。
    • 用新的 RTO 重启计时器。
  • New Acknowledgment Event:
    • 将 RTO 重置回初始值。
    • 重置连续重传计数。
    • 如果仍有未确认数据,用初始 RTO 重启计时器。

1.4 Flow Control (流量控制)

  • 理解接收方窗口 (rwnd) 的概念,以及发送方如何根据它来限制发送速率。
  • Zero Window Probing: 当接收方通告窗口为0时,发送方需要定期发送小的探测段(实验中要求假定窗口为1发送一个字节),以发现窗口何时重新打开。注意,这只是 push 方法的临时行为,不改变实际记录的窗口大小。

1.5 Connection Establishment (SYN related)

  • 第一个发送的段通常是 SYN 段。
  • SYN 段消耗一个序列号,需要被确认。
  • 发送方在发送第一个数据段(或空的SYN段)时会设置SYN标志。

1.6 Connection Termination (FIN related)

  • 当发送方的 ByteStreamclose()end_input() 时,发送方需要发送一个 FIN 段。
  • FIN 段消耗一个序列号,需要被确认。
  • 发送方在发送最后一个包含数据的段或者一个单独的FIN段时会设置FIN标志。

1.7 State Management (发送方状态)

  • Next Sequence Number to Send (next_seqno_): 下一个要分配给新数据的序列号。
  • Outstanding Segments: 维护一个已发送但尚未被确认的段的集合/队列。需要能够找到"最早的"未确认段进行重传。
  • Receiver’s Window Information: 记录从接收方收到的最新 acknowindow_size

1.8 Interaction with ByteStream

  • 如何从 ByteStream 中读取数据。
  • 如何知道 ByteStream 是否已关闭(以便发送FIN)。
  • 如何知道 ByteStream 是否为空。

1.9 Absolute vs. Relative Sequence Numbers

  • 虽然实验中可能使用绝对序列号(从0开始的 WrappingInt32),但要理解TCP的序列号是32位无符号整数,会回绕。实验库会处理回绕的比较。

1.10 “Bytes in Flight”

  • 指已经发送出去但尚未收到确认的字节数。这个值必须小于等于接收方的窗口大小。计算方式通常是 next_seqno_absolute - ackno_absolute (需要考虑SYN/FIN)。

1.11 Distinction between “Zero Window” and “Full Window”

  • Zero Window: 接收方通告的 window_size 为0。发送方不能发送新数据(除了零窗口探测)。
  • Full Window: 接收方通告的 window_size 大于0,但是 bytes_in_flight 已经等于或接近 window_size,导致发送方不能再发送新数据。

这个实验简化了的一些真实 TCP 的复杂性,例如:

  • Congestion Control (拥塞控制): 真实的 TCP 有复杂的拥塞控制算法 (慢启动、拥塞避免、快速重传、快速恢复)。这个实验中,主要的"退避"行为是通过 RTO 的指数退避来实现的,没有显式的拥塞窗口 (cwnd)。
  • SACK (Selective Acknowledgment): 真实 TCP 可以选择性确认收到的乱序段,帮助发送方更精确地知道哪些段丢失了。本实验不要求实现 SACK,段必须被完全(连续地)确认。
  • Fast Retransmit: 真实 TCP 在收到多个重复 ACK 后会触发快速重传,而不是等待 RTO 超时。本实验不要求。
  • Nagle’s Algorithm / Delayed ACKs: 一些优化传输小数据包的算法,本实验不涉及。

2. 指数退避

2.1 什么是指数退避?

指数退避是一种算法,用于在发生冲突或失败(在 TCP 中主要是指数据包丢失导致超时)后,逐渐增加重试操作之间的时间间隔。其核心思想是:如果第一次重试失败了,那么网络(或系统)可能正处于繁忙或不稳定的状态,立即再次重试很可能还会失败,并且会进一步加剧问题。因此,应该等待更长的时间再进行下一次尝试。每次连续失败后,等待时间以指数级增长(通常是翻倍)。

2.2 为什么 TCP 要使用指数退避?

在 TCP 通信中,当发送方发送一个数据段后,如果在 RTO (Retransmission Timeout) 时间内没有收到确认,发送方会认为该数据段丢失了。这种丢失可能由多种原因造成,其中一个主要原因是网络拥堵

  • 避免雪崩式拥堵 (Congestion Collapse): 如果没有指数退避,当网络开始拥堵并导致丢包时,多个发送方可能会同时超时并立即重传。这些重传的数据包会进一步涌入已经拥堵的网络,使得网络状况更加恶化,形成恶性循环,最终可能导致网络完全瘫痪。
  • 适应网络状况: 指数退避允许 TCP 发送方在检测到丢包时"退让",给网络一个恢复的机会。通过逐渐增加重传间隔,发送方能够更温和地探测网络容量,并逐步适应当前的网络条件。

2.3 在 CS144 Lab Checkpoint 3 中,指数退避的规则

根据文档 Section 2.1, point 6(b)ii:

  • 触发条件:

    1. tick 方法被调用。
    2. 重传计时器已过期。
    3. 发送方因此重传了一个段
    4. 并且,接收方的窗口大小(window_size)为非零
  • 动作:

    • 将当前 RTO 值加倍 (Double the value of RTO)
    • 这个操作通常在增加连续重传次数之后。

2.4 关键点和注意事项

  1. 只在实际重传且窗口非零时发生:

    • 如果计时器过期,但由于某种原因(例如,逻辑错误或特殊情况)没有实际重传任何段,则不应执行指数退避。
    • 非常重要:如果接收方通告的窗口大小为零 (window_size == 0),即使发生超时重传(通常是零窗口探测段),也不应该执行指数退避(即不加倍 RTO)。这是因为零窗口表示接收方暂时没有能力接收数据,这更多是接收方的问题,而不是网络拥堵。在这种情况下,发送方会定期发送小的探测段,如果对探测段的响应也超时,RTO 仍然按正常逻辑计时和处理,但不进行指数退避式的加倍。
      • 文档中明确写着 “If the window size is nonzero … Double the value of RTO”。
  2. 何时重置回初始 RTO:

    • 当发送方收到一个新的确认ackno 比之前收到的任何 ackno 都大)时,表明数据成功到达接收方,网络可能不再像之前那样拥堵。此时,当前 RTO 会被重置回 initial_RTO_ms_ (Section 2.1, point 7a)。这意味着指数退避的效果被清除了。
    • 同时,连续重传次数也会被重置为 0 (Section 2.1, point 7c)。
  3. 连续性: 指数退避是针对连续的超时重传。如果一次超时导致 RTO 加倍,然后下一次发送的数据成功被确认了,那么 RTO 就会重置。如果之后再次发生超时,RTO 又会从 initial_RTO_ms_ 开始,如果再次连续超时,才会再次加倍。

2.5 示例流程

假设 initial_RTO_ms_ = 100ms,接收方窗口大小 > 0。

  1. 发送段A,启动计时器(100ms)。
  2. 100ms后,段A超时。
    • 重传段A。
    • 连续重传次数变为1。
    • 当前 RTO 变为 100 * 2 = 200ms
    • 用新的 RTO (200ms) 重启计时器。
  3. 200ms后,段A再次超时。
    • 重传段A。
    • 连续重传次数变为2。
    • 当前 RTO 变为 200 * 2 = 400ms
    • 用新的 RTO (400ms) 重启计时器。
  4. 假设在接下来的400ms内,收到了对段A的确认(或者对段A之后的数据的确认,这意味着段A也被确认了)。
    • 当前 RTO 重置回 initial_RTO_ms_ (100ms)
    • 连续重传次数重置为0。
    • 如果还有未确认数据,用 100ms 重启计时器。

3. 关键状态

RTO 的核心思想是:发送方为每个发出的、需要确认的段启动一个计时器。如果在RTO时间内没有收到该段(或其覆盖的序列号)的确认,就认为该段丢失了,需要重传。

根据文档,RTO 的具体逻辑如下:

3.1 需要跟踪的几个关键状态

  1. initial_RTO_ms_: RTO 的初始值。在 TCPSender 构造时设定,并且在特定条件下会重置回这个值。
  2. 当前 RTO 值 (current_RTO_ms_): 实际用于计时器设置的 RTO 值。它会根据网络状况动态变化(主要是指数退避)。
  3. 重传计时器状态:
    • 是否正在运行?
    • 如果正在运行,它何时过期?(基于 TCPSender 内部累计的总毫秒数)
  4. 连续重传次数 (consecutive_retransmissions_count_): 用于判断连接是否 hopeless,以及配合指数退避。

3.2 RTO 逻辑的触发和行为

  1. 启动计时器 (Starting the Timer):

    • 条件: 当发送一个包含数据(即序列号长度大于0,包括SYN或FIN)的段时(无论是首次发送还是重传)。
    • 动作:
      • 如果计时器当前没有运行,则启动它。
      • 计时器将设置为在 current_RTO_ms_ 毫秒后过期。
    • (Section 2.1, point 4)
  2. 停止计时器 (Stopping the Timer):

    • 条件: 当所有已发送(outstanding)的数据都已经被确认。
    • 动作: 停止重传计时器。
    • (Section 2.1, point 5)
  3. 计时器过期 (Timer Expires - 在 tick 方法中检测):

    • 条件: tick 方法被调用,并且检测到重传计时器已经过期。
    • 动作:
      • a. 重传: 重传最早的(序列号最小的)那个尚未被完全确认的段。
      • b. 如果接收方窗口大小非零:
        • i. 增加连续重传次数: consecutive_retransmissions_count_ 加 1。
        • ii. RTO 指数退避 (Exponential Backoff): 将当前 RTO 值 (current_RTO_ms_) 加倍。这是为了在网络拥堵或丢包严重时减少发送频率。
      • c. 重置并重启计时器:
        • 使用更新后(可能已加倍)的 current_RTO_ms_ 来重新启动计时器。
    • (Section 2.1, point 6)
  4. 收到新的确认 (New Acknowledgment - 在 receive 方法中处理):

    • 条件: 收到一个 ackno,该 ackno 确认了新的数据(即,这个 ackno 比之前收到的任何 ackno 都要大)。
    • 动作:
      • a. RTO 重置: 将当前 RTO 值 (current_RTO_ms_) 重置回 initial_RTO_ms_
      • b. 如果仍有未确认的数据 (outstanding data): 重新启动重传计时器。此时计时器将使用刚刚重置为 initial_RTO_ms_ 的 RTO 值
      • c. 重置连续重传次数: 将 consecutive_retransmissions_count_ 重置为 0。
    • (Section 2.1, point 7)

3.3 总结和关键点

  • 时间来源tick() 方法是唯一的时间推进机制。计时器的"过期"是相对于 TCPSender 内部维护的总运行时间。
  • 数据段才启动计时器:只有发送了消耗序列号的段(包含SYN、有效载荷或FIN)才会启动或重启计时器。空ACK段的发送不影响RTO计时器。
  • 指数退避的目的:在丢包时,发送方会认为网络可能拥堵,通过加倍RTO来降低重传频率,避免进一步加剧拥堵。
  • 新ACK重置RTO:收到新的有效ACK表明网络状况尚可,之前的拥堵可能已经缓解,所以将RTO重置回初始值,并将连续重传计数清零。
  • "窗口大小非零"才加倍RTO:这是个重要细节。如果接收方窗口为0,发送方会进行零窗口探测(发送一个字节的段)。如果这种探测段超时,不应该执行指数退避。因为这更多是接收方处理能力问题,而不是网络拥堵。文档明确指出了这一点。
  • 最早的段:当超时发生时,总是重传飞行队列中序列号最小的那个未确认段。

4. 实现

4.1 RTO

image.png

4.2 核心组件

4.2.1 RTO状态变量
  • initial_RTO_ms_:初始超时值,在构造时设定,作为重置RTO的基准值
  • current_RTO_ms_:当前使用的超时值,会根据网络状况动态调整
  • timer_running_:标记计时器是否在运行
  • time_elapsed_:自计时器启动后经过的毫秒数
  • consecutive_retx_:连续重传计数,用于指数退避和判断连接是否应该放弃
4.2.2 未确认段队列
  • 队列按发送顺序存储段,最早发送的段在队首
  • 当计时器超时时,会重传队首的段(最早的未确认段)
  • 当收到确认消息时,从队首开始移除已经被确认的段

4.3 工作流程

4.3.1 push()方法 - 发送数据

目的: 从ByteStream读取数据并发送尽可能多的段,填充接收方的窗口

关键步骤:

  • 检查FIN标志: 若已发送FIN,则不再发送新数据,直接返回
  • 计算有效窗口: 特殊处理零窗口情况(视为1),确定可用空间
  • 循环创建发送段: 直到填满窗口或无更多数据可发送
    • 创建新消息并设置序列号
    • 视情况设置SYN标志(连接开始)
    • 计算可发送的数据量(受窗口和MAX_PAYLOAD_SIZE限制)
    • 从ByteStream读取数据
    • 视情况设置FIN标志(流结束)
    • 通过transmit回调发送段
    • 将段添加到未确认队列
  • 启动计时器: 如有数据发送且计时器未运行,启动重传计时器

特殊处理:

  • 零窗口处理(视为1)
  • SYN和FIN标志占用序列号空间
  • 段大小限制在MAX_PAYLOAD_SIZE以内
4.3.2 tick()方法 - 时间推进与超时处理

目的: 处理时间推进,检测超时,执行重传

关键步骤:

  • 检查计时器: 确认计时器是否在运行
  • 更新时间: 累加经过的时间(time_elapsed_ += ms)
  • 检查超时: 判断是否达到重传条件(time_elapsed_ ≥ current_RTO_ms_)
  • 重传段: 如超时,重传队首未确认段
  • 窗口检查: 如窗口大小>0
    • 增加连续重传计数
    • 将RTO值翻倍(指数退避)
  • 重置计时器: 无论窗口大小,都重置计时器(time_elapsed_ = 0)

特殊处理:

  • 零窗口特例: 不执行指数退避
  • RTO翻倍限制于窗口非零情况

tick()方法实现了计时器的核心逻辑:

  1. 检查计时器是否运行,如果运行则更新已经过时间:

    time_elapsed_ += ms_since_last_tick
    
  2. 检查计时器是否超时:

    if (time_elapsed_ >= current_RTO_ms_) {
        // 计时器超时处理
    }
    
  3. 计时器超时时的处理:

    • 重传最早的未确认段(队列中的第一个段)
    • 如果窗口大小大于0:
      • 增加连续重传计数
      • 执行指数退避(将current_RTO_ms_值加倍)
    • 重置计时器(time_elapsed_ = 0),但保持计时器运行状态
4.3.3 receive()方法 - 处理确认消息

目的: 处理从接收方收到的确认消息,更新窗口和移除已确认段

关键步骤:

  • 更新窗口大小: 记录接收方通告的新窗口大小
  • 检查确认号: 确认是否包含ackno字段
  • 验证确认号: 转换为绝对序列号并验证有效性
  • 检查新确认: 判断是否确认了新数据(比之前的ackno大)
  • 处理确认号:
    • 更新内部ackno_记录
    • 循环移除已完全确认的段
    • 更新outstanding_seqno_计数
  • 重置状态: 如有新确认
    • 重置RTO为初始值
    • 重置连续重传计数为0
    • 重置计时器或在无未确认段时停止计时器

特殊处理:

  • 序列号转换(Wrap32与绝对序列号)
  • 确认号有效性验证
  • 段的完全确认判定(所有序列号小于ackno)

4.4 RTO状态流转

  1. 初始状态运行状态:当发送包含数据的段且计时器未运行时
  2. 运行状态超时状态:当经过RTO时间后
  3. 超时状态初始状态:重传段并重置计时器后
  4. 运行状态初始状态:当所有段都被确认,停止计时器

4.5 特殊情况处理

  1. 窗口大小为0时

    • 在push()方法中,将窗口大小视为1(仅在push方法中这样处理)
    • 在重传时不执行指数退避(不加倍RTO值)
  2. 空消息(零序列号长度)

    • 不加入未确认队列
    • 不会触发计时器的启动或重置

5. 核心架构

image.png

5.1 状态管理设计

TCP发送器需要维护的关键状态可以更优化地组织为三个逻辑组:

5.1.1 连接状态管理
  • next_seqno_:下一个待分配的绝对序列号
  • bytes_in_flight_:已发送未确认的字节数(更准确的性能指标)
  • syn_sent_/fin_sent_:连接建立/终止标志
  • stream_ended_:输入字节流是否已结束
5.1.2 流量控制状态
  • last_ackno_received_:最近收到的确认号(绝对序列号)
  • receiver_window_size_:接收方通告的窗口大小
  • available_window_space_:实际可用窗口空间(优化计算)
  • zero_window_probe_active_:是否处于零窗口探测模式
5.1.3 重传机制状态
  • current_RTO_ms_/initial_RTO_ms_:当前/初始RTO值
  • timer_running_:计时器运行状态标志
  • time_elapsed_ms_:计时器启动后的累计时间
  • consecutive_retransmissions_:连续重传计数

5.2 数据结构优化

5.2.1 未确认段队列
struct OutstandingSegment {
    TCPSenderMessage message;     // 完整TCP消息
    uint64_t send_time;           // 发送/重传时间
    uint64_t absolute_seqno;      // 起始绝对序列号(快速查询)
    uint64_t sequence_length;     // 序列号空间长度(预计算)
};
std::deque<OutstandingSegment> outstanding_segments_;
  • 使用deque而非vector以优化队首移除操作
  • 预计算并存储序列长度,避免重复计算
  • 存储绝对序列号便于快速比较和区间检查

6. 算法优化

6.1 窗口管理优化

6.1.1 窗口填充策略
// 有效窗口计算
uint64_t effective_window = window_size_ > 0 ? window_size_ : 1;
uint64_t available_window = std::min(
    effective_window,
    ackno_ + effective_window > next_seqno_ ? 
        ackno_ + effective_window - next_seqno_ : 0
);

// 分段策略
uint64_t max_segment_size = std::min(
    TCPConfig::MAX_PAYLOAD_SIZE,
    available_window
);
  • 高效处理边界情况(窗口大小为0,窗口完全填满)
  • 自适应段大小,根据可用窗口动态调整
6.1.2 零窗口探测优化
// 只在真正需要时发送零窗口探测
if (window_size_ == 0 && !outstanding_segments_.empty() && 
    time_since_last_segment > current_RTO_ms_) {
    // 发送单字节段作为探测
    send_zero_window_probe(transmit);
}
  • 精确控制探测时机,避免过度探测
  • 区分常规重传和零窗口探测,分别处理

6.2 重传机制优化

6.2.1 计时器管理优化
void start_timer() {
    timer_running_ = true;
    time_elapsed_ms_ = 0;
    timer_start_time_ = current_time_ms_;  // 记录启动时刻
}

bool timer_expired() const {
    return timer_running_ && 
           time_elapsed_ms_ >= current_RTO_ms_;
}
  • 添加显式的计时器状态检查方法
  • 使用相对时间和绝对时间混合策略提高精度
6.2.2 智能重传策略
// 计时器到期处理
if (timer_expired()) {
    // 只重传最早的未确认段
    const auto& oldest_segment = outstanding_segments_.front();
    transmit(oldest_segment.message);
    
    // 记录重传时间和更新重传次数
    oldest_segment.send_time = time_elapsed_ms_;
    
    // 指数退避(仅在窗口非零时)
    if (window_size_ > 0) {
        consecutive_retransmissions_++;
        current_RTO_ms_ *= 2;  // 指数退避
    }
    
    // 重置计时器
    reset_timer();
}
  • 精确跟踪每个段的发送/重传时间
  • 优化指数退避策略,避免非必要的RTO增长

6.3 段确认处理优化

6.3.1 高效确认处理
// 快速路径: 检查是否有新数据被确认
bool new_data_acked = ackno > last_ackno_received_;

// 移除已确认段
while (!outstanding_segments_.empty()) {
    auto& segment = outstanding_segments_.front();
    // 使用预计算的绝对序列号范围快速判断
    if (segment.absolute_seqno + segment.sequence_length <= ackno) {
        bytes_in_flight_ -= segment.sequence_length;
        outstanding_segments_.pop_front();
    } else {
        break;
    }
}

// 仅在确认新数据时重置RTO
if (new_data_acked) {
    current_RTO_ms_ = initial_RTO_ms_;
    consecutive_retransmissions_ = 0;
    
    // 条件性重启计时器
    if (!outstanding_segments_.empty()) {
        reset_timer();
    } else {
        stop_timer();
    }
}
  • 使用快速路径检查优化常见情况
  • 预计算和缓存序列号范围减少运行时计算
  • 批量处理已确认段,提高效率

8. 核心组件说明

image.png

8.1 Checkpoint 0: ByteStream

  • 实现基本的字节流抽象,包含Writer和Reader两个接口
  • 提供容量控制机制,限制可缓存的数据量
  • 作为整个协议栈的基础数据流容器
  • 类似于TCP套接字提供给应用层的流接口

8.2 Checkpoint 1: Reassembler

  • 负责接收带索引的字节子串并按顺序重组
  • 处理网络中可能出现的乱序、重复数据
  • 使用ByteStream作为输出目标
  • 将不连续的数据片段重组为连续的字节流

8.3 Checkpoint 2: TCPReceiver

  • 处理32位TCP序列号(Wrap32)与64位内部索引的转换
  • 接收TCP段(包含SYN, 数据载荷, FIN等)
  • 使用Reassembler重组数据流
  • 生成确认信息(ackno)和窗口大小(window)
  • 实现TCP接收端的核心功能

8.4 Checkpoint 3: TCPSender (本次实验)

  • 从ByteStream读取数据并创建TCP段
  • 管理SYN和FIN等连接控制标志
  • 遵循接收方通告的窗口限制发送数据
  • 跟踪未确认的段并实现超时重传
  • 实现指数退避(exponential backoff)的RTO机制
  • 确保可靠数据传输

8.5 Checkpoint 4: TCPConnection

  • 整合TCPSender和TCPReceiver
  • 管理TCP连接的状态变化
  • 处理三次握手和四次挥手等连接操作
  • 实现完整的TCP协议状态机
  • 向应用层提供可靠的双向字节流

9. 数据结构与辅助组件

9.1 TCP协议数据结构

  • TCPSenderMessage: 发送方消息格式
  • TCPReceiverMessage: 接收方确认消息格式
  • Wrap32: 处理32位序列号的封装类型
  • 窗口管理: 流量控制机制

10. 组件间的关系

10.1 层次关系

  • ByteStream → Reassembler → TCPReceiver
  • ByteStream → TCPSender
  • TCPSender + TCPReceiver → TCPConnection
  • TCPConnection → 应用层

10.2 数据流关系

  • 发送方向: 应用层 → ByteStream → TCPSender → 网络层
  • 接收方向: 网络层 → TCPReceiver → Reassembler → ByteStream → 应用层

10.3 功能依赖

  • TCPSender依赖ByteStream读取数据
  • TCPReceiver依赖Reassembler重组数据
  • Reassembler依赖ByteStream存储数据
  • TCPConnection同时依赖TCPSender和TCPReceiver

TCP 发送器的完整工作流程:push() 和 receive() 方法的交互示例

通信场景设置

假设我们有以下初始条件:

  • 发送器初始序列号(ISN) = 1000
  • 初始重传超时(RTO) = 100ms
  • 接收方初始窗口大小 = 10 字节
  • 待发送的数据:“HelloWorld!”(12个字节)
  • 发送器初始状态:
    next_seqno_ = 1000
    ackno_ = 0
    window_size_ = 10
    outstanding_seqno_ = 0
    timer_running_ = false
    syn_sent_ = false
    fin_sent_ = false
    

第一步:首次调用 push()

应用程序调用 push() 方法开始发送数据:

sender.push(transmit_function);
push() 方法执行流程:
// 检查 FIN 标志
if (fin_sent_) { // 为 false,继续
    return;
}

// 计算窗口
uint64_t effective_window = (window_size_ > 0) ? window_size_ : 1; // = 10
available_window = 10;

// 循环发送数据
while (available_window > 0) {
    // 创建第一个段(包含 SYN)
    TCPSenderMessage msg1;
    msg1.seqno = Wrap32::wrap(1000, 1000); // = 1000
    msg1.SYN = true; // 设置 SYN 标志
    syn_sent_ = true;
    available_window--; // 为 SYN 减少窗口空间,现在是 9
    
    // 计算可发送的数据量
    uint64_t payload_size = min(9, MAX_PAYLOAD_SIZE); // = 9
    
    // 读取数据
    string payload = "HelloWorl"; // 9 字节数据
    msg1.payload = payload;
    
    // 发送段
    transmit(msg1);
    
    // 更新状态
    uint64_t msg1_size = 1 + 9; // SYN + 数据
    outstanding_seqno_ += msg1_size; // = 10
    next_seqno_ += msg1_size; // = 1010
    
    // 添加到未确认队列
    outstanding_segments_.emplace_back(msg1, time_elapsed_);
    
    // 启动计时器
    if (!timer_running_) {
        timer_running_ = true;
        time_elapsed_ = 0;
    }
    
    // 更新窗口
    available_window -= 9; // = 0
    
    // 窗口用尽,退出循环
    break;
}

发送结果

  • 发送了一个包含 SYN 和 9 字节数据 “HelloWorl” 的段
  • 发送器状态更新为:
    next_seqno_ = 1010
    outstanding_seqno_ = 10
    syn_sent_ = true
    timer_running_ = true
    time_elapsed_ = 0
    outstanding_segments_ = [SYN+"HelloWorl"]
    
  • 剩余未发送数据:“d!”(2字节)

第二步:接收第一个确认

接收方收到 SYN+“HelloWorl”,发送确认消息:

  • ackno = 1010(确认收到了 SYN 和 9 字节数据)
  • window_size = 8(可继续接收 8 字节)

发送器接收到这个消息:

sender.receive(receiver_message);
receive() 方法执行流程:
// 更新窗口大小
window_size_ = 8;

// 处理确认号
uint64_t abs_ackno = 1010; // 已转换为绝对序列号

// 检查有效性
if (abs_ackno > next_seqno_) { // 1010 <= 1010,是有效确认
    // 继续处理
}

// 检查是否是新确认
bool new_data_acked = (abs_ackno > ackno_); // 1010 > 0,是新确认

// 更新确认号
ackno_ = 1010;

// 移除已确认的段
// 检查 SYN+"HelloWorl":seg_end = 1000 + 10 = 1010
// 1010 <= 1010,已完全确认,移除
outstanding_seqno_ -= 10; // = 0
outstanding_segments_.pop_front(); // 队列为空

// 处理新确认
if (new_data_acked) {
    // 重置 RTO
    current_RTO_ms_ = initial_RTO_ms_; // = 100ms
    consecutive_retransmissions_ = 0;
    
    // 处理计时器
    if (!outstanding_segments_.empty()) {
        reset_timer();
    } else {
        stop_timer(); // 停止计时器
    }
}

处理结果

  • 窗口大小更新为 8
  • 确认号更新为 1010
  • 未确认队列清空
  • 计时器停止
  • 发送器状态更新为:
    ackno_ = 1010
    window_size_ = 8
    outstanding_seqno_ = 0
    timer_running_ = false
    outstanding_segments_ = []
    

第三步:再次调用 push() 发送剩余数据

应用程序再次调用 push() 方法:

sender.push(transmit_function);
push() 方法执行流程:
// 检查 FIN 标志
if (fin_sent_) { // 为 false,继续
    return;
}

// 计算窗口
uint64_t effective_window = (window_size_ > 0) ? window_size_ : 1; // = 8
available_window = 8;

// 循环发送数据
while (available_window > 0) {
    // 创建第二个段
    TCPSenderMessage msg2;
    msg2.seqno = Wrap32::wrap(1010, 1000); // = 1010
    
    // 计算可发送的数据量
    uint64_t payload_size = min(8, MAX_PAYLOAD_SIZE); // = 8
    
    // 读取剩余数据
    string payload = "d!"; // 2 字节,全部剩余数据
    msg2.payload = payload;
    
    // 检查是否需要设置 FIN
    // 流已结束且有空间放置 FIN
    bool should_set_fin = true;
    available_window--; // 为 FIN 减少窗口空间,现在是 7
    
    // 设置 FIN 标志
    msg2.FIN = true;
    fin_sent_ = true;
    
    // 发送段
    transmit(msg2);
    
    // 更新状态
    uint64_t msg2_size = 2 + 1; // 数据 + FIN
    outstanding_seqno_ += msg2_size; // = 3
    next_seqno_ += msg2_size; // = 1013
    
    // 添加到未确认队列
    outstanding_segments_.emplace_back(msg2, time_elapsed_);
    
    // 启动计时器
    if (!timer_running_) {
        timer_running_ = true;
        time_elapsed_ = 0;
    }
    
    // FIN 已发送,退出循环
    break;
}

发送结果

  • 发送了一个包含 2 字节数据 “d!” 和 FIN 标志的段
  • 发送器状态更新为:
    next_seqno_ = 1013
    outstanding_seqno_ = 3
    fin_sent_ = true
    timer_running_ = true
    time_elapsed_ = 0
    outstanding_segments_ = ["d!"+FIN]
    
  • 所有数据已发送完毕

第四步:网络丢包和超时重传

假设第二个段在传输过程中丢失。经过一段时间,发送器的 tick() 方法被调用:

// 经过 100ms
sender.tick(100, transmit_function);
tick() 方法执行流程:
// 检查计时器状态
if (timer_running_) { // 为 true,继续
    // 更新时间
    time_elapsed_ += 100; // = 100ms
    
    // 检查是否超时
    if (time_elapsed_ >= current_RTO_ms_) { // 100 >= 100,触发超时
        // 重传最早的未确认段
        if (!outstanding_segments_.empty()) {
            // 重传 "d!"+FIN
            transmit(outstanding_segments_.front().message);
            
            // 指数退避
            if (window_size_ > 0) { // 为 8,条件成立
                consecutive_retransmissions_++; // = 1
                current_RTO_ms_ *= 2; // = 200ms
            }
        }
        
        // 重置计时器
        time_elapsed_ = 0;
    }
}

执行结果

  • 重传了 “d!”+FIN 段
  • RTO 值增加为 200ms
  • 连续重传计数增加为 1
  • 计时器重置,继续运行
  • 发送器状态更新为:
    current_RTO_ms_ = 200
    consecutive_retransmissions_ = 1
    time_elapsed_ = 0
    

第五步:接收最终确认

接收方收到重传的 “d!”+FIN 段,发送最终确认:

  • ackno = 1013(确认所有数据和 FIN)
  • window_size = 10(窗口恢复)

发送器接收到这个消息:

sender.receive(final_ack);
receive() 方法执行流程:
// 更新窗口大小
window_size_ = 10;

// 处理确认号
uint64_t abs_ackno = 1013; // 已转换为绝对序列号

// 检查有效性
if (abs_ackno > next_seqno_) { // 1013 <= 1013,是有效确认
    // 继续处理
}

// 检查是否是新确认
bool new_data_acked = (abs_ackno > ackno_); // 1013 > 1010,是新确认

// 更新确认号
ackno_ = 1013;

// 移除已确认的段
// 检查 "d!"+FIN:seg_end = 1010 + 3 = 1013
// 1013 <= 1013,已完全确认,移除
outstanding_seqno_ -= 3; // = 0
outstanding_segments_.pop_front(); // 队列为空

// 处理新确认
if (new_data_acked) {
    // 重置 RTO
    current_RTO_ms_ = initial_RTO_ms_; // = 100ms
    consecutive_retransmissions_ = 0;
    
    // 处理计时器
    if (!outstanding_segments_.empty()) {
        reset_timer();
    } else {
        stop_timer(); // 停止计时器
    }
}

最终状态

  • 窗口大小更新为 10
  • 确认号更新为 1013
  • 未确认队列清空
  • 计时器停止
  • RTO 重置为初始值
  • 连续重传计数重置为 0
  • 发送器状态更新为:
    ackno_ = 1013
    window_size_ = 10
    outstanding_seqno_ = 0
    timer_running_ = false
    current_RTO_ms_ = 100
    consecutive_retransmissions_ = 0
    outstanding_segments_ = []
    

第六步:尝试再次发送数据

应用程序再次调用 push() 方法:

sender.push(transmit_function);

push() 方法执行流程:

// 检查 FIN 标志
if (fin_sent_) { // 为 true,终止发送
    return;
}

结果

  • 因为 FIN 已发送,表示流已结束,不再发送数据
  • 发送器状态保持不变
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值