CS144-Lab4 计算机网络:TCP Connection的实现

TCP Connection

TCP Connection的部分本身并不难,这个实验的主要核心是学习使用tsharkwireshark一类的工具对TCP的网络状况进行分析,找出正确或错误的数据包。

需要实现的逻辑

在这个实验中我们需要将前面写的TCP SenderTCP Receiver两个部分的逻辑进行合并,使得两者之间可以进行数据的传输。

除了几个可以直接调用前面实验函数的函数以外,我们主要需要完成的我认为是收到某个报文以后的处理函数segment_received(const TCPSegment &seg)和时间函数tick()

实现细节

接受报文

对于接受报文这个函数,首先通过对实验报告的分析,我们可以知道我们主要要做的事情可以分为以下三个大逻辑:

对报文本身合法性的分析
  • 记录收到这个报文的时间,无论对错
  • 检查这个报文是否是带RST标志的报文,如果是的话则直接断开连接
  • 如果是在LISTEN状态的时候接受到这个报文的,则要判断对方是否连接

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 首先无论如何,刷新收到报文的时间
_time_since_last_segment_received = 0;

// 然后先检查这个报文是否出错,如果出错则直接返回
if (seg.header().rst) {
_receiver.stream_out().set_error();
_sender.stream_in().set_error();
_is_active = false;
return;
}

// 如果TCP连接处于LISTEN状态,只接受SYN报文,并且返回一个SYN + ACK的报文
if (not _receiver.ackno().has_value()) {
if (seg.header().syn) {
_sender.fill_window();
} else {
return;
}
}
对报文进行处理

接受这个报文,如果带有ACK信息则更新对方已经确认了的ackno和对方当前的window_size

1
2
3
4
5
6
7
// 接受这个报文
_receiver.segment_received(seg);

// 对ACK报文进行确认更新,用于下一次更新的确认
if (seg.header().ack) {
_sender.ack_received(seg.header().ackno, seg.header().win);
}
正确的处理连接的关闭

这部分是我认为Lab4里面在理解上较难的部分。其中,TCP的断开分为三种不同的情况:

  1. 由于RST标志导致的强制退出(unclean shutdown)
  2. 正常的通讯结束而导致的关闭(clean shutdown)

但是对于第二种情况,我们可以进一步分为两种情况:

首先是最简单的四次挥手报文:

  1. Client发送完毕数据,告诉Server我结束(FIN)了
    Client客户端在给Server发送完毕了所有数据以后,主动发送FIN数据包,表示自己的数据已经发送完毕了。然后Server在收到ClientFIN报文并处理完毕以后则会返回一个FIN ACK报文,来告诉Client他发过来的数据已经在服务端被处理完成了。

  2. Server也发送完毕了数据,告诉Client我结束(FIN)了
    这个时候Server也会给Client发送一个FIN的报文,同样等待Client那边确认,如果Client发送了确认报文来确认这个ACK,则代表客户端那边也处理完了,这个时候按理来说首先提出数据发送完毕的Client就可以断开链接了。

Client:主动关闭

但是,服务端有可能收不到这最后一个ACK确认报文,从而导致自己一直在等待客户端向自己发送ACK确认报文。

为了避免这种情况,最简单的处理方法就是让ClientServer发送了FIN ACK报文以后不要急着断开连接,而是设置一个计时器,等待看看Server会不会重传FIN报文。

如果重传了FIN则代表Server并没有收到先前发送的FIN ACK,这个时候Client就需要重新发送一个ACK回去,告知Server可以断开连接了。

如果超过了计时器的时间,Client也没有收到Server的重传报文,那么我们就假设Server已经收到了FIN ACK,并且已经关闭了他那边的连接,这个时候Client就可以断开连接了。而这段计时器的等待时间,就是实验中的linger_time = 10 *_cfg.rt_timeout,这个时间往往是比Server超时重传的时间大很多的,也就留给了Server足够多的时间来重传FIN报文。

Server:被动关闭

服务端这边就很简单了,在发送完自己的FIN之后,只需要正常等待ClientACK确认报文,如果没有等到则重传FIN,如果等到了则直接断开连接。

连接关闭的代码实现

知道了上面的区分以后,我们实现起来就很简单了,只需要通过添加一个变量_linger_after_streams_finish来判断到底是对方先结束还是自己先结束。如果是对方先结束,则我们不需要等待linger_time,在后面收到了FIN报文以后直接断开连接即可。否则则需要在后面tick()函数的部分添加超时断开连接的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
// 接收到正确的EOF报文,代表对方发送过来的数据流已经结束了,但是自己还有数据要发送
// 因此需要等待自己的数据流发送完毕后才能关闭连接
if (_receiver.stream_out().eof() && not _sender.stream_in().eof()) {
_linger_after_streams_finish = false;
}

// _linger_after_streams_finish是false说明对方发送给我们的数据流已经全部被接受了
// 此时有_sender的eof和bytes_in_flight都为0,说明自己的数据流也已经全部发送完毕
// 因此可以关闭连接了
if (_sender.stream_in().eof() && bytes_in_flight() == 0 && not _linger_after_streams_finish) {
_is_active = false;
}
发送确认报文

