CS 144 Lab Four -- the TCP connection

news2024/12/29 10:36:54

CS 144 Lab Four -- the TCP connection

  • TCPConnection 简述
  • TCP 状态图
  • 代码实现
    • 完整流程追踪
  • 测试


对应课程视频: 【计算机网络】 斯坦福大学CS144课程

Lab Three 对应的PDF: Lab Checkpoint 4: down the stack (the network interface)


TCPConnection 简述

TCPConnection 需要将 TCPSender 和 TCPReceiver 结合,实现成一个 TCP 终端,同时收发数据。

TCPConnection 有几个规则需要遵守:

对于接收数据段而言:

  • 如果接收到的数据包设置了 RST 标志,则将输入输出字节流全部设置为 错误 状态,并永久关闭 TCP 连接。

  • 如果没有收到 RST 标志,则将该数据包传达给 TCPReceiver 来处理,它将对数据包中的 seqno、SYN、payload、FIN 进行处理。

  • 如果接收到的数据包中设置了 ACK 标志,则向当前 TCPConnection它自己的 TCPSender 告知远程终端的 ackno 和 window_size。

    • 这一步相当重要,因为数据包在网络中以乱序形式发送,因此远程发送给本地的 ackno 存在滞后性。

    • 将远程的 ackno 和 window size 附加至发送数据中可以降低这种滞后性,提高 TCP 效率。

  • 如果接收到的 TCP 数据包包含了一个有效 seqno,则 TCPConnection 必须至少返回一个 TCP 包作为回复,以告知远程终端 此时的 ackno 和 window size。

  • 如果接收到的 TCP 数据包包含的 seqno 是无效的,则 TCPConnection 也需要回复一个类似的无效数据包。这是因为远程终端可能会发送无效数据包以确认当前连接是否有效,同时查看此时接收方的 ackno 和 window size。这被称为 TCP 的 keep-alive

  if (_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0 && seg.header().seqno == _receiver.ackno().value() - 1) {
    _sender.send_empty_segment();
  }

对于发送数据段来说:

  • 当 TCPSender 将一个 TCPSegment 数据包添加到待发送队列中时,TCPConnection 需要从中取出并将其发送。
  • 在发送当前数据包之前,TCPConnection 会获取当前它自己的 TCPReceiver 的 ackno 和 window size,将其放置进待发送 TCPSegment 中,并设置其 ACK 标志。

TCPConnection 需要检测时间的流逝。它存在一个 tick 函数,该函数将会被操作系统持续调用。当 TCPConnection 的 tick 函数被调用后,它需要

  • 告知 TCPSender 时间的流逝,这可能会让 TCPSender 重新发送被丢弃的数据包
  • 如果连续重传次数超过 TCPConfig::MAX RETX ATTEMPTS,则发送一个 RST 包。
  • 在条件适合的情况下关闭 TCP 连接(当处于 TCP 的 TIME_WAIT 状态时)。

TCP 连接的关闭稍微麻烦一些,主要有以下几种情况需要考虑:

  • 接收方收到 RST 标志或者发送方发送 RST 标志后,设置当前 TCPConnection 的输入输出字节流的状态为错误状态,并立即停止退出。这种属于暴力退出(unclear shutdown),可能会导致尚未传输完成的数据丢失(例如仍然在网络中运输的数据包在接收方收到RST标志后被丢弃)。
  • 若想让双方都在数据流收发完整后退出(clear shutdonw),则情况略微麻烦一点。先上张四次挥手的图:

在这里插入图片描述

简单讲下挥手的流程:

  • 客户端的数据全部发送完成,则将会发送 FIN 包以告知服务器 客户端数据全部发送完成(发送完成,不等于被接收完成)。但请注意,此时的服务器仍然可以发送数据至客户端。

  • 当服务器对 客户端的 FIN 进行 ack 后,则说明服务器确认接收客户端的全部数据

  • 服务器继续发送数据,直到服务器的数据已经全部发送完成,则向客户端发送 FIN 包以告知服务端数据全部发送完成

  • 当客户端对服务端的 FIN 发送 ack 后,则说明客户端确认接收服务端的全部数据。注意,此时客户端可以确认:

    • 服务端成功接收客户端全部数据
    • 客户端成功接收服务端的全部数据

此时客户端可以百分百相信,此时断开连接对客户端是没有任何危害的

但是!当服务器没接收到 客户端的 ACK 时,

  • 服务器可以确认它成功接收客户端全部数据
  • 服务器不知道客户端是否成功接收服务端的全部数据

也就是说,服务器一定要获得到客户端的 ACK 才能关闭。

若服务器在超时时间内没获得到客户端的 FIN ACK,则会重发 FIN 包。但假如此时客户端已经断连,那么服务器将永远无法获取到客户端的 FIN ACK。因此即便客户端已经完成了它的所有任务,它仍然需要等待服务器端一小段时间,以便于处理服务端的 FIN 包。

当服务器获取到了客户端的 FIN_ACK 后,它就直接关闭连接。而客户端也会在超时后静默关闭。此时双方均成功获取对方的全部数据,没有造成任何危害。

这里有个很重要的点是,TCP 不会对 ACK 包来进行 ACK。例如服务端不会对客户端发来的 FIN_ACK 回复一个 FIN_ACK_ACK。


TCP 状态图

