Lab4:The TCP Connection
# Lab4:The TCP Connection
lab4.pdf (cs144.github.io) (opens new window)
在这个 lab 中,我们要站在全局的视角上,利用之前实现的 Sender 和 Receiver 来实现 TCP 的连接。
过程中参考了一部分 Stanford-CS144-Sponge 笔记 - Lab 4: The TCP Connection | 吃着土豆坐地铁的博客 (opens new window)
首先在做这个 lab 之前,我们必须对 Sender、Receiver、Connection 的状态机十分熟悉,可以说这个 lab 就是对着他们的 FSM 图来进行实现的。
做之前可以看一下下面几篇文章:
下面是我自己画的一张 TCP 连接的状态机,一共 11 个状态(字有点丑):
每一个状态都将由 Sender、Receiver 和 Connection 三者的状态唯一定义,我们将在 segment_received()
对其实现。
下面先来实现一下其他的一些函数。
先是写入数据,也就是我方的 Sender 需要发送的数据。
size_t TCPConnection::write(const string &data) { // 返回写入长度
if (data.empty()) {
return 0;
}
size_t res = _sender.stream_in().write(data); // 往发送方的 bytestream 写入数据
_sender.fill_window(); // 根据接收窗口发送数据,数据放进 _segments_out
send_data(); // 发送数据到 receiver
return res;
}
而其中的 send_data() 执行了发送的操作,同时还更新了一下我方 Receiver 的操作。具体来说,我们会将 Sender 发送队列中的数据移到 Connection 的队列中;另外,至于后面的发送传输就不是传输层所关心的事了。
void TCPConnection::send_data() {
// 发送 sender 的信息
while (!_sender.segments_out().empty()) { // 将 sender 发送队列中的数据放入 connection 的发送队列中
auto seg = _sender.segments_out().front();
_sender.segments_out().pop();
if (_receiver.ackno().has_value()) { // 将自己这里的 receiver 的数据写入首部
seg.header().ack = true;
seg.header().ackno = _receiver.ackno().value();
seg.header().win = _receiver.window_size() > std::numeric_limits<uint16_t>::max() ? std::numeric_limits<uint16_t>::max() : _receiver.window_size(); // 限定最大值
}
segments_out().emplace(seg); // 放入 connection 的发送队列
}
// 如果对方的发送完毕了
if (_receiver.stream_out().input_ended()) {
if (_sender.stream_in().eof() == false) {// 我方的发送还未结束(被动连接)
_linger_after_streams_finish = false; // 不用进入 TIME_WAIT
} else if (_sender.stream_in().eof() == true && _sender.bytes_in_flight() == 0) { // 我方已经发送完了,且全送达了
// 如果我方不用进入 TIME_WAIT 或者 我方 TIME_WAIT 结束
if (_linger_after_streams_finish == false || _time_since_last_segment_received >= 10 * _cfg.rt_timeout)
_active = false; // 我方关闭连接 cleanly
}
}
}
接下来我们要实现计时的函数,其维护了一个变量,来记录距离上一次传输的报文被接受的时间间隔
void TCPConnection::tick(const size_t ms_since_last_tick) {
if (_active == false) return;
_time_since_last_segment_received += ms_since_last_tick;
_sender.tick(ms_since_last_tick);
if (_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS) { // 重传次数超过上限
reset_connection(); // 设置 RST
return;
}
send_data();
}
还有设置 RST 位的函数,我们将在适当时候调用该函数,来设置并发送 RST 报文段和设置 error,以及关闭连接,例如重传次数超过上限、unclearly 地关闭连接
void TCPConnection::reset_connection() {
// 发送 RST 段
TCPSegment seg;
seg.header().rst = true;
segments_out().emplace(seg);
// 标记错误 和 设置 _active
_sender.stream_in().set_error();
_receiver.stream_out().set_error();
_active = false;
}
下面是其他的一些比较简单的函数,就不多做解释了
bool TCPConnection::active() const {
return _active;
}
size_t TCPConnection::remaining_outbound_capacity() const {
return _sender.stream_in().remaining_capacity();
}
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;
}
最后就是重头 segment_received()
函数。
上文说过我们通过 sender、receiver、connection 状态来定义唯一的连接状态。
其中 sender 和 receiver 的状态定义在之前的实验手册中已经有给出了,下面我们来简单谈一谈 connection 的状态以及八股重灾区 TCP 连接的 3 次握手和 4 次挥手。
在端系统之间利用 TCP 进行通信之前,会先建立 TCP 的连接,其通过 3 次握手来实现,下面是一张来自自顶向下书上的图。
客户主机先主动发起连接,他初始化了一个数据的起始序号 client_isn
,并标记了 SYN
,具体含义在之前的 lab 中已经有解释过了。服务器接收到这个建立请求的报文段后,如果可以建立,那么他就会为这段连接分配相应的资源,此时服务器是 Passive Open,而这第一个报文段的发送也就是第一次握手;
之后服务器会对这个报文段进行确认,并发送对应的 ACK 确认报文段 client_isn + 1
。与此同时 TCP 是双向的连接,因此服务器也要向客户主机发起一个连接的请求,他会进行和客户主机第一次握手类似的行为,初始化了数据的起始序号 server_isn
。然而,前面所说的这两件事可以合并在一起做,即服务器主机发起第二次握手,其用来确认客户主机的连接请求和发起由服务器向客户主机的连接,这个合并操作也被称为捎带;
同理,客户主机也要对接受到的来自服务器的连接请求报文段发送 ACK 确认 server_isn + 1
,同时还可以捎带客户要发送给服务器的数据,这被称为第三次握手。
至此,C/S 双方便建立起了 TCP 连接,可以依此来传输数据了。
而当数据传输结束,那么对应的也需要拆除连接,也就需要四次挥手了,下面也是一张来自自顶向下书上的图片:
如图所示,客户主动先发送带有 FIN 的报文段来主动结束,此时服务器是 Passive Close,并回复对应的 ACK 报文段;而随后当服务器的数据页传输结束后,它也会向客户发送带有 FIN 的报文段,客户也会回复对应的 ACK 报文段,这一来一回一共四回,被称为四次挥手。
上面的描述会让 TCP 关闭连接上看上去非常简单,但事实却不是如此,在客户等待由服务器发送回来的 ACK 时,还可能会接收到来此服务器的 FIN,抑或是同时接受到 ACK 和 FIN...而这些都会引起连接进入不同的状态。在这里我们先关注图中有一个特别的部分 “定时等待”,客户此时也就对应着状态机中的 TIME_WAIT 状态。
为什么要设置定时等待呢?
试想如果在接受到 FIN 报文段,并发送 ACK 报文段之后,客户直接关闭连接会有什么事情发生呢?很显然客户发送的 ACK 未必能保证到达目的服务器,那么服务器长时间未能接收到 ACK 的话,它就会不断的重复发送 FIN 报文段,当重复次数到达上限后就会设置 RST 以及 ERROR,并且 unclealy 地关闭连接。此时服务器的 FIN 其实已经被正确接受了,但最终却是以错误的方式关闭了连接,而这显然不是我们想要的结果。
为了尽可能地避免这种现象,在主动关闭连接的一方会在此时进入定时等待状态,其就像个无情的回复机器,一旦接收到 FIN 就会回复对应的 ACK 确认报文段。
需要注意的是,这种现象只能尽可能的避免,具体可以先了解一下两军问题 (opens new window),因为不能保证发出去的所有 ACK 都能被接受,只要连接关闭依赖于最后一次 ACK,那么就不能完美解决此问题。
在上文中,端系统是否要进入定时等待,即 TIME_WAIT 状态,就用变量 _linger_after_streams_finish
来表示,其值初始默认为 true,而这也就是 connection 的状态部分。
接下来我们对 TCP 连接过程中的 11 种状态以及出现错误的 1 种状态进行定义(可以直接参考 tcp_state.cc
)
TCP 连接状态 | Receiver 状态 | Sender 状态 | Connection 状态 |
---|---|---|---|
LISTEN | LISTEN | CLOSED | TRUE |
SYN_REVD | SYN_REVD | SYN_SENT | TRUE |
SYN_SENT | LISENT | SYN_SENT | TRUE |
ESTABLISHED | SYN_REVD | SYN_ACKED | TRUE |
CLOSE_WAIT | FIN_REVD | SYN_ACKED | FALSE |
LAST_ACK | FIN_REVD | FIN_SENT | FALSE |
FIN_WAIT_1 | SYN_REVD | FIN_SENT | TRUE |
FIN_WAIT_2 | SYN_REVD | FIN_ACKED | TRUE |
CLOSING | FIN_REVD | FIN_SENT | TRUE |
TIME_WAIT | FIN_REVD | FIN_ACKED | TRUE |
CLOSED | FIN_REVD | FIN_ACKED | FALSE |
RESET | ERROR | ERROR | FALSE |
另外,在 CLOSED 和 RESET 状态中,连接的状态 active
为 false,其余状态都为 true。
了解了上面的内容后就可以开始实现 segment_received()
啦,我们要关注在不同状态下的接收到报文段的各种行为。
// sender、receiver、connection 确定唯一状态,对照 tcp_state.cc
void TCPConnection::segment_received(const TCPSegment &seg) {
if (_active == false) return;
// 获取 sender 状态
auto get_sender_state = [&](const TCPSender &sender) {
if (sender.stream_in().error()) {
return "ERROR";
} else if (sender.next_seqno_absolute() == 0) {
return "CLOSED";
} else if (sender.next_seqno_absolute() == sender.bytes_in_flight()) {
return "SYN_SENT";
} else if (not sender.stream_in().eof()) {
return "SYN_ACKED";
} else if (sender.next_seqno_absolute() < sender.stream_in().bytes_written() + 2) {
return "SYN_ACKED";
} else if (sender.bytes_in_flight()) {
return "FIN_SENT";
} else {
return "FIN_ACKED";
}
};
// 获取 receiver 状态
auto get_receiver_state = [&](const TCPReceiver &receiver) {
if (receiver.stream_out().error()) {
return "ERROR";
} else if (not receiver.ackno().has_value()) {
return "LISTEN";
} else if (receiver.stream_out().input_ended()) {
return "FIN_RECV";
} else {
return "SYN_RECV";
}
};
std::string sen_st = get_sender_state(_sender);
std::string rev_st = get_receiver_state(_receiver);
_time_since_last_segment_received = 0; // 重置时间
// 收到 RST 段
if (seg.header().rst == true) {
_sender.stream_in().set_error();
_receiver.stream_out().set_error();
_active = false;
}
// LISTEN
else if (rev_st == "LISTEN" && sen_st == "CLOSED" && _linger_after_streams_finish == true) {
// 被动连接 Passive open(进入SYN_REVD)
if (seg.header().syn) {
_receiver.segment_received(seg);
connect(); // 会发送 ACK 和 SYN
}
}
// SYN_REVD
else if (rev_st == "SYN_RECV" && sen_st == "SYN_SENT" && _linger_after_streams_finish == true)
{
// sender 接受 ACK(进入ESTABLISHED)
_receiver.segment_received(seg);
_sender.ack_received(seg.header().ackno, seg.header().win);
}
// SYN_SENT
else if (rev_st == "LISTEN" && sen_st == "SYN_SENT" && _linger_after_streams_finish == true)
{
// client 收到 ACK 和 SYN,发送 ACK 后进入 ESTABLISHED
if (seg.header().ack == true && seg.header().syn == true) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
_sender.send_empty_segment(); // 通过空包,来发送 ACK
send_data();
}
// client 也作为 server,收到了 SYN,发送 SYN 和 ACK 后进入 SYN_REVD
else if (seg.header().syn == true && seg.header().ack == false) {
_receiver.segment_received(seg);
_sender.send_empty_segment();
send_data();
}
}
// ESTABLISHED
else if (rev_st == "SYN_RECV" && sen_st == "SYN_ACKED" && _linger_after_streams_finish == true) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
if (seg.length_in_sequence_space() > 0) {
_sender.send_empty_segment(); // 发送 ACK
}
_sender.fill_window();
send_data();
}
// FIN_WAIT_1(发送完毕,但接受未完毕)
else if (rev_st == "SYN_RECV" && sen_st == "FIN_SENT" && _linger_after_streams_finish == true) {
// 收到 FIN,进入 CLOSING
if (seg.header().fin == true && seg.header().ack == false) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
}
// 收到 FIN、ACK,发送 ACK,进入 TIME_WAIT
else if (seg.header().fin == true && seg.header().ack == true) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
_sender.send_empty_segment();
send_data();
}
// 收到 ACK,进入 FIN_WAIT_2
else if (seg.header().fin == false && seg.header().ack == true) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
send_data();
}
}
// FIN_WAIT_2,收到 FIN,发送 ACK,进入 TIME_WAIT
else if (rev_st == "SYN_RECV" && sen_st == "FIN_ACKED" && _linger_after_streams_finish == true) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
_sender.send_empty_segment();
send_data();
}
// TIME_WAIT
else if (rev_st == "FIN_RECV" && sen_st == "FIN_ACKED" && _linger_after_streams_finish == true) {
// 收到 FIN 的话就发送 ACK(不断开连接)
if (seg.header().fin == true) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
_sender.send_empty_segment();
send_data();
}
}
// CLOSE_WAIT
else if (rev_st == "FIN_RECV" && sen_st == "SYN_ACKED" && _linger_after_streams_finish == false) {
if (seg.header().fin) { // 此状态接收到 FIN 报文段说明需要重传
_sender.send_empty_segment();
}
// 继续发送数据
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
send_data();
}
// LAST_ACK
else if (rev_st == "FIN_RECV" && sen_st == "FIN_SENT" && _linger_after_streams_finish == false) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
if (_sender.bytes_in_flight() == 0) {
_active = false;
}
}
// CLOSING
else if (rev_st == "FIN_RECV" && sen_st == "FIN_SENT" && _linger_after_streams_finish == true) {
_sender.ack_received(seg.header().ackno, seg.header().win);
_receiver.segment_received(seg);
send_data();
}
}
由于测试样例很多,会有很多的 corner case......
有一个点我调了很久,也就是在进入 CLOSE_WAIT 状态后,此时如果接受到的报文段带有 FIN,那么我们要重传 ACK 确认报文段,其说明我们先前传过的 ACK 没能正确到达对方。
# 实验结果
(TEST 我是在 win 上用 WSL2 挂代理跑的,不然很有可能会 TIME_OUT)