CS 144 Lab Two -- TCPReceiver
- TCPReceiver 简述
- 索引转换
- TCPReceiver 实现
- 测试
对应课程视频: 【计算机网络】 斯坦福大学CS144课程
Lab Two 对应的PDF: Lab Checkpoint 2: the TCP receiver
TCPReceiver 简述
在 Lab2,我们将实现一个 TCPReceiver,用以接收传入的 TCP segment 并将其转换成用户可读的数据流。
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 和 FIN,64 位非循环计数。
这是一个简单浅显的例子,用于区分开三种索引的区别:
序列号和绝对序列号之间相互转换稍微有点麻烦,因为序列号是循环计数的。在该实验中,CS144 使用自定义类型 WrappingInt32 表示序列号,并编写了它与绝对序列号之间的转换。
- 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 包时终止数据传输。
- 如果接收到了 SYN 包,则设置 ISN 编号。
-
ackno()
:返回接收方尚未获取到的第一个字节的字节索引。如果 ISN 暂未被设置,则返回空。 -
window_size()
:返回接收窗口的大小,即第一个未组装的字节索引和第一个不可接受的字节索引之间的长度。
这是 CS144 对 TCP receiver 的期望执行流程:
三次握手:
实现思路:
对于 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() 表示.