本文记录了 Lab 3:the TCP sender 的实验过程

Lab 3: the TCP sender

实现 TCP 的发送侧,即 TCPSenderTCPSender 负责

  1. 根据 TCPReceiver 发回的窗口大小来改变自己的窗口大小
  2. ByteStream 中读取数据,将 stream 变成向外发送的 TCP segment(如果需要的话会在 segment 中带上 SYN/FIN 控制标志)
  3. 跟踪那些已经被发送但是还未被 receiver 确认的 segments,在 Lab 中称这些 segments 为 “outstanding” segments
  4. 如果一个 segment 在发送后经过一段时间没有被确认,那么重传这个 segment

思路

一开始,看了下实验指导书,发现不是很理解要怎么实现这几个接口,然后对着 test case 来理解这个 lab 要我们怎么实现,例如 send_connect.cc 这个文件

1
2
3
4
5
6
7
8
9
10
{
TCPConfig cfg;
WrappingInt32 isn(rd());
cfg.fixed_isn = isn;

TCPSenderTestHarness test{"SYN sent test", cfg};
test.execute(ExpectState{TCPSenderStateSummary::SYN_SENT});
test.execute(ExpectSegment{}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
test.execute(ExpectBytesInFlight{1});
}

它是测试 SYN 的发送,只发送了一个携带 SYN = true 的 segment,还记得 SYN 占用一个序列号嘛?因此 bytes_in_flight 应该等于 1。接下来是 SYN Ack test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
TCPConfig cfg;
WrappingInt32 isn(rd());
cfg.fixed_isn = isn;

TCPSenderTestHarness test{"SYN acked test", cfg};
test.execute(ExpectState{TCPSenderStateSummary::SYN_SENT});
test.execute(ExpectSegment{}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
test.execute(ExpectBytesInFlight{1});
test.execute(AckReceived{WrappingInt32{isn + 1}});
test.execute(ExpectState{TCPSenderStateSummary::SYN_ACKED});
test.execute(ExpectNoSegment{});
test.execute(ExpectBytesInFlight{0});
}

这个时候需要通过 ack_received 函数处理收到的 acknowinfow_size。wrong ack test 需要你你在 ack_received 函数中判断收到的 ackno 是否合法。

最后在 SYN acked, data 中,在发送 SYN、确认之后还发送了一个带有数据的 segment,再对它进行确认。


Task #1

首先,我们先不用管 segment 的重传。为了完成必要的功能,我们需要在头文件中添加必要的私有成员变量,例如窗口大小 window_size、上一个被确认的序列号 last_ackno、已经被发送但还未被确认的个数 bytes_in_flight 等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//! last ackno
uint64_t _last_ackno{0};

//! number of seqnos are occupied by segments sent but not yet acknowledged
uint64_t _bytes_in_flight{0};

//! window size, default window size is 1 before gotten a ACK from the receiver. (See: FAQ)
uint16_t _window_size{1};

//! “outstanding” segments: segments have been sent but not yet acknowledged by the receiver
std::queue<TCPSegment> _segments_outstanding{};

//! `true` if TCPSender sends the first segment
bool _syn{false};

//! `true` if TCPSender sends the last segment
bool _fin{false};

//! number of consecutive retransmissions that have occurred in a row
unsigned int _consecutive_retransmissions{0};

注意

  • TCPSender 通过 fill_window 函数来发送 segment,我们需要处理发送 SYN、数据、FIN segments。能否发送它们需要根据当前窗口的剩余容量
  • 发送完一个 segment 记得更新 bytes_in_flight_next_seqno
  • ack_received 函数中需要根据 acknowinfow_size 更新我们添加的私有成员变量,然后根据 ackno 移除那些已经被确认的 “outstanding” segments。使用 length_in_sequence_space 函数获得这个 segment 的长度

Task #2

现在我们需要完成计时器部分的代码了,由于一个 segment 在发送之后,会启动一个计时器,当超时后,会重传这个 segment。对于计时器的设计,可以将它设计成一个类。

对于重传计时器的实现需要注意

  1. 计时器的 time passing 是通过调用 tick 函数实现的,而不是调用 OS 或者 CPU 的 time 或者 clock
  2. retransmission timeout (RTO) 是重新发送一个 outstanding TCP segment 之前等待的毫秒数
  3. 每当发送一个包含数据的 segment 时,如果计时器还未启动,则启动计时器
  4. 当所有的 outstanding TCP segment 都被确认时,关闭计时器
  5. 当计时器超时
    • 重传那个最早发送、但却未被确认的 segment
    • 如果窗口大小不为 0:(i)增加连续重传的次数。在下个 Lab 中,TCPConnection 会根据这个信息来决定是否终止连接。(ii)将 RTO 的值加倍。
  6. 当 sender 收到了 receiver 发送的 ackno 时,它会
    • 将 RTO 设置回初始值
    • 如果 sender 存在 outstanding data,重新启动重传计时器,这样它就会在经过 RTO 后过期
    • 重新设置 consecutive retransmissions 为 0

测试

1
2
make -j4
make check_lab3