[CS144] Lab 2: the TCP receiver

Lab 2: the TCP receiver

  • Lab Guide: Checkpoint 2: the TCP receiver
  • Lab Code: https://github.com/peakcrosser7/sponge/tree/lab2-startercode

3.1 Translating between 64-bit indexes and 32-bit seqnos

要点

  • 实现 wrap()unwrap() 函数, 以完成序列号和相对序列号之间的转换.

思路

相对序列号 -> 序列号:

该转换相对简单, 直接在初始序列号 ISN 的基础上加上相对序列号即可, 由于序列号是 32 位, 相对序列号是 64 位, 因此相对序列号只需要考虑其低 32 位即可.

序列号 -> 相对序列号:

该转换是该部分实验相对难的地方, 关键在于有一个检查点 c h e c k p o i n t checkpoint checkpoint, 由于序列号位数比相对序列号少, 因此转换后的相对序列号应该满足是最接近检查点的.

  • 首先, 根据当前序列号 n n n 和初始序列号 I S N ISN ISN 可以计算出两者的差值 d e l t a = n − I S N delta=n-ISN delta=nISN, 而实际的相对序列号应该是 A = { d e l t a + k ⋅ 2 32 ∣ k = 0 , 1 , . . . } A=\{delta+k\cdot2^{32}| k=0,1,...\} A={delta+k232k=0,1,...} 之中最接近检查点 c h e c k p o i n t checkpoint checkpoint 的一个.
    距离检查点更近的数字范围为 B = { c h e c k p o i n t − 2 31 + 1 ≤ x ≤ c h e c k p o i n t + 2 31 ∣ x ∈ Z } B=\{checkpoint-2^{31}+1\leq x\leq checkpoint+2^{31}|x\in Z\} B={checkpoint231+1xcheckpoint+231xZ}. 而这里有一个比较特殊的情况, 即 c h e c k p o i n t − 2 31 checkpoint-2^{31} checkpoint231 c h e c k p o i n t + 2 31 checkpoint+2^{31} checkpoint+231 据检查点的距离均为 2 31 2^{31} 231, 但根据实验指导中的说明 “In your TCP implementation, you’ll use the index of the last reassembled byte as the checkpoint”, 即检查点实际上是最后一个重排的字节序号, 而当前收到字节的序号理论上应该在检查点之后, 因此这里选择的是后者 c h e c k p o i n t + 2 31 checkpoint+2^{31} checkpoint+231.
    最后, 实际的相对序列号 r e s u l t result result r e s u l t = A ∩ B result=A\cap B result=AB.
  • 为了确定 A A A 中的 k k k, 需要计算 d e l t a delta delta 相对 c h e c k p o i n t checkpoint checkpoint 的距离 o f f s e t = d e l t a − u i n t 32 _ t ( c h e c k p o i n t ) offset=delta-uint32\_t(checkpoint) offset=deltauint32_t(checkpoint), 这里 c h e c k p o i n t checkpoint checkpoint 需要仅考虑低 32 位, 这样计算出的距离 o f f s e t offset offset 实际上就是序列号 n n n 的相对序列号和检查点 c h e c k p o i n t checkpoint checkpoint 的距离, o f f s e t offset offset 满足 0 ≤ o f f s e t ≤ 2 32 0\leq offset\leq 2^{32} 0offset232.
    然后判断该距离, 若 o f f s e t ≤ 2 31 offset\leq 2^{31} offset231, 则实际 n n n 的相对序列号在检查点的右侧 r e s u l t = c h e c k p o i n t + o f f s e t result=checkpoint+offset result=checkpoint+offset; 而若 o f f s e t > 2 31 offset>2^{31} offset>231, 则 n n n 的相对序列号在检查点的左侧 r e s u l t = c h e c k p o i n t − ( 2 32 − o f f s e t ) result=checkpoint-(2^{32}-offset) result=checkpoint(232offset).
  • 这里有一个特殊情况, 即 c h e c k p o i n t = 0 , n = 2 32 − 1 , I S N = 0 checkpoint=0, n=2^{32}-1, ISN=0 checkpoint=0,n=2321,ISN=0 的情况, 根据上述算法, 计算出来的 n n n 的相对序列号应该在检查点的左侧. 此时便会发生整数下溢, 但同样根据实验指导, 传输 2 64 2^{64} 264 字节需要几十年, 因此可以说不存在 64 位溢出的情况, 因此在计算有 64 位整数下溢情况时, 应该选择在检查点右侧的值, 即 c h e c k p o i n t + o f f s e t checkpoint+offset checkpoint+offset.

代码

libsponge/wrapping_integers.cc

#include "wrapping_integers.hh"

// Dummy implementation of a 32-bit wrapping integer

// For Lab 2, please replace with a real implementation that passes the
// automated checks run by `make check_lab2`.

template <typename... Targs>
void DUMMY_CODE(Targs &&.../* unused */) {}

