CS144 Lab 2
上一个实验:Lab 1
这篇文章会讲解 CS144 Lab 2 的思路和代码。
Lab 2
这个 Lab 的目标是写一个 TCP receiver,它支持三个功能:
- 可以接受一个 TCPSegment,并把其中的数据传给上一个 Lab 实现的 StreamReassembler;
- 支持查询 ackno,也就是被排列好的最后一个字符的序数 +1;
- 支持查询 window_size,也就是滑动窗口里剩下的空间。
在 git merge origin/lab2-startercode 合并 Lab 2 的代码后,我们就可以开始啦。
Translating between 64-bit indexes and 32-bit seqnos
为了达成这三个目的,我们需要先弄清楚这三个序数的定义:
- Sequence Numbers, 记为 SN
- Absolute Sequence Numbers,记为 ASN
- Stream Indices,记为 SI
这三个数的定义在原文档我就不复制了,简单说一下他们的关系:
- SN = (ASN + ISN) & (0xFFFFFFFF) (取最低32位)
- SI = ASN - 1
我们还需要一个把 WrappingInt32 转换为 uint54_t 的转换函数:
uint64_t wrappingInt32_to_uint64(WrappingInt32 n) { 
    return static_cast<uint64_t>(n.raw_value()); 
}
在它的帮助下,我们可以写出 wrap 函数:
WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) {
    uint64_t res = n + wrappingInt32_to_uint64(isn);
    res &= 0xFFFFFFFF;
    return WrappingInt32{static_cast<uint32_t>(res)};
}
unwrap 函数稍微复杂一些。由于 CS144 lab 都是有测试例的,所以即使一些方法可以帮助我们完成主要目标(指实现 TCPReceiver),但想完成 Lab 的话不能绕过这些测试例。比如这里,其实我们可以返回比 checkpoint 大的最小满足条件的数,但是由于 Lab 已经规定好了 unwrap 是寻找最接近的数,因此还是得按要求来。参考代码如下:
uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
    uint64_t res = wrappingInt32_to_uint64(n) - wrappingInt32_to_uint64(isn);
    res &= 0xFFFFFFFF;
    uint64_t first_part = checkpoint & 0xFFFFFFFF00000000;
    res |= first_part;
    uint64_t alternative = res;
    if (res < checkpoint) {
        alternative = res + (1ul << 32);
        return alternative - checkpoint >= checkpoint - res ? res : alternative;
    } else if (res > checkpoint) {
        if (res < (1ul << 32))
            return res;
        alternative = res - (1ul << 32);
        return checkpoint - alternative >= res - checkpoint ? res : alternative;
    }
    return res;
}
这部分可以在 build 目录下编译并测试,和之前类似:
$ CXX=g++-8 cmake ..
$ make format
$ make -j4
$ ctest -R wrap
通过的话会得到类似字样:
100% tests passed, 0 tests failed out of 4
Total Test time (real) =   0.14 sec
在这些函数的帮助下,我们可以开始实现 TCPreceiver 了。
Implementing the TCP receiver
正如同文档所说,这个 Lab 没有什么需要构造的数据结构,但是我们需要对很多数值的定义小心。
由于 ISN 指代的是 SYN 的 SN,所以除了第一个带有 SYN 的 TCPsegment 以外,在我们计算其他的 TCPSegment 的 payload 对应的 SI 时,都需要减一。另外需要注意的是如果没有收到 SYN 信号的话,我们应该抛弃所有的 TCPSegment,因为这个时候我们的 TCPReceiver 还没准备好接受分段,这里我们维护变量 initFlag 来记载是否接收到了 SYN。
参考代码:
void TCPReceiver::segment_received(const TCPSegment &seg) {
    if (seg.header().syn) {
        // initialize ISN
        ISN = seg.header().seqno;
        initFlag = true;
    } else if (!initFlag) {
        return;
    }
    size_t stream_idx = unwrap(seg.header().seqno, ISN, _reassembler.stream_out().bytes_written());
    if (!seg.header().syn)
        stream_idx -= 1;
    _reassembler.push_substring(seg.payload().copy(), stream_idx, seg.header().fin);
}
optional<WrappingInt32> TCPReceiver::ackno() const {
    size_t abs_idx = _reassembler.stream_out().bytes_written();
    if (!initFlag)
        return {};
    abs_idx++;
    if (_reassembler.stream_out().input_ended())
        abs_idx++;
    return wrap(abs_idx, ISN);
}
size_t TCPReceiver::window_size() const { 
    return _reassembler.stream_out().remaining_capacity(); 
}
完成后我们在build目录下编译并测试,和之前类似:
$ CXX=g++-8 cmake ..
$ make format
$ make -j4
$ make check_lab2
通过的话会得到类似字样:
100% tests passed, 0 tests failed out of 26
Total Test time (real) =   0.82 sec
下一个实验:Lab 3
Let me know what you think of this article on twitter @CreamEggMeat or leave a comment below!