这里放两张TCP 双方的状态图,做完这些实验再去看它们就相当轻松了:

  • 建立连接时的三次握手

在这里插入图片描述

  • 释放连接时的四次挥手

在这里插入图片描述


代码实现

  • TCP发送端和接收端相关配置
//! Config for TCP sender and receiver
class TCPConfig {
  public:
    // 发送器和接收器缓冲区的默认容量。缓冲区容量指的是在给定时间内可以存储的最大数据量
    static constexpr size_t DEFAULT_CAPACITY = 64000;  //!< Default capacity
    // tcp数据报中payload部分最大容量限制
    static constexpr size_t MAX_PAYLOAD_SIZE = 1000;   //!< Conservative max payload size for real Internet
    // 默认的重传超时时间,以毫秒为单位。
    // 当TCP发送器向接收器传输数据时,它期望在规定的超时时间内收到一个确认(ACK)。如果发送器在超时时间内没有收到确认,它会重新传输数据
    static constexpr uint16_t TIMEOUT_DFLT = 1000;     //!< Default re-transmit timeout is 1 second
    // 数据包在放弃之前允许的最大重传次数。如果发送器在经过指定的重传尝试次数后仍未收到确认,它会认为连接不可靠并采取适当的措施
    static constexpr unsigned MAX_RETX_ATTEMPTS = 8;   //!< Maximum re-transmit attempts before giving up
    // 用于保存重传超时的初始值,以毫秒为单位。它指定发送器在重新传输数据之前应等待ACK的时间
    // 由于重传超时时间会在网络拥塞的时候动态增加,因此当重置超时重传计数器时,需要将重传超时时间恢复为初始值 
    uint16_t rt_timeout = TIMEOUT_DFLT;       //!< Initial value of the retransmission timeout, in milliseconds
    // 接收和发送缓冲区默认大小
    size_t recv_capacity = DEFAULT_CAPACITY;  //!< Receive capacity, in bytes
    size_t send_capacity = DEFAULT_CAPACITY;  //!< Sender capacity, in bytes
    // 初始序列号,如果没有设置,那么会采用随机值策略
    std::optional<WrappingInt32> fixed_isn{};
};
  • TCPConnection.h
//! \brief A complete endpoint of a TCP connection
class TCPConnection {
  private:
    // TCP相关配置信息
    TCPConfig _cfg;
    // 初始化TCP接收端和发送端
    TCPReceiver _receiver{_cfg.recv_capacity};
    TCPSender _sender{_cfg.send_capacity, _cfg.rt_timeout, _cfg.fixed_isn};

    //! outbound queue of segments that the TCPConnection wants sent
    // 数据报队列,用于存放希望发送出去的TCP数据报
    std::queue<TCPSegment> _segments_out{};

    //! Should the TCPConnection stay active (and keep ACKing)
    //! for 10 * _cfg.rt_timeout milliseconds after both streams have ended,
    //! in case the remote TCPConnection doesn't know we've received its whole stream?
    // 主动发起TCP连接断开的一方,是否需要在FIN_WAIT_2状态后,等待2MSL会,防止自己发出的ACK超时或丢失,导致另一方不断重传FIN
    // 该值在TCP连接建立时被设置为true,在本次TCP连接销毁时被设置为false
    bool _linger_after_streams_finish{true};
    // 记录距离最后一次接受到TCP数据报过了多久
    size_t _time_since_last_segment_received_ms{0};
    // 当前TCP连接是否处于活动状态
    bool _is_active{true};
    ...
};
  • TCPConnection.cc
// 发送窗口剩余空闲空间
size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }

// 已经发送但是还没有ack的字节数量
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_ms; }

// 当前TCP连接是否存活
bool TCPConnection::active() const { return _is_active; }

// 将待发送的数据包加上期望接受到数据的ackno和当前自己作为接收端的滑动窗口大小(TCP是全双工协议,因此每一端既作为发送端也作为接收端)
void TCPConnection::_trans_segments_to_out_with_ack_and_win() {
    // 将等待发送的数据包加上本地的 ackno 和 window size
    while (!_sender.segments_out().empty()) {
        // 先把放入传输队列中的tcp报文取出
        TCPSegment seg = _sender.segments_out().front();
        _sender.segments_out().pop();
        // 判断当前是否处于Listen状态,即还没有建立TCP连接 -- 下面条件不满足说明处于LISTEN状态
        if (_receiver.ackno().has_value()) {
            // 如果当前已经建立了TCP连接
            // 那么向接收方发送下一个期望收到的ackno和当前自己的接收窗口大小
            seg.header().ack = true;
            // ackno()返回的是按序到达的最后一个字节的seqno
            seg.header().ackno = _receiver.ackno().value();
            seg.header().win = _receiver.window_size();
        }
        // 把tcp报文重新放入传输队列
        _segments_out.push(seg);
    }
}

// 关闭sender端的写入通道
void TCPConnection::end_input_stream() {
    // 关闭发送端的写入流通道 -- 此时不能写,但是可以将写入缓冲区中剩余数据全部读取完毕
    _sender.stream_in().end_input();
    // 在输入流结束后,必须立即发送 FIN -- 如果写入缓冲区还存在数据,会先发送完毕,最后发送FIN
    _sender.fill_window();
    // 将等待发送的数据包加上本地的 ackno 和 window size
    _trans_segments_to_out_with_ack_and_win();
}

// 建立连接
void TCPConnection::connect() {
    // 第一次调用 _sender.fill_window 将会发送一个 syn 数据包
    _sender.fill_window();
    // TCP连接激活
    _is_active = true;
    // 携带本地ackno和windowsize
    _trans_segments_to_out_with_ack_and_win();
}

// TCPConnection的析构函数
TCPConnection::~TCPConnection() {
    try {
        // 如果TCP连接处于激活状态,那么关闭连接
        if (active()) {
            cerr << "Warning: Unclean shutdown of TCPConnection\n";
            _set_rst_state(false);
        }
    } catch (const exception &e) {
        std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
    }
}

// 关闭连接 -- 参数: 是否需要发送RST包来终止TCP连接或清除异常状态
void TCPConnection::_set_rst_state(bool send_rst) {
    if (send_rst) {
        // 发送一个RST包,通知对端接受者立即终止本次TCP连接
        TCPSegment rst_seg;
        rst_seg.header().rst = true;
        _segments_out.push(rst_seg);
    }
    // 关闭输入输出流
    _receiver.stream_out().set_error();
    _sender.stream_in().set_error();
    // 连接已经彻底断开,所以不需要再等待2MSL秒了,该标志设置为false
    _linger_after_streams_finish = false;
    // 设置TCP连接为不活跃状态
    _is_active = false;
}

RST(Reset)包是TCP(传输控制协议)中的一种特殊类型的数据包,它的作用是用于终止TCP连接或清除异常状态。RST包在TCP连接中具有以下作用:

  1. 终止连接:当一方(发送方或接收方)希望立即中止TCP连接时,它可以发送一个RST包。接收方收到RST包后,会立即关闭连接,不再继续交换数据。
  2. 异常处理:RST包也被用于处理异常情况。例如,当一个TCP连接收到不期望的或错误的数据,或者连接处于无效状态,接收方可能会发送RST包来重置连接并回到初始状态。
  3. 同步连接:在TCP的三次握手过程中,如果接收方收到一个不是处于"SYN-RECEIVED"状态的连接请求(SYN包),它会发送一个RST包作为响应,以拒绝连接。
  4. 发送方超时:当TCP发送方发送数据并等待确认(ACK)超过一定的时间,它可能会认为连接已经失效,发送RST包来终止连接。

总之,RST包在TCP连接中用于清除异常状态、立即终止连接以及拒绝不合法的连接请求,从而保障连接的可靠性和稳定性。

// 发送数据
size_t TCPConnection::write(const string &data) {
    // 向写入缓冲区写入data
    size_t write_size = _sender.stream_in().write(data);
    // 根据对端接收缓冲区大小控制数据发送量大小
    _sender.fill_window();
    // 将等待发送的数据包加上本地的 ackno 和 window size
    _trans_segments_to_out_with_ack_and_win();
    return write_size;
}

// 接收TCP数据报
void TCPConnection::segment_received(const TCPSegment &seg) {
    _time_since_last_segment_received_ms = 0;
    // 如果发来的是一个 ACK 包,则无需发送 ACK
    bool need_send_ack = seg.length_in_sequence_space();

    // 读取并处理接收到的数据
    // _receiver 足够鲁棒以至于无需进行任何过滤
    _receiver.segment_received(seg);

    // 如果接收到的是 RST 包,则直接终止
    //! NOTE: 当 TCP 处于任何状态时,均需绝对接受 RST。因为这可以防止尚未到来数据包产生的影响
    if (seg.header().rst) {
        _set_rst_state(false);
        return;
    }

    // 确保在处理接收到的TCP段之前,发送器没有待发送的TCP段
    assert(_sender.segments_out().empty());
    // 如果收到了 ACK 包,则更新 _sender 的状态并补充发送数据
    // NOTE: _sender 足够鲁棒以至于无需关注传入 ack 是否可靠
    if (seg.header().ack) {
        _sender.ack_received(seg.header().ackno, seg.header().win);
        // _sender.fill_window(); // 这行其实是多余的,因为已经在 ack_received 中被调用了,不过这里显示说明一下其操作
        // 如果原本需要发送空ack,并且此时 sender 发送了新数据,则停止发送空ack
        if (need_send_ack && !_sender.segments_out().empty())
            need_send_ack = false;
    }

    // 如果是 LISEN 到了 SYN
    // 接收器初次接收到SYN包,并且此时发送器还处于关闭状态,所以当前由Listen转为了SYN_SENT状态
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::SYN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::CLOSED) {
        // 此时肯定是第一次调用 fill_window,因此会发送 SYN + ACK
        connect();
        return;
    }
   
    // 判断 TCP 断开连接时是否时需要等待
    // CLOSE_WAIT  -- 该状态只有server端才会存在,因此将_linger_after_streams_finish设置为false
    // 因为server端无需在close连接的时候等待2MSL毫秒
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::SYN_ACKED)
        _linger_after_streams_finish = false;

    // 如果到了准备断开连接的时候。服务器端先断
    // CLOSED
    // _linger_after_streams_finish==false确保让服务器端先断开连接,因为该参数在客户端为true
    // 因此下面的条件对于客户端而言不满足
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED && !_linger_after_streams_finish) {
        _is_active = false;
        return;
    }

    // 如果收到的数据包里没有任何数据,则这个数据包可能只是为了 keep-alive
    // note: TCP 不会对 ACK 包来进行 ACK -- 但是ACK可以携带在数据包中,因此针对这种情况,我们需要进行回复
    if (need_send_ack)
        _sender.send_empty_segment();
    _trans_segments_to_out_with_ack_and_win();
}

//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
// 参数: 距离上次调用该方法过了多少毫秒
void TCPConnection::tick(const size_t ms_since_last_tick) {
    assert(_sender.segments_out().empty());
    // 定时调用发送器的tick方法
    _sender.tick(ms_since_last_tick);
    // 如果重传计数超过了设定的最大次数
    if (_sender.consecutive_retransmissions() > _cfg.MAX_RETX_ATTEMPTS) {
        // 在发送 rst 之前,需要清空可能重新发送的数据包
        _sender.segments_out().pop();
        // 发送RST包,通知对端终止本次TCP连接,并且关闭自端的TCP连接
        _set_rst_state(true);
        return;
    }
    // 转发可能重新发送的数据包
    _trans_segments_to_out_with_ack_and_win();
    _time_since_last_segment_received_ms += ms_since_last_tick;

    // 如果处于 TIME_WAIT 状态并且超时,则可以静默关闭连接 -- 下面的条件是针对客户端而言的
    // 因为只有客户端的_linger_after_streams_finish参数才会为true
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED && _linger_after_streams_finish &&
        _time_since_last_segment_received_ms >= 10 * _cfg.rt_timeout) {
        _is_active = false;
        _linger_after_streams_finish = false;
    }
}
  • tcp_state.cc --> 获取当前接收端和发送端的TCP状态
// TCP协议栈由接收器和发送器组成,TCP连接生命周期所有状态如下面case枚举的状态所示
// 每个状态由一组接收器和发送器状态组成
TCPState::TCPState(const TCPState::State state) {
    switch (state) {
        case TCPState::State::LISTEN:
            _receiver = TCPReceiverStateSummary::LISTEN;
            _sender = TCPSenderStateSummary::CLOSED;
            break;
        case TCPState::State::SYN_RCVD:
            _receiver = TCPReceiverStateSummary::SYN_RECV;
            _sender = TCPSenderStateSummary::SYN_SENT;
            break;
        case TCPState::State::SYN_SENT:
            _receiver = TCPReceiverStateSummary::LISTEN;
            _sender = TCPSenderStateSummary::SYN_SENT;
            break;
        case TCPState::State::ESTABLISHED:
            _receiver = TCPReceiverStateSummary::SYN_RECV;
            _sender = TCPSenderStateSummary::SYN_ACKED;
            break;
        case TCPState::State::CLOSE_WAIT:
            _receiver = TCPReceiverStateSummary::FIN_RECV;
            _sender = TCPSenderStateSummary::SYN_ACKED;
            _linger_after_streams_finish = false;
            break;
        case TCPState::State::LAST_ACK:
            _receiver = TCPReceiverStateSummary::FIN_RECV;
            _sender = TCPSenderStateSummary::FIN_SENT;
            _linger_after_streams_finish = false;
            break;
        case TCPState::State::CLOSING:
            _receiver = TCPReceiverStateSummary::FIN_RECV;
            _sender = TCPSenderStateSummary::FIN_SENT;
            break;
        case TCPState::State::FIN_WAIT_1:
            _receiver = TCPReceiverStateSummary::SYN_RECV;
            _sender = TCPSenderStateSummary::FIN_SENT;
            break;
        case TCPState::State::FIN_WAIT_2:
            _receiver = TCPReceiverStateSummary::SYN_RECV;
            _sender = TCPSenderStateSummary::FIN_ACKED;
            break;
        case TCPState::State::TIME_WAIT:
            _receiver = TCPReceiverStateSummary::FIN_RECV;
            _sender = TCPSenderStateSummary::FIN_ACKED;
            break;
        case TCPState::State::RESET:
            _receiver = TCPReceiverStateSummary::ERROR;
            _sender = TCPSenderStateSummary::ERROR;
            _linger_after_streams_finish = false;
            _active = false;
            break;
        case TCPState::State::CLOSED:
            _receiver = TCPReceiverStateSummary::FIN_RECV;
            _sender = TCPSenderStateSummary::FIN_ACKED;
            _linger_after_streams_finish = false;
            _active = false;
            break;
    }
}

// 接收器具备的状态
namespace TCPReceiverStateSummary {
const std::string ERROR = "error (connection was reset)";
const std::string LISTEN = "waiting for SYN: ackno is empty";
const std::string SYN_RECV = "SYN received (ackno exists), and input to stream hasn't ended";
const std::string FIN_RECV = "input to stream has ended";
}  // namespace TCPReceiverStateSummary

