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

CS 144 Lab Four -- the TCP connection

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 bytessize_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连接销毁时被设置为falsebool _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 sizewhile (!_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()返回的是按序到达的最后一个字节的seqnoseg.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) {// 向写入缓冲区写入datasize_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 包,则无需发送 ACKbool 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 发送了新数据,则停止发送空ackif (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 + ACKconnect();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参数才会为trueif (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_ACKEDreturn 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 到了 SYNif (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::SYN_RECV &&TCPState::state_summary(_sender) == TCPSenderStateSummary::CLOSED) {// 此时肯定是第一次调用 fill_window,因此会发送 SYN + ACKconnect();return;}
  1. tcp_connection.cc 文件中的sgement_received函数中接收到服务端发来的SYN包后,回复服务端一个ACK
     // 接收服务端发来的SYN包_receiver.segment_received(seg);...// 处理服务端发送来的ackif (seg.header().ack) {_sender.ack_received(seg.header().ackno, seg.header().win);...}...// 发送ACK进行回应 -- 回应服务端的SYN包,无需对ACK包进行ackif (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毫秒,将该参数设置为falseif (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 中捕获其数据包来往:

在这里插入图片描述


相关文章:

CS 144 Lab Four -- the TCP connection

CS 144 Lab Four -- the TCP connection TCPConnection 简述TCP 状态图代码实现完整流程追踪 测试 对应课程视频: 【计算机网络】 斯坦福大学CS144课程 Lab Three 对应的PDF: Lab Checkpoint 4: down the stack (the network interface) TCPConnection 简述 TCPConnection 需…...

在Volo.Abp微服务中使用SignalR

假设需要通过SignalR发送消息通知&#xff0c;并在前端接收消息通知的功能 创建SignalR服务 在项目中引用 abp add-package Volo.Abp.AspNetCore.SignalR在Module文件中添加对模块依赖 [DependsOn(...typeof(AbpAspNetCoreSignalRModule))] public class IdentityApplicati…...

数据可视化(七)常用图表的绘制

1. #seaborn绘制常用图表 #折线图 #replot&#xff08;x&#xff0c;y&#xff0c;kind&#xff0c;data&#xff09; #lineplot&#xff08;x&#xff0c;y&#xff0c;data&#xff09; #直方图 #displot&#xff08;data&#xff0c;rug&#xff09; #条形图 #barplot&…...

【ARM 常见汇编指令学习 8 - dsb sy 指令及 dsb 参数介绍】

文章目录 ARM dsb sy 指令 上篇文章&#xff1a;ARM 常见汇编指令学习 7 - LDR 指令与LDR伪指令及 mov指令 下篇文章&#xff1a;ARM 常见汇编指令学习 9 - 缓存管理指令 DC 与 IC ARM dsb sy 指令 数据同步屏障是一种特殊类型的内存屏障。 只有当DSB指令执行完毕后&#xff…...

YOLOv5本地模型训练报错解决

报错解决 页面文件太小&#xff0c;无法完成操作 训练过程中&#xff0c;发生下图所示的报错&#xff0c;同时pycharm崩溃 1. 更改虚拟内存 进入高级系统设置&#xff0c;应该都会进&#xff0c;就不说过程了 设置虚拟内存大小 2. 减小占用内容大小 新建一个fixNvPe.py程序…...

tomcat p12证书另存为nginx .crt证书和.key私钥

tomcat p12证书另存为nginx .crt证书和.key私钥 Tomcat使用的.pfx或.keystore文件都是私钥及公钥证书一起的&#xff0c;通过pin保证安全&#xff1b;nginx只需要使用.pem或.crt公钥证书文件和.key私钥即可&#xff0c;如果原ssl证书不方便重新下载&#xff0c;在已有tomcat证…...

Docker的userland-proxy

前言 Docker针对端口映射前后有两种方案&#xff0c;一种是1.7版本之前docker-proxyiptables DNAT 的方式&#xff1b;另一种则是1.7版本(及之后)提供的完全由iptables DNAT实现的端口映射。不过在目前docker 1.9.1中&#xff0c;前一种方式依旧是默认方式。但是从Docker 1.7版…...

uniapp封装request请求

在基础文件里面创建一个api文件 在创建两个 js文件 http.js 里面封装 request 请求 let baseUrl https://white.51.toponet.cn; //基地址 export const request (options {}) > {//异步封装接口&#xff0c;使用Promise处理异步请求return new Promise((resolve, reject…...

Go如何构建高效API接口| 青训营

Go语言作为一个高效的静态类型语言&#xff0c;在构建API服务时也表现出了很大的优势。本文将介绍如何使用Go语言构建高效的API服务&#xff0c;帮助开发者更好地应对日益增长的API需求。 一、选择适合的框架 首先&#xff0c;选择适合的框架是构建高效API服务的重要一步。在…...

【云原生K8s】二进制部署单master K8s+etcd集群

一、实验设计 mater节点master01192.168.190.10kube-apiserver kube-controller-manager kube-scheduler etcd node节点node01192.168.190.20kubelet kube-proxy docker (容…...

TRUNC(截取)函数的用法

TRUNC函数在Oracle中用于截断日期、时间或数字的精度。其语法如下&#xff1a; 截取数字&#xff1a; TRUNC(number [, precision])其中&#xff1a; number 表示要截断的数字。 precision表示截断的精度。可以是负数、整数或者默认为空。正数表示保留小数位数&#xff0c;负…...

IELAB-网络工程师的路由答疑10问(1)

各位同学&#xff0c;我相信对于许多新学习的同学而言&#xff0c;在刚接触该的时候总会产生许多问题&#xff0c;今天 我们就简单讲解一下常见的几种问题&#xff1a; 什么是路由&#xff1f; 简单来讲&#xff0c;路由通常发生在网络层&#xff0c;为什么呢&#xff1f;首先…...

OpenLayers入门,OpenLayers加载TopoJson数据,使用行政区划边界作为示例

专栏目录: OpenLayers入门教程汇总目录 前言 本章讲解一下OpenLayers如何加载解析TopoJson格式的数据。 TopoJson介绍 TopoJson是用于表示地理空间数据的格式。是GeoJson格式的改进版,相比 GeoJSON 直接使用 Polygon、Point 之类的几何体来表示图形的方法,TopoJSON 中的…...

【图像去噪】基于原始对偶算法优化的TV-L1模型进行图像去噪研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

RISC-V基础之函数调用(五)函数递归调用及函数参数数量溢出(超出现有寄存器个数)约定(包含实例)

首先先解释一下栈在函数调用中的作用&#xff0c;更详细的部分请参照考研复习之数据结构笔记&#xff08;五&#xff09;栈和队列&#xff08;上&#xff09;&#xff08;包含栈的相关内容&#xff09;_管二狗赶快去工作&#xff01;的博客-CSDN博客 函数嵌套调用栈的作用是用…...

力扣:48. 旋转图像(Python3)

题目&#xff1a; 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&…...

HarmonyOS应用开发者基础与高级认证题库——中级篇

系列文章目录 HarmonyOS应用开发者基础与高级认证题库——基础篇 HarmonyOS应用开发者基础与高级认证题库——中级篇 文章目录 系列文章目录前言一、判断二、单选三、多选 前言 今天刚换了台果子手机就收到了华子鸿蒙开发认证邀请&#xff08;认证链接&#xff09;&#xff0…...

Python中实现多个列表、字典、元组、集合的连接

目录 目录 前言 一、列表 1、运算符 2、extend&#xff08;&#xff09;方法 3、解包操作 * 二、字典 1、update&#xff08;&#xff09;方法 2、解包操作 ** 三、元组 1、 运算符 2、解包操作 * 四、集合 1、union方法 2、| 运算符 3、解包操作 * 五、不同类…...

1005 继续(3n+1)猜想

描述 卡拉兹(Callatz)猜想已经在1001中给出了描述。在这个题目里&#xff0c;情况稍微有些复杂。 当我们验证卡拉兹猜想的时候&#xff0c;为了避免重复计算&#xff0c;可以记录下递推过程中遇到的每一个数。例如对 n3 进行验证的时候&#xff0c;我们需要计算 3、5、8、4、…...

基于图片、无人机、摄像头拍摄进行智能检测功能

根据要求进行无人机拍摄的视频或图片进行智能识别&#xff0c;开发过程需要事项 1、根据图片案例进行标记&#xff0c;进行模型训练 2、视频模型训练 开发语言为python 根据需求功能进行测试结果如下 根据车辆识别标记进行的测试结果截图 测经过查看视频 8月1日...

模型参数、模型存储精度、参数与显存

模型参数量衡量单位 M&#xff1a;百万&#xff08;Million&#xff09; B&#xff1a;十亿&#xff08;Billion&#xff09; 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的&#xff0c;但是一个参数所表示多少字节不一定&#xff0c;需要看这个参数以什么…...

在rocky linux 9.5上在线安装 docker

前面是指南&#xff0c;后面是日志 sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io -y docker version sudo systemctl start docker sudo systemctl status docker …...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...

VTK如何让部分单位不可见

最近遇到一个需求&#xff0c;需要让一个vtkDataSet中的部分单元不可见&#xff0c;查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行&#xff0c;是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示&#xff0c;主要是最后一个参数&#xff0c;透明度…...

uniapp微信小程序视频实时流+pc端预览方案

方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度​WebSocket图片帧​定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐​RTMP推流​TRTC/即构SDK推流❌ 付费方案 &#xff08;部分有免费额度&#x…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

GitFlow 工作模式(详解)

今天再学项目的过程中遇到使用gitflow模式管理代码&#xff0c;因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存&#xff0c;无论是github还是gittee&#xff0c;都是一种基于git去保存代码的形式&#xff0c;这样保存代码…...

day36-多路IO复用

一、基本概念 &#xff08;服务器多客户端模型&#xff09; 定义&#xff1a;单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用&#xff1a;应用程序通常需要处理来自多条事件流中的事件&#xff0c;比如我现在用的电脑&#xff0c;需要同时处理键盘鼠标…...