CS144-2021|Lab2 个人实验记录

934 阅读3分钟

lab2

个人实验留档:cs144-2021

Overview

image.png

这个lab主要实现的部分是这个图中的TCPReceiver,也就是接收tcp数据报文,将数据报文传输给StreamReassembler。

此外,TCPReceiver还负责告知sender:

  • 下一个需要接收的数据的序列号,也就是acknowledge number
  • 接收方现在还能够接收多少的数据,也就是window size。

The TCP Receiver

Translating between 64-bit indexes and 32-bit seqnos

实际上,在tcp报头中的seqno只有32位,能够表示的数据范围十分有限(4 GiB)因此,在传输的数据量十分大时(超过4GiB),序列号就不得不循环使用了。

其次,在tcp连接时,sender在发送syn报文时,其序列号时一个随机的值,然而在我们实现的StreamReassembler中,接收到的数据起始index是0,因此,就不得不进行seqno→ stream index 的转换。

下面是seqno、absolute seqno、stream index之间的关系

image.png

此外,在tcp中,syn报文和fin报文中,syn和fin标记位不占有实际的数据,但是占有一个序列号。例如,在tcp建立连接的三次握手以及关闭连接的四次握手时,即使没有传输实际的数据(payload为空),但是序列号hai'shi发生了变化。

因此,我们需要实现一个类WrappingInt32,来辅助实现seqno、absolute seqno、stream index之间的转换。

  • WrappingInt32 wrap(uint64 t n, WrappingInt32 isn) 进行 absolute seqno → seqno的转换。 这个转换只需要相加并截取底32位就行
//! Transform an "absolute" 64-bit sequence number (zero-indexed) into a WrappingInt32
//! \param n The input absolute 64-bit sequence number
//! \param isn The initial sequence number
WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) {
    uint32_t value = static_cast<uint32_t>((n + isn.raw_value()) & 0xFFFFFFFF);
    return WrappingInt32{value};
}
  • uint64 t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64 t checkpoint) 进行 seqno absolute seqno之间的转换 为什么需要checkpoint?这是因为 any given seqno corresponds to many absolute seqnos,所以需要一个checkpoint,将n转换成距离checkpoint最近的一个absolute seqno。 在实现时有些细节需要注意到,详见代码中的注释:
//! Transform a WrappingInt32 into an "absolute" 64-bit sequence number (zero-indexed)
//! \param n The relative sequence number
//! \param isn The initial sequence number
//! \param checkpoint A recent absolute 64-bit sequence number
//! \returns the 64-bit sequence number that wraps to `n` and is closest to `checkpoint`
//!
//! \note Each of the two streams of the TCP connection has its own ISN. One stream
//! runs from the local TCPSender to the remote TCPReceiver and has one ISN,
//! and the other stream runs from the remote TCPSender to the local TCPReceiver and
//! has a different ISN.
uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
    uint32_t offset = n.raw_value() - isn.raw_value();
    // 如果 offset >= checkpoint,表明这两个值都在 32bit范围内,此时offset就是n距离checkpoint最近的absolute seqno
    if(offset >= checkpoint){
        return offset;
    }
    uint32_t half = 1ull << 31;
    // 取 checkpoint 的高位
    uint64_t result = offset + (checkpoint & 0xFFFFFFFF00000000);
    // 如何求出离checkpoint最近的?
    // result位置可能在下面这两个位置之一:
    // | 表示中点
    // ||-------|----(1)----checkpoint----(2)---|---------checkpoint+2^32||
    // 如果此时的result比checkpoint+2^31还要大,那就表明n的absulute seqno应该在checkpoint前面的(1),才满足 "closest"这个要求
    if (result > checkpoint + half) {
        result -= (1ull << 32);
    // 否则,如果result比checkpoint-2^31还要小,则表明result应该在(2)位置
    // 需要注意,checkpoint在此时应该是大于1<<31,否则相减得到的数是负数,转化为 uint64_t 就是非常大的正数。
    // 例如:-----0--------cp-----1<<31--------offset----1<<32-----------------(3)--
    // 此时,offset位置就是result位置,如果不判断checkpoint > half,就会得到offset在位置(3),显然不正确
    } else if (checkpoint > half && result <= checkpoint - half) {
        result += (1ull << 32);
    }
    return result;
}

Implementing the TCP receiver

TCPReceiver在tcp连接中负责:

  • 接收tcp数据报
  • 使用StreamReassembler接收payload
  • 计算ackno以及window size

具体实现如下:

  • 添加私有变量:
class TCPReceiver {
    //! Our data structure for re-assembling bytes.
    StreamReassembler _reassembler;

    //! The maximum number of bytes we'll store.
    size_t _capacity;
    WrappingInt32 _isn; // the Initial Sequence Number
    bool _got_syn;  // Has SYN been received?

  public:
    //! \brief Construct a TCP receiver
    //!
    //! \param capacity the maximum number of bytes that the receiver will
    //!                 store in its buffers at any give time.
    TCPReceiver(const size_t capacity) :
      _reassembler(capacity), 
      _capacity(capacity),
      _isn(0),
      _got_syn(false){}
  • void segment_received(const TCPSegment &seg); 这个函数的参数为接收到的数据报文,在这和函数中,主要:
    • 设置isn:在接收到syn报文时,设置isn,建立tcp连接
    • 使用StreamReassembler接收payload

实现如下:

void TCPReceiver::segment_received(const TCPSegment &seg) {
    if (! _got_syn && !seg.header().syn){
        return;
    }

    WrappingInt32 seqno = seg.header().seqno;
    if (! _got_syn && seg.header().syn){
        _got_syn = true;
        _isn = seqno;
        // 由于SYN标志位是没有数据但是占用一个序列号,因此需要加一,得到的是数据的seqno
        seqno = seqno + 1;
    }
    // 获取上一次读入的数据的最后一个字节的idx作为checkpoint
    uint64_t checkpoint = _reassembler.stream_out().bytes_written();
    // 对于stream_idx,应该为abs_seq - 1,
    size_t stream_idx = unwrap(seqno, _isn, checkpoint) - 1;
    _reassembler.push_substring(seg.payload().copy(), stream_idx, _got_syn && seg.header().fin);
}
  • std::optional<WrappingInt32> ackno() const; 返回ackno,也就是下一个希望接受到的数据的index
optional<WrappingInt32> TCPReceiver::ackno() const {
    // akono 是接收端希望接收的下一个字节的序号
    if (! _got_syn){
        return {};
    }
    size_t ackno = _reassembler.stream_out().bytes_written() + 1;
    // 如果接收到的报文中出现了FIN,由于FIN占用一个序列号,但是没有实际数据,因此需要加一
    if (stream_out().input_ended()){
        return wrap(ackno+1, _isn);
    }
    return wrap(ackno, _isn);
}
  • size_t window_size() const; 返回window size。 关于window size,可以回顾这个图:

image.png

window size就是上图中first unassembled 到 first unaccept 之间的距离

size_t TCPReceiver::window_size() const {
    // window_size 应该是 first unassembled 到 first unaccept 之间的距离
    // size_t first_unassemble = _reassembler.stream_out().bytes_written();
    // size_t first_unaccept = _reassembler.stream_out().bytes_read() + _capacity;
    // return first_unaccept - first_unassemble;
    return _reassembler.stream_out().remaining_capacity();
}

测试

  • make check_lab2

image.png

总结

这个实验总体来说,实现流程还是比较清晰的,但是在实现unwrap()这个函数时,需要考虑边界情况(checkpoint ≤ half),否则有可能ctest -R wrap 过了但是最终的测试没过。