using namespace std;

//! 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) { return isn + static_cast<uint32_t>(n); }

//! 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) {
    const uint32_t offset = n - isn - static_cast<uint32_t>(checkpoint);
    uint64_t res;
    if (offset <= (1U << 31)) {
        res = checkpoint + offset;
    } else {
        // if `res` has 64 bit underflow, chose the right one. 
        res = checkpoint - ((1UL << 32) - offset);
        if (res > checkpoint) {
            res = checkpoint + offset;
        }
    }
    return res;
}

极简 unwrap()

对于 unwrap() 的实现, 有如更加简洁的代码:

uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
    int32_t offset = static_cast<uint32_t>(checkpoint) - (n - isn);
    int64_t result = checkpoint - offset;
    return result >= 0 ? result : result + (1UL << 32);
}

该实现的基本思想和之前的实现代码的思想是相同的.

  • 根据距离检查点更近的数字范围 B = { c h e c k p o i n t − 2 31 + 1 ≤ x ≤ c h e c k p o i n t + 2 31 ∣ x ∈ Z } B=\{checkpoint-2^{31}+1\leq x\leq checkpoint+2^{31}|x\in Z\} B={checkpoint231+1xcheckpoint+231xZ}, 以及随后会计算 c h e c k p o i n t checkpoint checkpoint o f f s e t offset offset 相加或相减, 实际上就可以将 o f f s e t offset offset 视为一个 32 位有符号整数 int32_t. 这样对于在检查点左右两侧的情况都可以用 c h e c k p o i n t + o f f s e t checkpoint+offset checkpoint+offset 表示.
  • 但由于 int32_t 表示的范围为 { c h e c k p o i n t − 2 31 ≤ x ≤ c h e c k p o i n t + 2 31 − 1 ∣ x ∈ Z } \{checkpoint-2^{31}\leq x\leq checkpoint+2^{31}-1|x\in Z\} {checkpoint231xcheckpoint+2311∣xZ}, 与 B B B 在端点处正好相反, 因此这里将 o f f s e t offset offset 表示为 u i n t 32 _ t ( c h e c k p o i n t ) − d e l t a uint32\_t(checkpoint)-delta uint32_t(checkpoint)delta. 这样在检查点左右两侧的情况都可以用 c h e c k p o i n t − o f f s e t checkpoint-offset checkpointoffset 表示.
  • 而对于 64 位溢出的情况, 此处同样使用了另一种做法, 即将 r e s u l t result result 先表示为有符号64整数 int64_t, 这样对于上述 64 溢出情况, r e s u l t result result 的值就会变为负数, 此时只需要再这基础上加 2 32 2^{32} 232 即可.
    但此时就会限制 r e s u l t result result 的大小不能大于等于 2 63 2^{63} 263, 否则会发生有符号整数上溢, 此时便会计算错误, 理论上应该再加 2 64 2^{64} 264 才是正确结果. 但实际上可以忽略上溢的情况, 因为根据传输 2 64 2^{64} 264 字节的时间推算, 达到 2 63 2^{63} 263 字节的情况也需要几十年, 因此理论上不存在这种情况.
  • 实际上两种实现的用时基本没有差别.
  • Ref: 【计算机网络】CS144 Lab 2:the TCP receiver_MSC419的博客-CSDN博客

测试

build 目录下执行 ctest -R wrap:
[CS144] Lab 2: the TCP receiver_第1张图片

3.2 Implementing the TCP receiver

要点

  • 实现 TCPReceiversegment_received()ackno()window_size() 三个方法
  • 注意序列号(seqno)、相对序列号(absolute seqno)和流索引(stream index)三者的使用场景和转换关系
    [CS144] Lab 2: the TCP receiver_第2张图片

思路

实现思路基本按照任务指导, 需要注意的是 TCPReceiver 接收到的是 TCP 报文段 TCPSegment, 其中报文首部记录的均为序列号(seqno), 而 TCPReceiver 内部使用的 StreamReassembler 实际上使用的是流索引(stream index), 过程中需要借助 unwrap()wrap() 函数进行转换. 而这其中就需要使用 ISN 进行转换, 因此需要添加一个 _isn 的私有成员记录该 TCP 连接的 ISN. 值得一提的是, 在未收到 SYN 标志位时, 没有 ISN, 因此最终使用 std::optional 作为 _isn 的类型.
对于 segment_received() 函数, 需要注意的有: 在接收到 SYN 报文段之前的报文都是无效报文, 需要丢弃不做处理. 在转换序列号到流索引时, 需要一个检查点(checkpoint), 根据指导书前文, 检查点是最后一个重组字节的相对序列号, 而 stream_out().bytes_written() 表示已经写入 ByteStream 字节流的字节数, 其值与最后一个重组的字节的相对序列号一致. 同时在使用 unwrap() 时需要注意 ISN 同样占一个序列号, 因此对于其负载的数据的序列号需要额外加 1.
对于 ackno() 函数, 在 ISN 未设置前需要返回空, 即 std::nullopt, 反之返回下一个字节的序列号. stream_out().bytes_written() 表示的为最后重组字节的相对序列号, 加 1 即第一个未重组字节的相对序列号, 再通过 wrap() 即可转换为序列号. 同样需要注意, FIN 标志位也占用一个序列号, 因此在收到 FIN 之后, 序列号还要再加 1.
对于 window_size(), 即第一个未重组字节和第一个不接受字节的间距, 也就是除去已重组的字节的空间大小. 由于 ByteStreamStreamRessembler 总容量为一致, 因此可以用 stream_out().remaining_capacity() 表示.
[CS144] Lab 2: the TCP receiver_第3张图片

