CS 144 Lab Four -- the TCP connection

CS 144 Lab Four -- the TCP connection

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


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

Lab Four 对应的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()) {// 从发送器的segments_out传输队列中取出待发送的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数据报放入TcpConnection的segments_out传输队列中_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 中捕获其数据包来往:

在这里插入图片描述


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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

利用Jmeter做接口测试全流程分析

利用Jmeter做接口测试怎么做呢&#xff1f;过程真的是超级简单。 明白了原理以后&#xff0c;把零碎的知识点填充进去就可以了。这篇文章就来介绍一下如何利用Jmeter做接口测试的流程&#xff0c;主要针对的是功能测试。暂不涉及到自动化测试和性能测试的内容。 一把来说&…

微信个人小程序申请 (AppID 和 AppSecret)

1. 登录微信公众平台 https://mp.weixin.qq.com/cgi-bin/loginpage?url%2Fcgi-bin%2Fhome%3Ft%3Dhome%2Findex%26lang%3Dzh_CN%26token%3D47421820 2. 右上角立即注册 3. 注册类型选择小程序 4. 账号信息 5. 邮箱激活 6. 小程序发布流程 7. 小程序信息 (前往填写) 8. 获取小程…

阿里云轻量应用服务器_2核2G3M_108元/年_性能测评

阿里云轻量应用服务器2核2G3M带宽108元一年&#xff0c;系统盘为50GB高效云盘&#xff1b;轻量服务器2核4G4M带宽&#xff0c;60GB高效云盘297.98元12个月。目前轻量应用服务器只有2核2G和2核4G有活动&#xff0c;阿里云百科分享阿里云轻量应用服务器入口&#xff1a; 目录 阿…

Flume拦截器

实现 Interceptor接口 方法1 是初始化: 方法2和3重载 拦截: 方法3 是关闭: 但是flume是通过内部类创建对象的

mongodb-win32-x86_64-2008plus-ssl-3.6.23-signed.msi

Microsoft Windows [版本 6.1.7601] 版权所有 (c) 2009 Microsoft Corporation。保留所有权利。C:\Users\Administrator>cd C:\MongoDB\Server\3.6\binC:\MongoDB\Server\3.6\bin> C:\MongoDB\Server\3.6\bin> C:\MongoDB\Server\3.6\bin>mongod --dbpath C:\Mongo…

springboot第34集:ES 搜索,nginx

#用search after解决深分页性能问题 #第一页 GET /bank/_search {"size": 10,"sort": [{"account_number": {"order": "asc"}}] }#第二页 GET /bank/_search {"size": 10,"sort": [{"account_numb…

ardupilot 三维向量如何进行旋转

目录 文章目录 目录摘要1.三维向量的旋转2.如何理解上面公式3.ardupilot中代码应用4.结论摘要 本节主要记录ardupilot中如何实现一个三维向量从一个坐标系转换到另外一个坐标系的过程,欢迎批评指正!!! 1.三维向量的旋转 这里需要特别注意,我们有时候需要把R系往B系转换,…

每天一个知识点——Normalization

这里结合大模型的学习&#xff0c;主要分析Layer Norm、RMS Norm和Deep Norm的异同&#xff0c;与此同时&#xff0c;究竟是在之前执行Normalization&#xff08;Pre-Norm&#xff09;还是之后执行&#xff08;Post-Norm&#xff09;&#xff0c;也是一个比较喜欢拿来讨论的知识…

编织人工智能:机器学习发展历史与关键技术全解析

文章目录 1. 引言1.1 机器学习的定义1.2 重要性和应用场景重要性应用场景 2. 机器学习的早期历史2.1 初期理论与算法感知机决策树 2.2 早期突破支持向量机神经网络初探 3. 21世纪初期的发展3.1 集成学习方法随机森林XGBoost 3.2 深度学习的崛起卷积神经网络&#xff08;CNN&…

Sketch打不开AI文件?转换方法在这里

1、对比设计软件 Sketch 与 AI 软件功能 Sketch 与 Illustrator 都是行业内优秀的矢量图形设计软件&#xff0c;各有千秋。Sketch 从 2010 年面世&#xff0c;专注 APP 界面设计&#xff0c;深受初学者与专业人士喜爱。Illustrator 拥有更悠久的历史&#xff0c;是处理复杂图标…

问道管理:A股缩量整理 新股上演久违暴涨模式

周三&#xff0c;大盘低开后震动&#xff0c;三大指数小幅跌落&#xff0c;创业板指相对偏强。 早盘开盘后&#xff0c;沪指、深证成指弱势震动&#xff0c;创业板指探底上升翻红&#xff0c;盘面热门乏善可陈。午后三大指数震动走弱&#xff0c;创业板指再度翻绿。医药板块活…

基于百度语音识别API智能语音识别和字幕推荐系统——深度学习算法应用(含全部工程源码)+测试数据集

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 翻译3. 格式转换4. 音频切割5. 语音识别6. 文本切割7. main函数 系统测试工程源代码下载其它资料下载 前言 本项目基于百度语音识别API&#xff0c;结合了语音识别、视频转换音频识别以及语句停顿…

性能测试jmeter连接数据库jdbc(sql server举例)

一、下载第三方工具包驱动数据库 1. 因为JMeter本身没有提供链接数据库的功能&#xff0c;所以我们需要借助第三方的工具包来实现。 &#xff08;有这个jar包之后&#xff0c;jmeter可以发起jdbc请求&#xff0c;没有这个jar包&#xff0c;也有jdbc取样器&#xff0c;但不能发起…

大模型的数据隐私问题有解了,浙江大学提出联邦大语言模型

作者 | 小戏、Python 理想化的 Learning 的理论方法作用于现实世界总会面临着诸多挑战&#xff0c;从模型部署到模型压缩&#xff0c;从数据的可获取性到数据的隐私问题。而面对着公共领域数据的稀缺性以及私有领域的数据隐私问题&#xff0c;联邦学习&#xff08;Federated Le…

ACM算法竞赛中在编辑器中使用输入输出样例-CPH

通用方法 我们可以在编辑器中创建三个文件&#xff0c;一个是main.cpp,一个是test.in,一个是test.out分别用来写代码&#xff0c;输入输入数据&#xff0c;显示输出数据 这种方法的好处是不需要插件&#xff0c;在任何编辑器中都可以实现&#xff0c;例如Devc,sublime,vscode…

HBase-读流程

创建连接同写流程。 &#xff08;1&#xff09;读取本地缓存中的Meta表信息&#xff1b;&#xff08;第一次启动客户端为空&#xff09; &#xff08;2&#xff09;向ZK发起读取Meta表所在位置的请求&#xff1b; &#xff08;3&#xff09;ZK正常返回Meta表所在位置&#x…

SQL常见命令语句

1.连接数据库 mysql (-h IP) -u root -p 密码2.查看数据库 show databases3.使用数据库 use db_name4.查看表 show tables [from db_name]5.查看表结构 desc tb_name6.创建、删除、选择数据库 create database db_namedrop database db_nameuse db_name7.数据类型 参考链…

Python-OpenCV中的图像处理-几何变换

Python-OpenCV中的图像处理-几何变换 几何变换图像缩放图像平移图像旋转仿射变换透视变换 几何变换 对图像进行各种几个变换&#xff0c;例如移动&#xff0c;旋转&#xff0c;仿射变换等。 图像缩放 cv2.resize() cv2.INTER_AREAv2.INTER_CUBICv2.INTER_LINEAR res cv2.r…

优化理论 | Time-Sharing Condition

版权声明 原创作品&#xff0c;整理不易&#xff0c;转载请标明出处。本篇推送更详细的内容介绍&#xff0c;可参见本人微信公众号“优化与博弈的数学原理”&#xff0c;公众号二维码参见文末。 编者按 OFDM系统中的功率分配问题是通信领域中的研究热点。本文重点考虑了面向…

7.5 详解批量规范化 对某个维度取平均值代码解读

一.举例计算均值、方差 假设我们有以下一组数据&#xff1a;[10, 15, 20, 25, 30]首先&#xff0c;我们计算均值&#xff0c;即将所有数据相加后除以数据的数量&#xff1a; **均值** (10 15 20 25 30) / 5 100 / 5 201.1标准差 接下来&#xff0c;我们计算标准差&…