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()) {// 先把放入传输队列中的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 中捕获其数据包来往:

在这里插入图片描述


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

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

相关文章

aardio:用 WebView 模仿 mdict 界面

aardio&#xff1a;用 WebView 模仿 mdict 界面 import win.ui; /*DSG{{*/ mainForm win.form(text"aardio2";right889;bottom467) mainForm.add( button{cls"button";text"go";left335;top22;right399;bottom41;z2}; button2{cls"button…

SpringBoot使用redis作为缓存的实例

目录 什么是缓存&#xff1f; 缓存的作用&#xff1f; 缓存的成本&#xff1f; 实际项目中的应用 代码展示 什么是缓存&#xff1f; 缓存就是数据交换的缓冲区&#xff08;称作Cache [ kʃ ] &#xff09;&#xff0c;是存贮数据的临时地方&#xff0c;一般读写性能较高。 缓…

HTML+CSS+JavaScript:两种方法实现商品价格筛选效果

一、需求 鼠标点击上方菜单栏中不同的价格区间&#xff0c;自动筛选出价格符合条件的商品&#xff0c;并渲染在页面中 二、代码素材 以下是缺失JS部分的代码&#xff0c;感兴趣的小伙伴可以先自己试着写一写 <!DOCTYPE html> <html lang"en"><head…

【绪论0】

#pic_center R 1 R_1 R1​ R 2 R^2 R2 目录 知识框架No.0 引言No.1 操作系统的概念功能和定义一、操作系统的概念和定义1、电脑的演变 二、操作系统的功能和目标 No.2 操作系统的特征一、并发二、共享三、虚拟四、异步 No.3 操作系统的发展与分类一、手工操作阶段二、批处理阶段…

Kubernetes高可用集群二进制部署(四)部署kubectl和kube-controller-manager、kube-scheduler

Kubernetes概述 使用kubeadm快速部署一个k8s集群 Kubernetes高可用集群二进制部署&#xff08;一&#xff09;主机准备和负载均衡器安装 Kubernetes高可用集群二进制部署&#xff08;二&#xff09;ETCD集群部署 Kubernetes高可用集群二进制部署&#xff08;三&#xff09;部署…

《吐血整理》高级系列教程-吃透Fiddler抓包教程(21)-如何使用Fiddler生成Jmeter脚本-上篇

1.简介 我们知道Jmeter本身可以录制脚本&#xff0c;也可以通过BadBoy&#xff0c;BlazeMeter等工具进行录制&#xff0c;其实Fiddler也可以录制Jmter脚本&#xff08;而且有些页面&#xff0c;由于安全设置等原因&#xff0c;使用Jmeter直接无法打开录制时&#xff0c;这时就…

类的6个默认成员函数 构造函数

类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。 空类中真的什么都没有吗&#xff1f;并不是&#xff0c;任何类在什么都不写时&#xff0c;编译器会自动生成以下6个默认成员函数。 默认成员函数&#xff1a;用户没有显式实现&#xff0c;编译器会生…

利用线程池多线程并发实现TCP两端通信交互,并将服务端设为守护进程

文章目录 实现目标实现步骤封装日志类封装线程池封装线程封装锁封装线程池 TCP通信的接口和注意事项accept TCP封装任务客户端Client.hppClient.cc 服务端Server.hpp Server.cc实现效果 守护进程服务端守护进程化 实现目标 利用线程池多线程并发实现基于TCP通信的多个客户端与…

JavaSE【继承和多态】(1)(重点:初始化、pretected封装、组合)

一、继承 继承 (inheritance) 机制 &#xff1a;是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特 性 的基础上进行扩展&#xff0c;增加新功能 &#xff0c;这样产生新的类&#xff0c;称 派生类 。 继承呈现了面向对象程序设计的层次结…

微波光子的参数:动态范围

微波光子的参数&#xff1a;无杂散动态范围 无杂散动态范围的定义 微波光子链路中的非线性失真主要由电光调制器的非线性调制产生&#xff0c;这些非线性失真可以分为谐波失真和交调失真两类。图1.2&#xff08;a&#xff09;给出了光信号在调制器内被一个频率为10 GHz的射频…

阿里云负载均衡SLB网络型NLB负载均衡架构性能详解

阿里云网络型负载均衡NLB是阿里云推出的新一代四层负载均衡&#xff0c;支持超高性能和自动弹性能力&#xff0c;单实例可以达到1亿并发连接&#xff0c;帮您轻松应对高并发业务。网络型负载均衡NLB具有超强性能、自动弹性伸缩、高可用、TCPSSL卸载、多场景流量分发和丰富的高级…

HCIP中期实验

1、该拓扑为公司网络&#xff0c;其中包括公司总部、公司分部以及公司骨干网&#xff0c;不包含运营商公网部分。 2、设备名称均使用拓扑上名称改名&#xff0c;并且区分大小写。 3、整张拓扑均使用私网地址进行配置。 4、整张网络中&#xff0c;运行OSPF协议或者BGP协议的设备…

devops-发布vue前端项目

回到目录 将使用jenkinsk8s发布前端项目 1 环境准备 node环境 在部署jenkins的服务器上搭建node环境 node版本 # 1.拉取 https://nodejs.org/download/release/v20.4.0/node-v20.4.0-linux-x64.tar.gz# 2.解压到/usr/local目录下 sudo tar xf v20.4.0.tar.gz -C /usr/loc…

面试热题(最长回文子串)

给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串 输入&#xff1a;s "babad" 输出&#xff1a;"bab" 最长回文子串以前的博客已经讲过KMP算法以及比较不常见的Manacher算法…

uni-app、H5实现瀑布流效果封装,列可以自定义

文章目录 前言一、效果二、使用代码三、核心代码总结 前言 最近做项目需要实现uni-app、H5实现瀑布流效果封装&#xff0c;网上搜索有很多的例子&#xff0c;但是代码都是不够完整的&#xff0c;下面来封装一个uni-app、H5都能用的代码。在小程序中&#xff0c;一个个item渲染…

【ASP.NET MVC】使用动软(二)(10)

一、添加动软生成工程 按前文添加动态到工程 双击动软 完成新建数据库服务器后 &#xff0c;需要关闭重新打开 选择简单三层&#xff0c;注意保存位置 注意切换数据库&#xff1a; 生成后拷贝五个文件夹到工程目录 注意目录结构&#xff1a; 添加四个项目到原来的工程&…

React 在 html 中 CDN 引入(包含 antd、axios ....)

一、简介 cdn 获取推荐 https://unpkg.com&#xff0c;unpkg 是一个快速的全球内容交付网络&#xff0c;适用于 npm 上所有内容。 【必备】react 相关 cdn。附&#xff1a;github 官方文档获取、现阶段官方文档 CDN 网址。 <script crossorigin src"https://unpkg.com…

Vue3和TypeScript项目-移动端兼容

1 全局安装typescript 2 检测安装成功 3 写的是ts代码&#xff0c;但是最后一定要变成js代码&#xff0c;才能在浏览器使用 这样就会多一个js文件 3 ts语法 数组语法 对象语法 安装vue3项目 成功后进入app。安装依赖。因为我们用的是脚手架&#xff0c;要引入东西的时候不需要…

I.MX6ULL_Linux_驱动篇(44)linux MISC驱动

MISC 驱动也叫做杂项驱动&#xff0c;也就是当我们板子上的某些外设无法进行分类的时候就可以使用 MISC 驱动。 MISC 驱动其实就是最简单的字符设备驱动&#xff0c;通常嵌套在 platform 总线驱动中&#xff0c;实现复杂的驱动&#xff0c;本章我们就来学习一下 MISC 驱动的编写…

Homer:一个简单的静态主页

什么是 Homer ? Homer 是一个完全静态的 html/js 仪表板&#xff0c;基于一个简单的 yaml 配置文件。它旨在由 HTTP 服务器提供服务&#xff0c;如果您直接通过 file:// 协议打开 index.html&#xff0c;它将无法工作。 安装 在群晖上以 Docker 方式安装。 在注册表中搜索 h…