当前位置: 首页 > news >正文

计网实验(四)CS144 Lab4

旧版lab4任务书地址

  1. 目标

    • 实现 TCPConnection 类,它将之前实验中完成的 TCPSenderTCPReceiver 组合起来。
    • 处理连接级别的全局事务和状态管理。
    • 最终你的 TCP 实现将能够通过用户态的 UDP 封装 (TCP-in-UDP) 或直接的 IP 数据报 (TCP/IP) 与互联网上的其他计算机进行通信。
  2. 核心警告

    • TCPConnection 本身的代码量预计不大(小于100行或100-150行)。
    • 实验的难度主要取决于你之前实现的 TCPSenderTCPReceiver 是否健壮。 如果它们有很多bug,调试这个实验会非常耗时。
    • 强烈建议尽早开始,不要拖到最后。
  3. TCPConnection 的主要职责 (Section 3, 4)

    • 接收段 (Receiving segments - segment_received 方法)

      • RST 标志处理:如果收到带 RST 标志的段,立即终止连接,设置输入输出流为错误状态,active() 返回 false。
      • 将段传递给 TCPReceiver:让 TCPReceiver 处理它关心的字段(seqno, SYN, payload, FIN)。
      • 如果段有 ACK 标志,将信息传递给 TCPSender:告知 TCPSender acknowindow_size
      • 确保响应:如果收到的段占用了序列号空间(即不是纯ACK),TCPConnection 需要确保至少发送一个段作为回复(通常是 ACK 段,用于更新对方的 ackno 和窗口信息)。
      • 处理"Keep-Alive"段 (特殊情况):如果收到一个不占用序列号空间、但序列号等于接收方期望的 ackno - 1 的段,这可能是一个keep-alive探测。TCPConnection 应该回复一个空段(通过 _sender.send_empty_segment()),即使这个探测段的序列号可能是“无效的”。
    • 发送段 (Sending segments)

      • TCPSendersegments_out 队列中有段时,TCPConnection 需要将其取出。
      • 在发送前,向 TCPReceiver 索取 acknowindow_size
      • 如果 TCPReceiver 提供了有效的 ackno,则在出站段上设置 ACK 标志,并填入 acknowindow_size
      • 将准备好的段放入 TCPConnection 自己的 _segments_out 队列,等待外部调用者(如 CS144TCPSocket)来真正发送它们。
    • 时间流逝 (Time passes - tick 方法)

      • 通知 TCPSender 时间流逝(调用 _sender.tick())。
      • 检查是否需要中止连接并发送 RST 段:如果 TCPSender 的连续重传次数超过了 TCPConfig::MAX_RETX_ATTEMPTS
      • 处理连接的干净关闭(详见 Section 5)。
  4. FAQs 和特殊情况 (Section 4)

    • inbound_stream():已在头文件实现,无需额外工作。
    • 不需要复杂数据结构:主要工作是“连接”已有的 TCPSenderTCPReceiver
    • 发送段的方式:将段 pushTCPConnection_segments_out 队列。
    • 时间感知:通过 tick() 方法。
    • 何时发送 RST 段
      1. TCPSender 连续重传次数超限。
      2. TCPConnection 的析构函数被调用,但连接仍然 active()
    • 如何构造带 RST 的段:可以通过调用 _sender.send_empty_segment()_sender.fill_window() 来让 TCPSender 生成一个带有正确序列号的段,然后 TCPConnection 再设置其 RST 标志。
    • ACK 标志和 ackno
      • 出站段:只要 TCPReceiver 能提供 ackno (通过 _receiver.ackno().has_value() 判断),就应该在出站段上设置 ACK 标志和相应的 acknowindow_size
      • 入站段:只有当入站段的 ACK 标志被设置时,才将其 acknowindow_size 提供给 TCPSender
    • 状态名称:参考 Lab 2 和 Lab 3 的图表,这些状态是根据已有模块的公共接口推断出来的,不需要额外创建状态变量。
    • 窗口大小字段限制:如果 TCPReceiver 想通告一个大于 TCP 头部 win 字段能表示的最大值的窗口,应发送能表示的最大值 (std::numeric_limits<uint16_t>::max())。
  5. 连接的结束 (The end of a TCP connection - Section 5) - 核心难点

    • 目标是判断连接何时完全“完成” (active() 返回 false)。

    • 不干净关闭 (Unclean shutdown):发送或接收到 RST 段,连接立即死亡。

    • 干净关闭 (Clean shutdown):确保双方数据都可靠送达。涉及四个先决条件:

      1. Prereq #1: 入站流已完全组装并结束 (FIN 已收到并处理)。
      2. Prereq #2: 出站流已被应用层关闭,并且包含 FIN 的段已发送给对端。
      3. Prereq #3: 出站流(包括其 FIN)已被对端完全确认。
      4. Prereq #4: 本地 TCPConnection 确信远程对端能够满足其 Prereq #3(即远程对端收到了本地对其 FIN 的确认)。这是最复杂的部分。
    • Prereq #4 的两种满足方式

      • Option A: Lingering after both streams end (逗留)

        • Prereqs #1, #2, #3 都满足。
        • 本地端需要等待一段时间(至少 10 * _cfg.rt_timeout,即 10 倍初始RTO),从最后一次收到远程对端的数据段算起。
        • 这是为了确保远程对端收到了本地对其FIN的ACK,避免远程对端不必要的重传。
        • 在这段时间内,TCPConnection 仍然 active,并可能需要响应进入的段(例如,发送ACK)。
        • _linger_after_streams_finish 变量: 初始为 true
      • Option B: Passive close (被动关闭)

        • Prereqs #1, #2, #3 都满足。
        • 并且,远程对端是第一个结束其流的一方 (即入站流先结束,在本地发送 FIN 之前)。
        • 这种情况下,本地端可以100%确定远程对端能满足其Prereq #3,因为本地端在发送自己的FIN(满足Prereq #2)的同时,也ACK了远程的FIN(满足Prereq #1)。远程对端对本地FIN的ACK(满足Prereq #3)意味着远程对端一定看到了本地对它FIN的ACK。
        • 此时,连接可以立即结束,不需要逗留
        • _linger_after_streams_finish 变量: 如果入站流在出站流的EOF之前结束(即 TCPConnection 尚未发送FIN段时,入站流就结束了),则此变量应设为 false
    • 实践总结 (Section 5.1)

      • TCPConnection 有一个成员变量 _linger_after_streams_finish(初始为 true)。
      • 如果入站流在 TCPConnection 到达其出站流的 EOF 之前结束,则 _linger_after_streams_finish 设为 false
      • 当 Prereqs #1, #2, #3 都满足时:
        • 如果 _linger_after_streams_finishfalse,连接立即“完成” (active() 返回 false)。
        • 否则(为 true),需要逗留:只有当从最后一次收到段开始,经过了足够长的时间 (10 * _cfg.rt_timeout)后,连接才“完成”。
  6. 测试 (Section 6)

    • 鼓励手动测试和使用 Wireshark 进行调试。
    • 提供了使用 tcp_ipv4 进行客户端/服务器通信的示例。
    • 测试零窗口情况:使用 -w 1 参数给 tcp_ipv4,强制接收方容量为1,测试发送方在零窗口探测时是否卡住。
  7. 性能 (Section 7)

    • 要求性能至少达到 “0.10 Gbit/s” (100 Mbps)。
    • 可能需要优化 ByteStreamStreamReassembler
  8. webget Revisited (Section 8)

    • 修改 Lab 0 的 webget.cc,使用你自己的 CS144TCPSocket 代替内核提供的 TCPSocket
    • 需要包含 tcp_sponge_socket.hh
    • get_URL() 函数末尾添加 socket.wait_until_closed(),因为用户态TCP在程序退出后连接状态不会被内核维护。
  9. 开发和提交建议 (Section 9, 10)

    • 主要修改 tcp_connection.cctcp_connection.hh
    • 遵循良好的编程和 Git 实践。
    • 提供了使用 sanitizers 和 gdb 进行调试的建议。
    • 提交前运行 make format, git status, make, make check
    • 撰写 writeups/lab4.md 报告。

核心挑战

  • 正确实现连接关闭逻辑 (Section 5),特别是区分何时需要逗留 (TIME_WAIT 状态的简化版) 以及何时可以立即关闭。这是 TCPConnection 最复杂的部分。
  • 确保 TCPSenderTCPReceiver 的健壮性,因为 TCPConnection 严重依赖它们。
  • 细致地处理各种标志位(RST, ACK, SYN, FIN)的设置和检查,以及它们与 TCPSenderTCPReceiver 的交互。

第一步:理解 TCPConnection 的核心角色和组件

  • 文档 Section 1 (Overview) 和 Figure 1:
    • TCPConnectionTCPSenderTCPReceiver 的“包装器”和协调者。
    • 它负责处理连接的全局事务。
    • 关键组件: 内部包含 _sender_receiver 对象。
    • 输出队列: _segments_out 用于存放准备发往网络的段。
  • tcp_connection.hh:
    • 确认成员变量 _cfg, _receiver, _sender, _segments_out, _linger_after_streams_finish。这些是基础。

第二步:分析对外接口(Public API)并映射到内部组件

这一步是“连线”工作,将 TCPConnection 的方法调用传递给 _sender_receiver

  • Writer接口 (connect, write, remaining_outbound_capacity, end_input_stream):
    • connect(): 发起连接,显然是 _sender 的职责(发送SYN)。
      • 思路: 调用 _sender 的某个方法(如 fill_window()send_empty_segment(),文档暗示fill_window会处理初始SYN)来产生SYN段。
    • write(data): 写入数据,也是 _sender 的职责。
      • 思路: 将 data 写入 _sender.stream_in(),然后调用 _sender.fill_window() 尝试将数据打包成段发送。
    • remaining_outbound_capacity(): 查询还能写多少,直接问 _sender 的输入流。
      • 思路: _sender.stream_in().remaining_capacity()
    • end_input_stream(): 关闭出站流,_sender 发送FIN。
      • 思路: _sender.stream_in().end_input(),然后调用 _sender.fill_window() 尝试发送FIN。
  • Reader接口 (inbound_stream):
    • inbound_stream(): 获取入站流,直接从 _receiver 获取。
      • 思路: _receiver.stream_out() (已在头文件实现)。
  • Accessor接口 (for testing):
    • bytes_in_flight(): _sender.bytes_in_flight()
    • unassembled_bytes(): _receiver.unassembled_bytes()
    • time_since_last_segment_received(): 需要 TCPConnection 自己维护一个计时器变量。
      • 思路: 添加 _time_since_last_segment_received_ms 成员变量。

第三步:分析核心事件处理方法 (segment_received, tick)

这是 TCPConnection 真正发挥协调作用的地方。

  • segment_received(const TCPSegment &seg) (文档 Section 3 “Receiving segments”):

    1. 重置“上次收到段的时间”: _time_since_last_segment_received_ms = 0; (这是为 active() 中的逗留计时服务的)。
    2. RST处理: 如果 seg.header().rst 为真,则连接死亡。
      • 思路: 设置 _sender_receiver 的流为错误状态,更新 TCPConnection 的活动状态(可能通过一个内部标志 _rst_flag_received_or_sent = true)。
    3. 传递给 _receiver: _receiver.segment_received(seg);
      • 引申思考 _linger_after_streams_finish: 文档 Section 5.1 提到,如果入站流在出站流EOF前结束,则 _linger_after_streams_finish = false。入站流是否结束是由 _receiver.stream_out().input_ended() 判断的。出站流EOF是指 _sender.stream_in().eof()
      • 思路: 在调用 _receiver.segment_received(seg) 之后,检查 _receiver.stream_out().input_ended() 是否因此变为真。如果为真,并且此时 !_sender.stream_in().eof() (即本地还没打算关闭出站流),则设置 _linger_after_streams_finish = false;
    4. 处理ACK: 如果 seg.header().ack 为真,将 acknowindow_size 传递给 _sender
      • 思路: _sender.ack_received(seg.header().ackno, seg.header().win);
    5. Keep-Alive响应 (特殊情况): 文档明确给出了判断条件和响应方式。
      • 思路: 实现文档给出的 if 条件,如果满足,调用 _sender.send_empty_segment();
    6. 确保回复: 如果收到的段消耗了序列号空间 (data, SYN, or FIN),TCP通常需要回复一个ACK。
      • 思路: 如果 seg.length_in_sequence_space() > 0,可以调用 _sender.send_empty_segment(); 来确保一个ACK被准备好。
    7. 尝试发送: 在上述处理后,_sender 的状态可能允许发送更多数据(例如,窗口更新了,或者有ACK要发)。
      • 思路: 调用 _sender.fill_window();
    8. 收集出站段: 将 _sender 内部队列中的段取出,进行 TCPConnection 级别的处理(添加ACK信息),然后放入 _segments_out
      • 思路: 创建一个私有辅助函数 _fill_sender_segments_and_queue_them() (或类似名称,之前我称之为 _send_segments())。
  • tick(const size_t ms_since_last_tick) (文档 Section 3 “When time passes”):

    1. 更新计时器: _time_since_last_segment_received_ms += ms_since_last_tick;
    2. 通知 _sender: _sender.tick(ms_since_last_tick);
    3. 检查 _sender 是否放弃 (连续重传超限): 如果 _sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS
      • 思路: 发送RST段 (创建私有辅助函数 _send_rst_and_terminate()),设置错误状态,标记连接死亡。
    4. 检查 active() 状态: active() 的逻辑依赖于流的状态和逗留计时。tick 是驱动逗留计时器检查的地方。
      • 思路: 在 tick 的末尾,如果连接因为逗留超时而变为非活动状态,这里会体现出来。
    5. 收集出站段: _sender 可能因为 tick (内部RTO超时) 而产生需要重传的段。
      • 思路: 调用 _fill_sender_segments_and_queue_them()

第四步:处理出站段的细节 (文档 Section 3 “Sending segments”)

这是 _fill_sender_segments_and_queue_them() 辅助函数的核心逻辑。

  • _sender.segments_out() 不为空时,取出每个段。
  • 关键: 在将段放入 _segments_out 之前,_receiver 索取当前的 acknowindow_size
  • 如果 _receiver.ackno().has_value() 为真,则设置取出的段的 ACK 标志,并填入 _receiver.ackno().value()_receiver.window_size()
  • 将处理好的段放入 TCPConnection_segments_out

第五步:实现 active() 方法 (文档 Section 5 “The end of a TCP connection”)

这是最复杂的部分,需要仔细理解 Prereqs 和两种关闭选项。

  1. 立即不活动的情况:
    • 如果已发送或接收到RST。
      • 思路: 检查内部的 _rst_flag_received_or_sent 标志。
  2. 干净关闭的先决条件检查:
    • Prereq #1: _receiver.stream_out().input_ended() (入站流FIN已处理)
    • Prereq #2: _sender.fin_sent() (我假设TCPSender会提供这个状态,或者通过 _sender.stream_in().eof() && 所有字节都已发送 来判断,确保FIN在飞行中或已ACK)
    • Prereq #3: _sender.bytes_in_flight() == 0 (所有出站数据,包括FIN,都被确认)
  3. 判断是否满足所有三个先决条件:
    • 如果都满足:
      • 如果 !_linger_after_streams_finish,则 active() 返回 false
      • 如果 _linger_after_streams_finish,则检查 _time_since_last_segment_received_ms >= 10 * _cfg.rt_timeout。如果满足,active() 返回 false;否则返回 true
    • 如果 Prereqs 未全部满足,active() 通常返回 true(除非有其他更早的终止条件如RST)。

第六步:实现析构函数 ~TCPConnection()

  • 文档明确指出:如果连接仍然 active(),析构时需要发送RST。
    • 思路: 检查 active()。如果为 true,调用 _send_rst_and_terminate()

第七步:实现RST发送辅助函数 _send_rst_and_terminate()

  1. _sender 生成一个空段(或使用 fill_window 如果它能保证生成一个段)。
  2. _sender.segments_out() 中取出这个段。
  3. 清空 _segments_out (因为RST优先,旧段作废)。
  4. 设置该段的 rst 标志为 true
  5. 关键: 这个RST段本身不需要ACK信息(因为是强制终止),但序列号需要正确。_sender 产生的段应该有正确的序列号。
  6. 将RST段放入 _segments_out
  7. 设置 _sender_receiver 的流为错误状态。
  8. 标记 _rst_flag_received_or_sent = true

相关文章:

  • 【技术原理】Linux 文件时间属性详解:Access、Modify、Change 的区别与联系
  • 2025年5月华为H12-821新增题库带解析
  • React学习———Redux 、 React Redux和react-persist
  • 分布式AI推理的成功之道
  • 20250515通过以太网让VLC拉取视熙科技的机芯的rtsp视频流的步骤
  • RK3588 桌面系统配置WiFi和蓝牙配置
  • 1、数据结构与算法(Python版-啃书)-绪论
  • 前端流行框架Vue3教程:16. 组件事件配合`v-model`使用
  • 【Java ee初阶】http(1)
  • 左手坐标系、右手坐标系、坐标轴方向
  • 2、数据操作DMLDQL
  • 中间件-MQ常见问题
  • 基于AH1101芯片的5V升18.6V LED恒流背光供电方案设计
  • 从代码学习深度学习 - 实战 Kaggle 比赛:图像分类 (CIFAR-10 PyTorch版)
  • electron进程通信
  • constexpr 关键字的意义(入门)
  • 怎样用 esProc 实现连续区间的差集运算
  • 什么是 NB-IoT ?窄带IoT 应用
  • 【SPIN】用Promela验证顺序程序:从断言到SPIN实战(SPIN学习系列--2)
  • 华为Watch的ECG功能技术分析
  • 张巍任中共河南省委副书记
  • 新城悦服务:独董许新民辞任,新任独董与另两人组成调查委员会将调查与关联方资金往来
  • 四川甘孜炉霍县觉日寺管委会主任呷玛降泽被查
  • 新任国防部新闻发言人蒋斌正式亮相
  • 六连板成飞集成:航空零部件业务收入占比为1.74%,市场环境没有重大调整
  • 遭“特朗普关税”冲击,韩国今年经济增长预期“腰斩”降至0.8%