// 发送器具备的状态
namespace TCPSenderStateSummary {
const std::string ERROR = "error (connection was reset)";
const std::string CLOSED = "waiting for stream to begin (no SYN sent)";
const std::string SYN_SENT = "stream started but nothing acknowledged";
const std::string SYN_ACKED = "stream ongoing";
const std::string FIN_SENT = "stream finished (FIN sent) but not fully acknowledged";
const std::string FIN_ACKED = "stream finished and fully acknowledged";
}  // namespace TCPSenderStateSummary
  • 三次握手转换图中每个状态对应的接收者和发送者状态
    在这里插入图片描述
  • 四次挥手转换图中每个状态对应的接收者和发送者状态
    在这里插入图片描述
  1. TCPReceiverStateSummary命名空间包含以下状态及其含义:

    • ERROR: 表示连接处于错误状态,可能由于某种异常情况导致连接重置。
    • LISTEN: 表示连接处于监听状态,即等待收到对方的SYN标志,ackno为空。
    • SYN_RECV: 表示连接接收到对方的SYN标志(ackno存在),并且输入流(stream)还没有结束。
    • FIN_RECV: 表示输入流(stream)已经结束,接收到对方的FIN标志,即将关闭连接。
  2. TCPSenderStateSummary命名空间包含以下状态及其含义:

    • ERROR: 表示连接处于错误状态,可能由于某种异常情况导致连接重置。
    • CLOSED: 表示连接处于关闭状态,等待发送端(Sender)发起连接(尚未发送SYN标志)。
    • SYN_SENT: 表示连接已经开始(已发送SYN标志),但尚未收到对方的确认。
    • SYN_ACKED: 表示连接正在进行中,已经收到对方的ACK确认。
    • FIN_SENT: 表示连接已经完成(发送了FIN标志),但尚未完全收到对方的确认。
    • FIN_ACKED: 表示连接已经完成并且完全收到了对方的确认。
// 判断接收端当前处于什么状态
string TCPState::state_summary(const TCPReceiver &receiver) {
    if (receiver.stream_out().error()) {
        return TCPReceiverStateSummary::ERROR;
    } else if (not receiver.ackno().has_value()) {
        return TCPReceiverStateSummary::LISTEN;
    } else if (receiver.stream_out().input_ended()) {
        return TCPReceiverStateSummary::FIN_RECV;
    } else {
        return TCPReceiverStateSummary::SYN_RECV;
    }
}
  1. TCPReceiver状态判断:

    • 如果接收器的输出流(stream_out)标记了错误(error()为真),则表示接收器处于错误状态。
    • 在之前条件不满足的基础上,如果接收器的确认号(ackno())为空(没有值),则表示接收器处于LISTEN状态。
    • 在之前条件不满足的基础上,如果接收器的输出流(stream_out)已经结束(input_ended()为真),则表示接收器处于FIN_RECV状态,即接收器已经接收到FIN标志,即将关闭连接。
    • 如果以上条件都不满足,则表示接收器处于SYN_RECV状态,即接收器已经接收到SYN标志。
  • 单单针对接收器而言,只要接收到了SYN包,那么下面就可以正常接收数据包了,但是此时三次握手流程是否完毕,取决于发送器是否收到了所发SYN包的ACK回应。
  • 单单针对接收器而言,只要接收到了FIN包,那么下面就会停止接收任何其他数据包了,但是此时四次握手流程是否完毕,取决于发送器是否收到了所发FIN包的ACK回应。
// 判断发送端当前处于什么状态
string TCPState::state_summary(const TCPSender &sender) {
    if (sender.stream_in().error()) {
        return TCPSenderStateSummary::ERROR;
    } else if (sender.next_seqno_absolute() == 0) {  
        return TCPSenderStateSummary::CLOSED;
    } else if (sender.next_seqno_absolute() == sender.bytes_in_flight()) {
        return TCPSenderStateSummary::SYN_SENT;
    } else if (not sender.stream_in().eof()) {
        return TCPSenderStateSummary::SYN_ACKED;
    } else if (sender.next_seqno_absolute() < sender.stream_in().bytes_written() + 2) {
        // CLOSE_WAIT状态中,发送器的状态为SYN_ACKED
        return TCPSenderStateSummary::SYN_ACKED;
    } else if (sender.bytes_in_flight()) {
        return TCPSenderStateSummary::FIN_SENT;
    } else {
        return TCPSenderStateSummary::FIN_ACKED;
    }
}
  1. TCPSender状态判断:

    • 如果发送器的输入流(stream_in)标记了错误(error()为真),则表示发送器处于错误状态。
    • 在之前条件不满足的基础上,如果发送器的下一个序列号(next_seqno_absolute())为0,表示发送器处于CLOSED状态,即尚未建立连接。
    • 在之前条件不满足的基础上,如果发送器的下一个序列号等于发送器当前发送的数据大小(bytes_in_flight()),表示发送器处于SYN_SENT状态,即发送器已发送SYN标志,等待对方回复。
    • 在之前条件不满足的基础上,如果发送器的输入流(stream_in)没有结束(eof()为假),或者输入流已经结束并且下一个序列号小于已写入输入流的数据大小加2,则表示发送器处于SYN_ACKED状态,即已经收到对方的ACK确认。
    • 在之前条件不满足的基础上,当前还有数据在传输中(bytes_in_flight()不为0),则表示发送器处于FIN_SENT状态,即发送器已发送FIN标志,正在等待对方回复。
    • 如果以上条件都不满足,并且没有数据在传输中(bytes_in_flight()为0),则表示发送器处于FIN_ACKED状态,即已经收到对方的FIN确认。
  • 发送器只要发送了SYN包或者FIN包,那么就会进入SYN_SENT或者FIN_SENT的状态 ,然后等待接收对应SYN包或者FIN包的ACK回应,从而转变为SYN_ACKED或者FIN_ACKED状态。
  • 发送器进入SYN_ACKED状态,表示本端的握手流程结束,可以进入正常数据收发阶段,此时接收器(SYN_RECV)和发送器(SYN_ACKED)状态一直保持不变。
  • 发送器进入FIN_ACKED状态,表示本端的挥手流程结束,连接已经进入关闭状态,此时接收器状态为FIN_RECV。

