CS 144 Lab Two -- TCPReceiver

CS 144 Lab Two -- TCPReceiver

  • TCPReceiver 简述
    • 索引转换
    • TCPReceiver 实现
  • 测试


对应课程视频: 【计算机网络】 斯坦福大学CS144课程

Lab Two 对应的PDF: Lab Checkpoint 2: the TCP receiver


TCPReceiver 简述

在 Lab2,我们将实现一个 TCPReceiver,用以接收传入的 TCP segment 并将其转换成用户可读的数据流。
CS 144 Lab Two -- TCPReceiver_第1张图片

TCPReceiver 除了将读入的数据写入至 ByteStream 中以外,它还需要告诉发送者两个属性

  • 第一个未组装的字节索引,称为确认号ackno,它是接收者需要的第一个字节的索引。
  • 第一个未组装的字节索引第一个不可接受的字节索引之间的距离,称为 窗口长度window size

ackno 和 window size 共同描述了接收者当前的接收窗口。接收窗口是 发送者允许发送数据的一个范围,通常 TCP 接收方使用接收窗口来进行流量控制,限制发送方发送数据。

总的来说,我们将要实现的 TCPReceiver 需要做以下几件事情:

  • 接收TCP segment
  • 重新组装字节流(包括EOF)
  • 确定应该发回给发送者的信号,以进行数据确认和流量控制

索引转换

TCP 报文中用来描述当前数据首字节的索引(序列号 seqno)是32位类型的,这意味着在处理上增加了一些需要考虑的东西:

  • 由于 32位类型最大能表达的值是 4GB,存在上溢的可能。因此当 32位的 seqno 上溢后,下一个字节的 seqno 就重新从 0 开始。

  • 处于安全性考虑,以及避免与之前的 TCP 报文混淆,TCP 需要让每个 seqno 都不可被猜测到,并且降低重复的可能性。因此 TCP seqno 不会从 0 开始,而是从一个 32 位随机数起步(称为初始序列号 ISN)。

    • 而 ISN 是表示 SYN 包(用以表示TCP 流的开始)的序列号。
  • TCP 流的逻辑开始数据包逻辑结束数据包各占用一个 seqno。除了确保接收到所有字节的数据以外,TCP 还需要确保接收到流的开头和结尾。 因此,在 TCP 中,SYN(流开始)和 FIN(流结束)控制标志将会被分别分配一个序列号(SYN标志占用的序列号就是ISN)。

    • 流中的每个数据字节也占用一个序列号。
    • 但需要注意的是,SYN 和 FIN 不是流本身的一部分,也不是传输的字节数据。它们只是代表字节流本身的开始和结束。

字节索引类型一多就容易乱。当前总共有三种索引:

  • 序列号 seqno。从 ISN 起步,包含 SYN 和 FIN,32 位循环计数
  • 绝对序列号 absolute seqno。从 0 起步,包含 SYN 和 FIN,64 位非循环计数
  • 流索引 stream index。从 0 起步排除 SYN 和 FIN64 位非循环计数。

这是一个简单浅显的例子,用于区分开三种索引的区别:

CS 144 Lab Two -- TCPReceiver_第2张图片
序列号和绝对序列号之间相互转换稍微有点麻烦,因为序列号是循环计数的。在该实验中,CS144 使用自定义类型 WrappingInt32 表示序列号,并编写了它与绝对序列号之间的转换。
CS 144 Lab Two -- TCPReceiver_第3张图片

  • wrapping_integers.cc