这部分逻辑就很简单了,如果在接受了对方传来的有序列号消耗数据包以后,我们并没有数据要传输(即无法告知对方我们接受到了数据),那么我们就需要单独传输一个ACK数据包给对方,告知我们已经接收到了对方的数据。(如果对方发送给我们的是一个ACK数据包,我们则不需要回复,也就是收到了占用序号为零的包)

1
2
3
4
if (_sender.segments_out().empty() &&
(seg.length_in_sequence_space() || seg.header().seqno != _receiver.ackno())) {
_sender.send_empty_segment();
}

其中seg.header().seqno != _receiver.ackno()代表的是一种特殊情况,在TCP连接中,有的时候为了确认当前连接是否依旧有效,对方有可能会随机发送一个错误的序列号给我们,这个时候我们就需要回复一个ACK报文给对方,以此告知对方这个连接依旧是有效的,同时也可以让对方更新我们的窗口大小。

接受报文的代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void TCPConnection::segment_received(const TCPSegment &seg) {
// 首先无论如何,刷新收到报文的时间
_time_since_last_segment_received = 0;

// 然后先检查这个报文是否出错,如果出错则直接返回
if (seg.header().rst) {
_receiver.stream_out().set_error();
_sender.stream_in().set_error();
_is_active = false;
return;
}

// 如果TCP连接处于LISTEN状态,只接受SYN报文,并且返回一个SYN + ACK的报文
if (not _receiver.ackno().has_value()) {
if (seg.header().syn) {
_sender.fill_window();
} else {
return;
}
}

// 接受这个报文
_receiver.segment_received(seg);

// 对ACK报文进行确认更新,用于下一次更新的确认
if (seg.header().ack) {
_sender.ack_received(seg.header().ackno, seg.header().win);
}

// 接收到正确的EOF报文,代表对方发送过来的数据流已经结束了,但是自己还有数据要发送
// 因此需要等待自己的数据流发送完毕后才能关闭连接
if (_receiver.stream_out().eof() && not _sender.stream_in().eof()) {
_linger_after_streams_finish = false;
}

// _linger_after_streams_finish是false说明对方发送给我们的数据流已经全部被接受了
// 此时有_sender的eof和bytes_in_flight都为0,说明自己的数据流也已经全部发送完毕
// 因此可以关闭连接了
if (_sender.stream_in().eof() && bytes_in_flight() == 0 && not _linger_after_streams_finish) {
_is_active = false;
}

if (_sender.segments_out().empty() && (seg.length_in_sequence_space() || seg.header().seqno != _receiver.ackno())) {
_sender.send_empty_segment();
}

// 填装需要发送的报文
_push_out();
}

时间流动

另外一个需要注意的函数就是tick()函数了。其实这一部分的重要也主要是连带了前面接受报文部分的关闭连接,主要要注意的就是添加一个对linger_time的判断。整个tick()函数的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//! \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) {
_time_since_last_segment_received += ms_since_last_tick;
_sender.tick(ms_since_last_tick);
// 如果超时重传次数超过了最大重传次数,那么就直接关闭连接
if (_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS) {
_send_rst();
return;
}
// 在我方的数据包全部发送并且处理完毕以后,如果接受到了对方传来的EOF报文,并且等待了十倍的RTT时间都没有新的报文传来,则代表连接已经关闭
if (time_since_last_segment_received() >= _linger_time && _sender.stream_in().eof() &&
_receiver.stream_out().input_ended()) {
_is_active = false;
}
_push_out();
}

问题和难点

Lab4实验主要的难点感觉还是在即使跑通了前面大部分的基本测试,也还是有可能因为Lab2Lab3里面的疏忽,而导致后面模拟真实通讯的时候很容易难以下手。但是在掌握了Wireshark抓包一类的工具用法以后还是很容易发现问题所在并加以纠正的。

比如我在Lab3中,对于TCPSender在填充窗口大小的时候,一开始并不是设置了一个额外的变量fill_space来控制可以发送的空闲空间的大小,而是直接使用了ack_received方法中收到的最新窗口大小,忽略了bytes_in_flight()也需要考虑在窗口占用里面的问题。在使用Wireshark抓包的时候就明显发现了发送数据包的序号要远超于接收方的确认序号

空闲窗口判断错误

而这个问题也在我通过修改Lab3对应空闲窗口大小的逻辑之后得到了解决。

我还遇到过的第二个问题就是在小窗口的情况下,没有正确处理链接的关闭。在通过Wireshark抓包以后可以看到

没有正确处理关闭

在发送方还没有给接收方发送完所有数据的时候,接收方就提前终止了自己的连接,这个问题主要出在tick()函数里面关于linger_time的逻辑错误,我并没有等到接收方接受到EOF就直接关闭了链接。错误代码如下:

1
2
3
if (time_since_last_segment_received() >= _linger_time && _sender.stream_in().eof()) {
_is_active = false;
}

修改后的代码如下:

1
2
3
4
if (time_since_last_segment_received() >= _linger_time && _sender.stream_in().eof()
+ && _receiver.stream_out().input_ended()) {
_is_active = false;
}

整个Lab4的代码可以在Github的仓库查看:

tcp_connection.cc
tcp_connection.hh


CS144-Lab4 计算机网络:TCP Connection的实现
https://halc.top/p/10e77bc5
作者
HalcyonAzure
发布于
2023年4月10日
许可协议