完整流程追踪

下面我们配合代码和图示来看看三次握手和四次挥手分别发生在代码中何处:

  • 三次握手
    在这里插入图片描述
  1. tcp_connection.cc 文件中的connect函数负责建立tcp连接,发送SYN包给服务器端
// 建立连接
void TCPConnection::connect() {
    // 第一次调用 _sender.fill_window 将会发送一个 syn 数据包
    _sender.fill_window();
    // TCP连接激活
    _is_active = true;
    // 携带本地ackno和windowsize
    _trans_segments_to_out_with_ack_and_win();
}
  1. tcp_connection.cc 文件中的sgement_received函数中接收到客户端发来的SYN包后,状态由Listen到SYN_RECV, 然后回复SYN和ACK
     // 接收服务端发来的SYN包
     _receiver.segment_received(seg);
     ...
    // 如果是 LISEN 到了 SYN
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::SYN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::CLOSED) {
        // 此时肯定是第一次调用 fill_window,因此会发送 SYN + ACK
        connect();
        return;
    }
  1. tcp_connection.cc 文件中的sgement_received函数中接收到服务端发来的SYN包后,回复服务端一个ACK
     // 接收服务端发来的SYN包
     _receiver.segment_received(seg);
    ...
    // 处理服务端发送来的ack
    if (seg.header().ack) {
        _sender.ack_received(seg.header().ackno, seg.header().win);
        ...
    }
    ...
    // 发送ACK进行回应 -- 回应服务端的SYN包,无需对ACK包进行ack
    if (need_send_ack)
        _sender.send_empty_segment();
    _trans_segments_to_out_with_ack_and_win();

  • 四次挥手

在这里插入图片描述

  1. tcp_sender.cc文件中如果client发现当前发送通道被关闭,则发送FIN包
void TCPSender::fill_window() {
         ...
        /**
         * 读取好后,如果满足以下条件,则增加 FIN
         *  1. 从来没发送过 FIN
         *  2. 输入字节流处于 EOF
         *  3. window 减去 payload 大小后,仍然可以存放下 FIN
         */
        if (!_set_fin_flag && _stream.eof() && payload.size() + _outgoing_bytes < curr_window_size)
            _set_fin_flag = segment.header().fin = true;
        // 将payload载入tcp报文结构体对象
        segment.payload() = Buffer(move(payload));
        ...
        // 发送组装好的TCP报文
        _segments_out.push(segment);
        ...
}
  1. tcp_received.cc 文件中服务端检测接收到FIN包,关闭接收流,设置_linger_after_streams_finish =false,然后回复一个ACK
void TCPReceiver::segment_received(const TCPSegment &seg) {
   ...
  // check fin
  // tcp头中fin标志被设置了 -- 记录结束序列号
  if (seg.header().fin)
    fin_seq_ = unwrap(seg.header().seqno, isn_.value(), seqno_) +
               seg.length_in_sequence_space();
  ... 
  // 最后一个参数fin : 关闭接收流
  reassembler_.push_substring(seg.payload().copy(), index, seg.header().fin);
  ...
}

tcp_connection.cc 文件中的sgement_received函数中只要接收到一个不是为空的ACK包的情况下,都会自动回复一个ACK包:

     // 接收服务端发来的SYN包 -- 如上文segment_received函数所示
     _receiver.segment_received(seg);
    ...
    // 判断 TCP 断开连接时是否时需要等待
    // CLOSE_WAIT  -- 此时服务端进入CLOSE_WAIT状态,无需等待2MSL毫秒,将该参数设置为false
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::SYN_ACKED)
        _linger_after_streams_finish = false;
    ...
    // 如果不是为空的ACK包,则发送ACK进行回应
    if (need_send_ack)
        _sender.send_empty_segment();
    _trans_segments_to_out_with_ack_and_win();
  1. 同理,tcp_sender.cc文件中如果如果服务端发现没有数据需要发送了,那么关闭发送通道,然后发送FIN包
void TCPSender::fill_window() {
         ...
        /**
         * 读取好后,如果满足以下条件,则增加 FIN
         *  1. 从来没发送过 FIN
         *  2. 输入字节流处于 EOF
         *  3. window 减去 payload 大小后,仍然可以存放下 FIN
         */
        if (!_set_fin_flag && _stream.eof() && payload.size() + _outgoing_bytes < curr_window_size)
            _set_fin_flag = segment.header().fin = true;
        // 将payload载入tcp报文结构体对象
        segment.payload() = Buffer(move(payload));
        ...
        // 发送组装好的TCP报文
        _segments_out.push(segment);
        ...
}
  1. tcp_received.cc 文件中中的sgement_received函数中,客户端接收服务端的FIN报文,然后关闭自己的接收流,随后回复一个ACK
void TCPReceiver::segment_received(const TCPSegment &seg) {
   ...
  // check fin
  // tcp头中fin标志被设置了 -- 记录结束序列号
  if (seg.header().fin)
    fin_seq_ = unwrap(seg.header().seqno, isn_.value(), seqno_) +
               seg.length_in_sequence_space();
  ... 
  // 最后一个参数fin : 关闭接收流
  reassembler_.push_substring(seg.payload().copy(), index, seg.header().fin);
  ...
}

