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连接中具有以下作用:
- 终止连接:当一方(发送方或接收方)希望立即中止TCP连接时,它可以发送一个RST包。接收方收到RST包后,会立即关闭连接,不再继续交换数据。
- 异常处理:RST包也被用于处理异常情况。例如,当一个TCP连接收到不期望的或错误的数据,或者连接处于无效状态,接收方可能会发送RST包来重置连接并回到初始状态。
- 同步连接:在TCP的三次握手过程中,如果接收方收到一个不是处于"SYN-RECEIVED"状态的连接请求(SYN包),它会发送一个RST包作为响应,以拒绝连接。
- 发送方超时:当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
- 三次握手转换图中每个状态对应的接收者和发送者状态
- 四次挥手转换图中每个状态对应的接收者和发送者状态
-
TCPReceiverStateSummary
命名空间包含以下状态及其含义:ERROR
: 表示连接处于错误状态,可能由于某种异常情况导致连接重置。LISTEN
: 表示连接处于监听状态,即等待收到对方的SYN标志,ackno为空。SYN_RECV
: 表示连接接收到对方的SYN标志(ackno存在),并且输入流(stream)还没有结束。FIN_RECV
: 表示输入流(stream)已经结束,接收到对方的FIN标志,即将关闭连接。
-
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;}
}
-
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;}
}
-
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。
完整流程追踪
下面我们配合代码和图示来看看三次握手和四次挥手分别发生在代码中何处:
- 三次握手
- 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();
}
- 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;}
- 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();
- 四次挥手
- 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);...
}
- 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();
- 同理,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);...
}
- 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;}
}
- 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:
网卡调试:
- 安装抓包软件wireshark (ubuntun 20.04)
sudo apt install wireshark
- 采用终端抓包方式
# 抓到的数据包存放于 /tmp/debug.raw 中,便于后期分析
sudo tshark -Pw /tmp/debug.raw -i tun144
- 图形界面抓包
# 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 中捕获其数据包来往: