lab2
个人实验留档:cs144-2021
Overview
这个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之间的关系
此外,在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,可以回顾这个图:
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
总结
这个实验总体来说,实现流程还是比较清晰的,但是在实现unwrap()这个函数时,需要考虑边界情况(checkpoint ≤ half),否则有可能ctest -R wrap 过了但是最终的测试没过。