代码

libsponge/tcp_receiver.hh

#ifndef SPONGE_LIBSPONGE_TCP_RECEIVER_HH
#define SPONGE_LIBSPONGE_TCP_RECEIVER_HH

#include "byte_stream.hh"
#include "stream_reassembler.hh"
#include "tcp_segment.hh"
#include "wrapping_integers.hh"

#include 

//! \brief The "receiver" part of a TCP implementation.

//! Receives and reassembles segments into a ByteStream, and computes
//! the acknowledgment number and window size to advertise back to the
//! remote TCPSender.
class TCPReceiver {
    //! Our data structure for re-assembling bytes.
    StreamReassembler _reassembler;

    //! the sequence number of ISN
    std::optional<WrappingInt32> _isn;

    //! The maximum number of bytes we'll store.
    size_t _capacity;

  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), _isn(), _capacity(capacity) {}

    //! \name Accessors to provide feedback to the remote TCPSender
    //!@{

    //! \brief The ackno that should be sent to the peer
    //! \returns empty if no SYN has been received
    //!
    //! This is the beginning of the receiver's window, or in other words, the sequence number
    //! of the first byte in the stream that the receiver hasn't received.
    std::optional<WrappingInt32> ackno() const;

    //! \brief The window size that should be sent to the peer
    //!
    //! Operationally: the capacity minus the number of bytes that the
    //! TCPReceiver is holding in its byte stream (those that have been
    //! reassembled, but not consumed).
    //!
    //! Formally: the difference between (a) the sequence number of
    //! the first byte that falls after the window (and will not be
    //! accepted by the receiver) and (b) the sequence number of the
    //! beginning of the window (the ackno).
    size_t window_size() const;
    //!@}

    //! \brief number of bytes stored but not yet reassembled
    size_t unassembled_bytes() const { return _reassembler.unassembled_bytes(); }

    //! \brief handle an inbound segment
    void segment_received(const TCPSegment &seg);

    //! \name "Output" interface for the reader
    //!@{
    ByteStream &stream_out() { return _reassembler.stream_out(); }
    const ByteStream &stream_out() const { return _reassembler.stream_out(); }
    //!@}
};

#endif  // SPONGE_LIBSPONGE_TCP_RECEIVER_HH

libsponge/tcp_receiver.cc

#include "tcp_receiver.hh"

// Dummy implementation of a TCP receiver

// For Lab 2, please replace with a real implementation that passes the
// automated checks run by `make check_lab2`.

template <typename... Targs>
void DUMMY_CODE(Targs &&.../* unused */) {}

using namespace std;

void TCPReceiver::segment_received(const TCPSegment &seg) {
    const TCPHeader &header = seg.header();
    if (!_isn.has_value()) {
        // the segment before receiving SYN segment should be discarded
        if (!header.syn) {
            return;
        }
        // set the ISN
        _isn = header.seqno;
    }
    // ISN occupies a seqno
    // `stream_out.bytes_written()` is equal to the index(absolute seqno) of last reassembled byte, which is checkpoint
    // use `unwrap()` to get absolute seqno, and minus 1 to get the stream index.
    uint64_t stream_index = unwrap(header.seqno + header.syn, _isn.value(), stream_out().bytes_written()) - 1;
    _reassembler.push_substring(seg.payload().copy(), stream_index, header.fin);
}

optional<WrappingInt32> TCPReceiver::ackno() const {
    // if the ISN hasn’t been set yet, return an empty optional
    if (!_isn.has_value()) {
        return nullopt;
    }
    // `stream_out.bytes_written()+1` is the absolute seqno of the first unassembled byte
    // FIN flag also occupies a seqno
    return wrap(stream_out().bytes_written() + 1 + stream_out().input_ended(), _isn.value());
}

size_t TCPReceiver::window_size() const { return stream_out().remaining_capacity(); }

测试

build 目录下执行 make 后执行 make check_lab2:
[CS144] Lab 2: the TCP receiver_第4张图片
在这里插入图片描述

你可能感兴趣的:(CS144,Labs,tcp/ip,c++,网络协议)