//! 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
// 将64位绝对序列号转换为32位序列号   
WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) { return WrappingInt32{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.
// 将TCP协议头中携带的32位序列号转换为64位绝对序列号
// 参数: 要转换的32位序列号,本次TCP连接的ISN(初始序列号),检查点(一个32位序列号对应多个64位序列号,因此这里选择靠近ISN的值)
uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
    uint64_t offset = uint32_t(n - isn);
    if (checkpoint < offset)
        return offset;
    offset += ((checkpoint - offset) >> 32) * (1lu << 32);
    return checkpoint - offset <= (1lu << 31) ? offset : offset + (1lu << 32);
}

TCPReceiver 实现

需要实现一些类成员函数

  • segment_received(): 该函数将会在每次获取到 TCP 报文时被调用。该函数需要完成:

    • 如果接收到了 SYN 包,则设置 ISN 编号。
      • 注意:SYN 和 FIN 包仍然可以携带用户数据并一同传输。同时,同一个数据包下既可以设置 SYN 标志也可以设置 FIN 标志
      • 将获取到的数据传入流重组器,并在接收到 FIN 包时终止数据传输。
  • ackno():返回接收方尚未获取到的第一个字节的字节索引。如果 ISN 暂未被设置,则返回空。

  • window_size():返回接收窗口的大小,即第一个未组装的字节索引第一个不可接受的字节索引之间的长度。

这是 CS144 对 TCP receiver 的期望执行流程:
CS 144 Lab Two -- TCPReceiver_第4张图片
三次握手:

CS 144 Lab Two -- TCPReceiver_第5张图片
实现思路:

对于 TCPReceiver 来说,除了错误状态以外,它一共有3种状态,分别是:

  • LISTEN:等待 SYN 包的到来。若在 SYN 包到来前就有其他数据到来,则必须丢弃
  • SYN_RECV:获取到了 SYN 包,此时可以正常的接收数据包
  • FIN_RECV:获取到了 FIN 包,此时务必终止 ByteStream 数据流的输入。

在每次 TCPReceiver 接收到数据包时,我们该如何知道当前接收者处于什么状态呢?可以通过以下方式快速判断:

  • 当 isn 还没设置时,肯定是 LISTEN 状态
  • 当 ByteStream.input_ended(),则肯定是 FIN_RECV 状态
  • 其他情况下,是 SYN_RECV 状态

Window Size 是当前的 capacity 减去 ByteStream 中尚未被读取的数据大小,即 reassembler 可以存储的尚未装配的子串索引范围。

ackno 的计算必须考虑到 SYN 和 FIN 标志,因为这两个标志各占一个 seqno。故在返回 ackno 时,务必判断当前 接收者处于什么状态,然后依据当前状态来判断是否需要对当前的计算结果加1或加2。而这条准则对 push_substring 时同样适用。


源码:

  • tcp_receiver.hh
//! 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 maximum number of bytes we'll store.
  size_t capacity_;
  // The absolute seqno
  uint64_t seqno_;
  // The initial sequence nummber
  std::optional<WrappingInt32> isn_;
  // The Fin seqno
  std::optional<uint64_t> fin_seq_;

 public:
  TCPReceiver(const size_t capacity)
      : reassembler_(capacity),
        capacity_(capacity),
        seqno_(0),
        isn_(),
        fin_seq_() {}

  //! 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;
  // number of bytes stored but not yet reassembled
  size_t unassembled_bytes() const { return reassembler_.unassembled_bytes(); }
  // handle an inbound segment
  void segment_received(const TCPSegment &seg);
  ByteStream &stream_out() { return reassembler_.stream_out(); }
  const ByteStream &stream_out() const { return reassembler_.stream_out(); }
};
  • tcp_receiver.cc
void TCPReceiver::segment_received(const TCPSegment &seg) {
  // check syn
  // tcp头中syn标志被设置了---记录初始序列号
  if (seg.header().syn) {
    isn_ = seg.header().seqno;
  }
  // 如果初始化序列号还没有设置,说明TCP连接还没有建立,忽略当前传入的数据包
  if (!isn_.has_value()) return;

  // check fin
  // tcp头中fin标志被设置了 -- 记录结束序列号
  if (seg.header().fin)
    fin_seq_ = unwrap(seg.header().seqno, isn_.value(), seqno_) +
               seg.length_in_sequence_space();

  // compute index(absolute seqno)
  // 将当前TCP报文的32位序列号转换为绝对序列号
  uint64_t index = unwrap(seg.header().seqno, isn_.value(), seqno_);
  // 如果syn标志设置了,那么减去SYN占用的序列号 -- SYN包也可以携带用户数据
  if (!seg.header().syn) index--;
  // 将TCP载荷数据推入流重组器中: 字节流,该批字节流起始的序列号,当前字节流是否是最后一批数据取决于当前TCP报文的fin标志是否设置了
  reassembler_.push_substring(seg.payload().copy(), index, seg.header().fin);
  // update the seqno
  // 更新下一个期望接收到的字节起始序列号 --> 也就是ack给发送者的seqno
  seqno_ = reassembler_.stream_out().bytes_written() + 1;
  // 如果fin标志被设置了,那么检查点序列号还需要+1 --> fin_seq也占据一个seqno
  if (fin_seq_.has_value() && fin_seq_.value() == seqno_ + 1) seqno_++;
}

// 如果连接已经建立,那么返回的ackno值就是seqno_
optional<WrappingInt32> TCPReceiver::ackno() const {
  return isn_.has_value() ? wrap(seqno_, isn_.value()) : isn_;
}

// 当前滑动窗口大小
size_t TCPReceiver::window_size() const {
  return capacity_ - (reassembler_.stream_out().bytes_written() -
                      reassembler_.stream_out().bytes_read());
}

需要注意的是 TCPReceiver 接收到的是 TCP 报文段 TCPSegment, 其中报文首部记录的均为序列号(seqno), 而 TCPReceiver 内部使用的 StreamReassembler 实际上使用的是流索引(stream index), 过程中需要借助 unwrap() 及 wrap() 函数进行转换. 而这其中就需要使用 ISN 进行转换, 因此需要添加一个 _isn 的私有成员记录该 TCP 连接的 ISN. 值得一提的是, 在未收到 SYN 标志位时, 没有 ISN, 因此最终使用 std::optional< WrappingInt32 > 作为 _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(), 即第一个未重组字节和第一个不接受字节的间距, 也就是除去已重组的字节的空间大小. 由于 ByteStream 和 StreamRessembler 总容量为一致, 因此可以用 stream_out().remaining_capacity() 表示.

CS 144 Lab Two -- TCPReceiver_第6张图片


测试

CS 144 Lab Two -- TCPReceiver_第7张图片

你可能感兴趣的:(#,CS,144,&,MIT,6.829,网络)