Stanford CS144 Lab 2
Lab Checkpoint 2: the TCP receiver
上一个 Checkpoint 的 Reassembler 维护 ByteStream,实现了现实世界中 TCP 接受组件和应用程序之间“接受缓冲区”的功能。这一个 Checkpoint 中,我们实现 TCPReceiver,向接受缓冲区中写入收到的数据。
2 Checkpoint 2: The TCP Receiver #
2.1 Translating between 64-bit indexes and 32-bit seqnos #
这不是我们 Rust 里的 std::num::Wrapping 吗(
简单来说就是 TCP 包头的 sequence number 是 32 位的,每个字节的序列号在模 $2^{32}$ 意义下 $+1$。每个流的初始 sequence number 是随机的,称为 ISN。需要我们写一个新的类型,能够在【模意义下 32 位从 ISN 开始的序列号】和【64 位从 0 开始的绝对序列号】之间互相转换。
总体代码不难。但需要注意,unwrap 方法需要返回离 checkpoint 最近的序列号,如果直接 $\pm 2^{32}$ 生成三个值比较和 checkpoint 的大小关系,可能会由于减到负数 u64 下溢得到很大的数,改变相对大小关系。需要留意特判,最好直接计算相对距离来实现。
1Wrap32 Wrap32::wrap( uint64_t n, Wrap32 zero_point )
2{
3 return Wrap32 { static_cast<uint32_t>( n + zero_point.raw_value_ ) };
4}
5
6uint64_t Wrap32::unwrap( Wrap32 zero_point, uint64_t checkpoint ) const
7{
8 uint64_t offset = raw_value_ - zero_point.raw_value_;
9 uint64_t base = checkpoint & 0xFFFFFFFF00000000;
10 uint64_t absolute = base + offset;
11 uint64_t pre = absolute - numeric_limits<uint32_t>::max() - 1;
12 uint64_t post = absolute + numeric_limits<uint32_t>::max() + 1;
13 uint64_t dis_1 = ( checkpoint > pre ) ? checkpoint - pre : pre - checkpoint;
14 uint64_t dis_2 = ( checkpoint > post ) ? checkpoint - post : post - checkpoint;
15 uint64_t dis_3 = ( checkpoint > absolute ) ? checkpoint - absolute : absolute - checkpoint;
16 if ( dis_1 <= dis_2 && dis_1 <= dis_3 ) {
17 return pre;
18 } else if ( dis_2 <= dis_1 && dis_2 <= dis_3 ) {
19 return post;
20 } else {
21 return absolute;
22 }
23}2.2 Implementing the TCP receiver #
要求我们实现 TCPReceiver 类,但其实也就是两个接口,一个 receive 负责接受信息,设置 seqno,最终插入 Reassembler,另一个接口 send 负责发出 ACK 信息,包含 window size 和希望收到的下一个 seqno。
1void TCPReceiver::receive( TCPSenderMessage message )
2{
3 if ( message.RST ) {
4 reassembler_.reader().set_error();
5 isn_set_ = false;
6 return;
7 }
8 if ( message.SYN && !isn_set_ ) {
9 isn_ = message.seqno;
10 isn_set_ = true;
11 }
12 if ( !isn_set_ ) {
13 return;
14 }
15 uint64_t checkpoint = writer().bytes_pushed();
16 uint64_t abs_stream_index = message.seqno.unwrap( isn_, checkpoint );
17 if ( message.SYN ) {
18 abs_stream_index = 0;
19 } else {
20 abs_stream_index -= 1;
21 }
22 reassembler_.insert( abs_stream_index, std::move( message.payload ), message.FIN );
23}
24
25TCPReceiverMessage TCPReceiver::send() const
26{
27 TCPReceiverMessage msg;
28 if ( reassembler_.reader().has_error() ) {
29 msg.RST = true;
30 return msg;
31 }
32 uint16_t window_size = static_cast<uint64_t>(
33 std::min( static_cast<uint64_t>( UINT16_MAX ), reassembler_.writer().available_capacity() ) );
34 msg.window_size = window_size;
35 if ( isn_set_ ) {
36 uint64_t ackno = writer().bytes_pushed() + 1 + static_cast<uint64_t>( writer().is_closed() );
37 msg.ackno = Wrap32::wrap( ackno, isn_ );
38 }
39 return msg;
40}总体实现不难,需要特别注意 SYN 和 FIN 也占据 seqno,需要加减 1。但是框架对于 ByteStream 的读写接口引起了我的一些疑问。在 TCPReceiver 类的 public 接口中,有如下定义:
1// Access the output
2const Reassembler& reassembler() const { return reassembler_; }
3Reader& reader() { return reassembler_.reader(); }
4const Reader& reader() const { return reassembler_.reader(); }
5const Writer& writer() const { return reassembler_.writer(); }可以看到,只有 reader() 是 non-const 的。这样设置的意图也很好理解,希望我们通过调用 reader().set_error() 以在收到 RST 消息时,将整个流置于错误状态。然而,在流的另一头,应用程序也会调用 reader,这会引起竞争。
我认为这是本实验中简化的一点,在单线程环境中使用单线程模拟测试,代码是交替执行的,不存在竞争问题。而在真实的操作系统中,TCP 协议栈和用户程序通过 syscall 进行沟通,TCP 协议栈需要用某种方法锁定 buffer,用户在进行 syscall 时,操作系统会保证竞争问题不出现。