此时客户端进入TIME_WAIT阶段,下面等待2MSL秒超时后,彻底关闭己端连接:

void TCPConnection::tick(const size_t ms_since_last_tick) {
    ...
    // 如果处于 TIME_WAIT 状态并且超时,则可以静默关闭连接 --- 针对客户端而言
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED && _linger_after_streams_finish &&
        _time_since_last_segment_received_ms >= 10 * _cfg.rt_timeout) {
        _is_active = false;
        _linger_after_streams_finish = false;
    }
}
  1. tcp_received.cc 文件中服务端检测接收到所发FIN包对应的ACK包, 然后彻底断开已端连接
// 接收TCP数据报
void TCPConnection::segment_received(const TCPSegment &seg) {
    ... 
    if (seg.header().ack) {
        _sender.ack_received(seg.header().ackno, seg.header().win);
        ...
    }
    ...
    
    // 如果到了准备断开连接的时候。服务器端先断
    // CLOSED  -- 此时服务端进入CLOSED状态,彻底断开本端的TCP连接
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED && !_linger_after_streams_finish) {
        _is_active = false;
        return;
    }
    ...
}

测试

在 build 目录下执行 make 后执行 make check_lab4:

在这里插入图片描述

benchmark:

在这里插入图片描述

网卡调试:

  1. 安装抓包软件wireshark (ubuntun 20.04)
sudo apt install wireshark
  1. 采用终端抓包方式
# 抓到的数据包存放于 /tmp/debug.raw 中,便于后期分析
sudo tshark -Pw /tmp/debug.raw -i tun144
  1. 图形界面抓包
# tun144 和 145 是 CS144 模拟出的两个虚拟网卡。这两张网卡可以互通
sudo wireshark

在这里插入图片描述
4. 之后分别在两个终端下键入命令以相互连接

# 在 tun144 网段下启动 server 监听,其地址为 169.254.144.9:9090
./apps/tcp_ipv4 -l 169.254.144.9 9090

# 在 tun145 网段下启动 client,其地址为 169.254.145.9,向 169.254.144.9:9090 发起连接
./apps/tcp_ipv4 -d tun145 -a 169.254.145.9 169.254.144.9 9090

之后便可以在 wireshark 中捕获其数据包来往:

在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/836386.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python系统学习1-3-变量,运算符

1、变量 变量&#xff1a;关联一个对象的标识符 学习目标&#xff1a;学会画变量的内存图 命名规则:字母数字下划线&#xff0c;所有单词小写&#xff0c;单词之间下划线隔开 赋值&#xff1a;创建一个变量或改变一个变量关联的数据。 语法&#xff1a;变量名数据&#xf…

vue运行在IE浏览器空白报错SCRIPT1006: 缺少‘)‘ -【vue兼容IE篇】

其他浏览器均正常&#xff0c;但是切换ie模式&#xff0c;打开空白&#xff0c;F12打开报错缺少‘)‘ &#xff0c;如下图 在搜狗浏览器下点开报错&#xff1a;定格在crypto-js处 解决&#xff1a; 步骤一&#xff1a;使用npm安装babel-polyfill 依赖&#xff08;已安装了可忽…

Java与Kotline Funcation函数与参数函数的详解

一.介绍 在现在以IDE为开发工具的时代&#xff0c;各种开发语言都有&#xff0c;kotlin的语法势头比较强&#xff0c;今天我们将介绍在项目中出现比较多的两种函数&#xff0c;一种是参数函数&#xff0c;还有一种就是Function函数 如果你不了匿名函数请阅读以下文档&#xff…

IT 运营分析 (ITOA)

IT 运营 &#xff08;ITOps&#xff09; 是指向组织实施、管理、交付和支持 IT 服务&#xff0c;ITOps 可帮助组织维护和运行所需的所有技术工具&#xff0c;以保持业务活动以最高质量正常运行&#xff0c;同时降低成本。 一些常见的 ITOps 过程是&#xff1a; 问题整改&…

el-table 去掉边框(修改颜色)

原始&#xff1a; 去掉表格的border属性&#xff0c;每一行下面还会有一条线&#xff0c;并且不能再拖拽表头 为了满足在隐藏表格边框的情况下还能拖动表头&#xff0c;修改相关css即可&#xff0c;如下代码 <style lang"less"> .table {//避免单元格之间出现白…

Clickhouse 优势与部署

一、clickhouse简介 1.1 clickhouse介绍 ClickHouse的背后研发团队是俄罗斯的Yandex公司&#xff0c;2011年在纳斯达克上市&#xff0c;它的核心产品是搜索引擎。我们知道&#xff0c;做搜索引擎的公司营收非常依赖流量和在线广告&#xff0c;所以做搜索引擎的公司一般会并行推…

【LeetCode-简单】剑指 Offer 52. 两个链表的第一个公共节点

题目 输入两个链表&#xff0c;找出它们的第一个公共节点。 如下面的两个链表&#xff1a; 在节点 c1 开始相交。 输入&#xff1a;intersectVal 8, listA [4,1,8,4,5], listB [5,0,1,8,4,5], skipA 2, skipB 3 输出&#xff1a;Reference of the node with value 8 输…

想参加华为杯竞赛、高教社杯和数学建模国赛的小伙伴看过来

