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=n−ISN, 而实际的相对序列号应该是
A
=
{
d
e
l
t
a
+
k
⋅
2
32
∣
k
=
0
,
1
,
.
.
.
}
A=\{delta+k\cdot2^{32}| k=0,1,...\}
A={delta+k⋅232∣k=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={checkpoint−231+1≤x≤checkpoint+231∣x∈Z}. 而这里有一个比较特殊的情况, 即 c h e c k p o i n t − 2 31 checkpoint-2^{31} checkpoint−231 与 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=A∩B. - 为了确定
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=delta−uint32_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}
0≤offset≤232.
然后判断该距离, 若 o f f s e t ≤ 2 31 offset\leq 2^{31} offset≤231, 则实际 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−(232−offset). - 这里有一个特殊情况, 即 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=232−1,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={checkpoint−231+1≤x≤checkpoint+231∣x∈Z}, 以及随后会计算
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\} {checkpoint−231≤x≤checkpoint+231−1∣x∈Z}, 与 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 checkpoint−offset 表示. - 而对于 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
:
3.2 Implementing the TCP receiver
要点
- 实现
TCPReceiver
的segment_received()
、ackno()
和window_size()
三个方法 - 注意序列号(seqno)、相对序列号(absolute seqno)和流索引(stream index)三者的使用场景和转换关系
思路
实现思路基本按照任务指导, 需要注意的是 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()
表示.
代码
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 <optional>
//! \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
: