KPBOT / CS144 Lab4 笔记 — TCPConnection:组装完整 TCP 协议栈

Created Sun, 26 Jul 2026 00:00:00 +0000 Modified Sun, 26 Jul 2026 00:00:00 +0000

这是整个实验最复杂的部分。

总的流程说实话我并没有完全做出来,抄了大量的作业,主要原因还是 TCP 的状态机确实有点复杂,有很多边缘条件需要考虑,调试起来死活都修不好。

我们需要将 TCPSender 和 TCPReceiver 结合起来变为一个完整的 TCPConnection。尽管在 TCPSender 与 TCPReceiver 等实现时已经完成了实验中所需的所有数据结构与算法部分,但 TCPConnection 最难的其实是它的状态机部分。

如何建立连接?SYN 请求被接收了如何更新 TCPReceiver 和 TCPSender?如何对异常的 segment 处理?如何对 TCPSender 的输出流进行封装、给他加上 ackno 等 header?如何应对 RST 的 header?这些都是必须一一解决的问题。

下图为 TCP 的 FSM 图:

TCP FSM

基础工具函数

首先从最简单的部分去实现:

size_t TCPConnection::remaining_outbound_capacity() const {
    return _sender.stream_in().remaining_capacity();
}

size_t TCPConnection::bytes_in_flight() const {
    return _sender.bytes_in_flight();
}

size_t TCPConnection::unassembled_bytes() const {
    return _receiver.unassembled_bytes();
}

size_t TCPConnection::time_since_last_segment_received() const {
    return _time_since_last_segment_received;
}

对各个 getter 类函数直接 return 即可。

push_segments_out

由于 receiver 部分涉及到大量状态的转换,所以先去实现 sender 部分。sender 主要要处理:

  • 如何写入数据到 stream 内
  • 如何给之前 sender 打包好的 seg 加头然后 push 到 segments_out 中
  • 如何实现 tick
  • 如何结束 sender 流

write 函数

直接将数据压入 stream_in 即可,然后去将 segment 压入 segments_out:

size_t ret = _sender.stream_in().write(data);
push_segments_out();
return ret;

实现 push_segments_out

push_segments_out 需要考虑以下情况:

  1. 正常数据传输
  2. SYN 请求与 SYN-ACK 请求
  3. RST 请求

SYN 请求与 SYN-ACK 请求分别处于两种状态:

  • 状态 1:未开始建立连接,主动发送 SYN 请求进行连接建立
  • 状态 2:对方请求连接(对方发来 SYN 请求),我方需要发送 SYN-ACK 进行回应

对于状态 1,直接让 sender 执行 fill_window 即可生成一个 SYN 的 seg(因为若要发送 SYN,那么证明当前状态的 sender 的 _syn_flag == false,且 abs_seqno、ackno 等也均开始初始化)。

对于状态 2,由于我们是接收到 SYN 后被动建立连接,此时 TCPReceiver 已经完成了 SYN 的接收,但是 TCPSender 并不知道对方已经发起连接,所以需要通过:

_sender.fill_window(send_syn || in_syn_recv());

这里的逻辑是主动触发 sender 发送 SYN:

  • 如果 send_syn = true,代表本端主动建立连接,此时直接调用 fill_window(true),允许 sender 发送 SYN。
  • 如果 in_syn_recv() 为 true,代表当前处于被动连接建立阶段,对端已经发送 SYN,需要回复 SYN-ACK,因此也调用 fill_window(true)

否则正常数据传输阶段:

_sender.fill_window(false);

sender 根据当前发送窗口以及 ByteStream 中的数据生成普通 DATA segment。

封装 TCPSegment

生成 segment 后,需要将 sender 输出的 segment 进行 TCPConnection 层面的封装。

TCPSender 负责的是可靠字节流传输,它只关心:

  • seqno
  • payload
  • SYN
  • FIN
  • retransmission

但是 TCPConnection 作为 TCP 连接层,需要负责两个方向状态的结合,因此需要给 sender 生成的 segment 添加 receiver 相关信息:

if (_receiver.ackno().has_value()) {
    seg.header().ack = true;
    seg.header().ackno = _receiver.ackno().value();
    seg.header().win = _receiver.window_size();
}

这里实际上完成了 TCP 中最核心的 ACK 复用机制。TCP 是全双工协议,发送方向和接收方向的数据流独立维护。例如:

A ---- data(seq=100) ----> B
A <--- ACK(101) ---------- B

ACK 并不是由 sender 产生,而是由 receiver 根据自己已经成功接收的数据产生。因此 TCPConnection 需要将:

TCPSender 生成:          TCPReceiver 生成:
  seqno                    ackno
  payload        +         window size
  SYN
  FIN

                    ↓

              完整 TCP segment

组合起来。

RST 处理

if (_need_send_rst) {
    _need_send_rst = false;
    seg.header().rst = true;
}

RST 是 TCP 中异常终止连接的机制。例如:

  • 收到非法 segment
  • 连接不存在
  • 对端状态异常

TCPConnection 会设置 _need_send_rst = true,然后在下一次输出 segment 时附加 RST 标志。这里需要注意,RST 通常需要携带 ACK 信息,因此它也应该经过正常的 segment 封装流程。

最终:

_segments_out.push(seg);

将完整 TCP segment 交给外层网络发送。

整体流程如下:

应用层 write()
        |
        v
ByteStream
        |
        v
TCPSender.fill_window()
        |
        v
生成 segment (seq, SYN, FIN, payload)
        |
        v
TCPConnection.push_segments_out()
        |
        +------ TCPReceiver 提供 ackno / window
        |
        +------ 添加 RST
        |
        v
完整 TCP segment
        |
        v
_segments_out
bool TCPConnection::in_syn_recv() {
    return _receiver.ackno().has_value() && !_receiver.stream_out().input_ended();
}

bool TCPConnection::push_segments_out(bool send_syn) {
    _sender.fill_window(send_syn || in_syn_recv());
    TCPSegment seg;
    while (!_sender.segments_out().empty()) {
        seg = _sender.segments_out().front();
        _sender.segments_out().pop();
        if (_receiver.ackno().has_value()) {
            seg.header().ack = true;
            seg.header().ackno = _receiver.ackno().value();
            seg.header().win = _receiver.window_size();
        }
        if (_need_send_rst) {
            _need_send_rst = false;
            seg.header().rst = true;
        }
        _segments_out.push(seg);
    }
    clean_shutdown();
    return true;
}

segment_received

之后需要实现 segment_received(),这是整个 TCPConnection 最复杂的部分。因为收到一个 segment 后,需要同时影响 sender 和 receiver:

收到 segment
       |
       +----------------+
       |                |
       v                v
TCPReceiver        TCPSender
处理数据流          处理 ACK
更新 ackno           滑动发送窗口
产生状态变化         清理 outstanding
       |
       |
       v
TCPConnection 判断状态
       |
       v
是否回复 ACK / SYN-ACK / RST

其中最大的难点在于 TCP 状态机。例如:

1. 收到 SYN:

LISTEN
 |
 | SYN
 v
SYN_RECEIVED

需要:receiver 接收 SYN、sender 发送 SYN、输出 SYN+ACK。

2. 收到 SYN+ACK:

SYN_SENT
 |
 | SYN+ACK
 v
ESTABLISHED

需要:receiver 接收 SYN、sender 收到 ACK、状态转换。

3. 收到 FIN:

FIN 本质上也是一个占用一个字节序列号的"数据",因此不能简单处理。需要:receiver 判断 FIN 是否连续、ByteStream 输入结束、sender 判断是否发送 FIN。

4. 异常 segment:

例如:未建立连接收到数据、ACK 超过发送范围、RST、非法状态转换——这些都需要严格按照 TCP FSM 处理。

因此 TCPConnection 实际上并没有增加新的可靠传输算法,它更像一个 TCP 状态机协调器

最终,根据 TCP 状态转移图实现 segment_received:

void TCPConnection::segment_received(const TCPSegment &seg) {
    // 检查连接存活状态
    if (!_active) {
        return;
    }
    _time_since_last_segment_received = 0;

    if (seg.header().ack && seg.payload().size() && in_syn_sent()) {
        return;
    }

    bool send_retry_ack = false;
    if (seg.header().ack && _sender.next_seqno_absolute())
        send_retry_ack = !_sender.ack_received(seg.header().ackno, seg.header().win);
    if (!_receiver.segment_received(seg))
        send_retry_ack = true;

    if (seg.header().syn && !_sender.next_seqno_absolute()) {
        connect();
        return;
    }
    if (seg.header().rst) {
        if (in_syn_sent() && !seg.header().ack) {
            return;
        }
        unclean_shutdown(false);
        return;
    }
    if (seg.length_in_sequence_space()) {
        send_retry_ack = true;
    }
    send_retry_ack = send_retry_ack
        && (_receiver.ackno().has_value() && _sender.segments_out().empty());
    if (send_retry_ack) {
        _sender.send_empty_segment();
    }
    push_segments_out();
}

clean_shutdown

TCP 关闭相比建立连接更加复杂,因为 TCP 是全双工协议,两个方向的数据流是独立关闭的。也就是说:

  • 对方关闭发送方向,不代表连接立即关闭
  • 本方关闭发送方向,也需要等待对方确认
  • 双方都结束后,TCPConnection 才能进入 inactive 状态

因此需要 clean_shutdown() 来判断当前连接是否可以安全关闭。

半关闭处理

if (_receiver.stream_out().input_ended() && !(_sender.stream_in().eof())) {
    _linger_after_streams_finish = false;
}

这里表示对方已经发送 FIN,但是本方还有数据没有发送完成。例如:

A ---------------- data -------------> B
A <--------------- FIN --------------- B

此时 B 的 receiver 已经收到 FIN(_receiver.stream_out().input_ended() == true),但 B 仍然有数据需要发送(_sender.stream_in().eof() == false),说明连接进入半关闭状态。

正常情况下 TCPConnection 会等待一段时间保持连接(_linger_after_streams_finish = true),因为 TCP 关闭后可能还需要接收最后的数据或者 ACK。但这种情况对方已经关闭发送方向,不可能再发送新的数据,因此无需继续 linger:

_linger_after_streams_finish = false;

真正关闭条件

if (_sender.stream_in().eof()
    && _sender.bytes_in_flight() == 0
    && _receiver.stream_out().input_ended())

需要同时满足三个条件:

第一:_sender.stream_in().eof() — 本端应用已经写完所有数据。

第二:_sender.bytes_in_flight() == 0 — 所有已经发送的数据都已经被对方 ACK。因为 TCP 是可靠传输协议,发送完成 ≠ 关闭连接,必须保证所有 segment 都收到 ACK 确认。

第三:_receiver.stream_out().input_ended() — 对方也已经发送 FIN,即双方发送方向均关闭。

三个条件满足后,连接已经逻辑结束。但是 TCP 还需要考虑 linger:

if (!_linger_after_streams_finish
    || time_since_last_segment_received() >= 10 * _cfg.rt_timeout)

如果 _linger_after_streams_finish = false 说明不需要等待直接关闭;否则等待 time_since_last_segment_received() >= 10 * retransmission_timeout 后关闭。原因是 TCP 关闭后仍然可能收到迟到 segment,如果立即释放连接可能无法正确处理这些延迟 segment,因此需要 linger 一段时间。

最后:

_active = false;

表示 TCPConnection 生命周期结束。

所以 clean_shutdown() 本质上是 TCPConnection 的终止状态机。


unclean_shutdown

unclean_shutdown() 负责处理异常关闭,也就是 TCP 连接无法按照正常 FIN 四次挥手流程结束时的情况。其中包括:

  • 收到 RST
  • 非法状态转换
  • 协议错误
  • 应用主动终止连接
void TCPConnection::unclean_shutdown(bool send_rst) {
    _receiver.stream_out().set_error();
    _sender.stream_in().set_error();
    _active = false;

首先需要通知上下层连接已经异常终止。

对于接收方向:

_receiver.stream_out().set_error();

表示 TCPReceiver 提供给应用层的输入流发生错误。之后应用读取数据时应该知道不是正常 EOF,而是连接异常。

对于发送方向:

_sender.stream_in().set_error();

表示应用写入发送流时发生错误。此时连接已经不存在,不能继续发送数据。

然后:

_active = false;

直接结束 TCPConnection 生命周期。这和 clean_shutdown() 不同——clean_shutdown() 是正常关闭,需要等待 FIN、ACK 等状态完成;unclean_shutdown() 是立即终止,不需要维护 TCP 状态机。

之后处理是否发送 RST:

if (send_rst)

RST 是 TCP 中用于通知对端「这个连接已经异常终止,不要继续发送数据」。设置 _need_send_rst = true 表示下一次发送 segment 时添加 RST 标志。

但是这里有一个特殊处理:

if (_sender.segments_out().empty()) {
    _sender.send_empty_segment();
}

RST 本身需要依附在 TCP segment 上发送。如果 sender 当前没有任何 segment(segments_out 为空),那么没有载体发送 RST。因此生成一个空 segment,然后 push_segments_out() 装载 RST 发送请求。