本文目录 ⭐ 赛事介绍⭐ 辅导比赛 ⭐ 赛事介绍 ⭐ 参赛好处 ⭐ 辅导比赛 ⭐ 写在最后 ⭐ 赛事介绍 华为杯全国研究生数学建模竞赛是由华为公司主办的一项面向全国研究生的数学建模竞赛。该竞赛旨在通过实际问题的建模和解决&#xff0c;培养研究生的创新能力和团队合作精神&a…

【ASP.NET MVC】使用动软(四)(12)

一、筛选器类和Cookie实现路由 需解决的问题&#xff1a; 网站登录往往需要用户名密码验证&#xff0c;为避免重复验证&#xff0c;一般采用Cookie 、Session等技术来保持用户的登录状态&#xff1a; Session是在服务端保存的一个数据结构&#xff0c;用来跟踪用户的状态&…

EtherCAT转MODBUS RTU/RS485/232总线协议网关

产品功能 JM-ECT-RTU是一款EtherCAT从站功能的通讯网关。该产品主要功能是将EtherCAT网络和MODBUS-RTU网络连接起来。 JM-ECT-RTU网关连接到EtherCAT总线中作为从站使用&#xff0c;连接到MODBUS-RTU总线中作为主站或从站使用。 本网关产品将基于MODBUS 的设备或串行RS-232/…

10分钟理解React生命周期

前言 学习React&#xff0c;生命周期很重要&#xff0c;我们了解完生命周期的各个组件&#xff0c;对写高性能组件会有很大的帮助。 一、简介 React /riˈkt/ 组件的生命周期指的是组件从创建到销毁过程中所经历的一系列方法调用。这些方法可以让我们在不同的时刻执行特定的…

科班应届生,我选择来黑马提升技能!

不论是因为对未来的迷茫和焦虑&#xff0c;还是对生活的现状不满意&#xff0c;又或者是想完善自己的专业知识&#xff0c;亦或是跨界迎接新的挑战&#xff0c;都可以来黑马…… 学科 | JavaEE 校区 | 武汉 薪资 | 10k&#xff08;应届生&#xff09; 黑马程序员的学弟、学妹…

【方法】Excel表格如何拆分数据?

当需要把多个数据逐个填到Excel单元格的时候&#xff0c;我们可以利用Excel的数据拆分功能&#xff0c;可以节省不少时间。 小编以下面的数据为例&#xff0c;看看如何进行数据拆分。 首先&#xff0c;要选择数字所在的单元格&#xff0c;然后依次点击菜单栏中的“数据”>…

Django实现音乐网站 ⑹

使用Python Django框架制作一个音乐网站&#xff0c; 本篇主要是在添加编辑过程中对后台歌手功能优化及表模型名称修改、模型继承内容。 目录 表模型名称修改 模型继承 创建抽象基类 其他模型继承 更新表结构 歌手新增、编辑优化 表字段名称修改 隐藏单曲数和专辑数 姓…

Redis 单线程VS多线程

面试题 redis到底是单线程还是多线程&#xff1f;IO多路复用是什么&#xff1f;redis为什么快&#xff1f; Redis单线程 是什么 Redis的版本很多3.x、4.x、6.x&#xff0c;版本不同架构也是不同的&#xff0c;不限定版本问是否单线程也不太严谨。 1、版本3.x &#xff0c;最…

中外人工智能专家共话大语言模型与 AI 创新

文章目录 一、前言二、主要内容三、总结 &#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、前言 智源社区活动&#xff0c;中外人工智能专家共话大语言模型与 AI 创新。 对谈书目&#xff1a; 《大模型时代》&#xff0c;龙志勇、黄雯 著&#xf…

.Net6 Web Core API --- Autofac -- AOP

目录 一、AOP 封装 二、类拦截 案例 三、接口拦截器 案例 AOP拦截器 可开启 类拦截器 和 接口拦截器 类拦截器 --- 只有方法标注 virtual 标识才会启动 接口拦截器 --- 所有实现接口的方法都会启动 一、AOP 封装 // 在 Program.cs 配置 builder.AddAOPExt();//自定义 A…

软件测试方案模板

第一章 概述 ​ 软件的错误是不可避免的&#xff0c;所以必须经过严格的测试。通过对本软件的测试&#xff0c;尽可能的发现软件中的错误&#xff0c;借以减少系统内部各模块的逻辑&#xff0c;功能上的缺陷和错误&#xff0c;保证每个单元能正确地实现其预期的功能。检测和排…

docker端口映射详解(随机端口、指定IP端口、随意ip指定端口、指定ip随机端口)

目录 docker端口映射详解 一、端口映射概述&#xff1a; 二、案例实验&#xff1a; 1、-P选项&#xff0c;随机端口 2、使用-p可以指定要映射到的本地端口。 Local_Port:Container_Port&#xff0c;任意地址的指定端口 Local_IP:Local_Port:Container_Port 映射到指定地…

亚马逊买家号付款异常被锁定是什么原因?怎么解决?

如果在亚马逊上&#xff0c;买家号出现付款异常&#xff0c;可能会导致账号被锁定或受限。这通常是为了保护消费者和卖家的利益&#xff0c;以及防范欺诈行为。 1、常见的买家号付款异常情况可能包括&#xff1a; 付款信息不一致&#xff1a;如果买家提供的付款信息与其账号信…