lab2-TCP receiver
我们在checkpoint2中要依靠之前的代码实现一个TCP分组接收器,我们在之前的实验中实现了ByteStream和reaseambler,这些模块本身并不涉及传输控制协议(TCP)的具体细节,但是在我们完成checkpoint2时很有帮助。我们下面介绍一下TCP分组接收器的作用:
TCP 是一种协议,它能够在不可靠的数据报之上可靠地传输一对受流量控制的字节流(每个方向各一个)。两个参与方(或称为“对等方”)建立 TCP 连接,并且每个对等方同时充当“发送方”(发送自己的字节流)和“接收方”(接收对方的字节流)。TCP接收方负责接收来自发送方的消息、重新组装字节流(包括处理流的结束),并确定需要发送回发送方的消息,以进行确认和流量控制。其中确认和流量控制的作用分别是:
- 确认(acknowledgment) 的含义是:“接收方下一步需要的字节索引是多少,以便继续重新组装
ByteStream?” 这告诉发送方它需要发送或重新发送哪些字节。 - 流量控制(flow control) 的含义是:“接收方感兴趣并愿意接收的字节索引范围是多少?”(这取决于其可用的缓冲区大小)。 这告诉发送方它最多可以发送多少数据。
文档中告诉我们实现的TCP接收器需要具备以下两个功能:
- 使用
send()方法将期待的下一个字节序号回传给发送方; - 告知发送方接收端的缓冲容量,也称为接收窗口大小。
在之前的字节流实验中,我们的代码实现是建立在字节编号为uint64下的,但是由于TCP数据报的结构可知:
TCP报文中的字节编号的类型是uint32的,所以我们必须要将uint32(序列号)的字节编号向uint64(绝对序列号)的字节编号进行转化,为此,我们需要对字节编号实现一个封装。那么我们为什么要让字节流的字节编号使用uint64,而TCP报文的字节编号使用uint32呢?
原因:TCP报文使用32位序列号是因为32位序列号在网络上传输更加高效,减少带宽消耗,而且这是协议的规范。而字节流使用64位序列号是因为32位序列号只可以传输4GB的数据,长连接可能传输超过4GB的数据,超出32位能表示的范围,使用64位序列号可以避免在实际应用中出现溢出。
TCP为了防止TCP序列号预测攻击。如果ISN是可预测的(比如固定值或简单递增),攻击者可以猜测序列号并伪造TCP连接,执行会话劫持攻击。TCP采用了随机序列号的方式,TCP 设定当前传输的字节的序号的起始计数值(ISN)并不是 0,而是一个位于 [0, 2^32 - 1]范围内的一个随机值,因此这使得从序列号到绝对序列号的转化更加困难。
接下来我们来看文档中提供的信息:
- 需要处理32位整数的回绕,2^32(4 GB)这个大小并不算特别大。一旦 32 位序列号增长到 2³² - 1,下一个字节的序列号将回绕到 0。
- 为了提高网络的健壮性,并防止旧的 TCP 连接数据包被错误地识别为当前连接的数据包,TCP 采用 随机初始序列号(Initial Sequence Number, ISN)。这意味着,流的序列号并不是从 0 开始,而是从一个随机的 32 位数 ISN 开始。SYN数据包使用ISN作为序列号,注意,第一个数据字节的序列号是ISN+1。
- 逻辑上的流起点和终点各占有一个序列号。在 TCP 连接中,不仅需要确保所有字节都被正确接收,还需要 可靠地标识数据流的起点(SYN)和终点(FIN),因此,SYN和FIN各占有一个序列号,每个数据字节页各占有一个序列号。
文档中还给出了序列号,绝对序列号和字节流下标的示例,便于我们理解:
在 TCP 传输中,绝对序列号(absolute sequence number) 和 流索引(stream index) 之间的转换非常简单——只需要 加 1 或减 1 即可。然而,在 序列号(seqno) 和 绝对序列号(absolute sequence number) 之间进行转换则要复杂一些。如果混淆了这两者,可能会导致难以察觉的 Bug。为了系统性地防止此类错误,我们会使用一个 自定义类型 Wrap32 来表示 32 位的 TCP 序列号,并在其中实现它与 绝对序列号(uint64_t) 之间的转换。
wrap()方法的实现相对简单,由于是从64位转化到32位,参考上面给出的示例,我们就知道只需要将当前绝对序列号n加上SYN,即函数参数中的zero_point即可,其中由于将一个 64 位数转换为 32 位数的过程是在对 uint64 取 2^32的模,所以可以自动实现回绕。
unwrap()方法的实现比wrap()方法更加复杂,文档要求在这个函数中,利用checkpoint和zero_point找到最接近check_point的绝对序列号。其中checkpoint的作用是:如果要确定一个 uint32 的余数值对应哪一个 uint64 整数,我们就需要有一个 64 位无符号整数 checkpoint 帮我们定位我们所期望的最接近某个无符号 32 位整数的 uint64 的整数的数值。
在wrap32的实现中,我参考了这篇博客,写的十分详细,对其中的数学原理给出了很好的解释。
Wrap32 Wrap32::wrap( uint64_t n, Wrap32 zero_point )
{
return Wrap32( static_cast<uint32_t>( n ) + zero_point.raw_value_ );
}
uint64_t Wrap32::unwrap( Wrap32 zero_point, uint64_t checkpoint ) const
{
// up_bound = 2^32
const uint64_t up_bound = static_cast<uint64_t>( UINT32_MAX ) + 1;
// checkpoint_mod表示checkpoint以zeropoint为起始点在uint_32中的位置
const uint32_t checkpoint_mod = Wrap32::wrap( checkpoint, zero_point ).raw_value_;
uint32_t dis = this->raw_value_ - checkpoint_mod;
if ( dis <= ( up_bound >> 1 ) || checkpoint + dis < up_bound ) {
return checkpoint + dis;
}
return checkpoint + dis - up_bound;
}
完成wrap32并且通过测试代码之后,我们就可以开始实现TCP receiver了。在这个部分,我们需要根据实现好的wrap32作为TCP序列号实现TCP接收器,由之前的分析可知,该TCP接收器的作用有:
- 根据接收到的信息将字节序列传输给
Reassembler处理,并由它推入流中; - 向发送方发送接收报文,其中包括期待收到的下一个字节序号ackno和拥塞控制窗口大小window_size。
文档还给我们介绍了TCP协议中接收和发送报文时承载报文信息的结构体,TCPSenderMessage 和 TCPReceiverMessage:当接收器接受信息时,接受的是 TCPSenderMessage,而回传信息时需要使用 TCPReceiverMessage。
/*
* The TCPSenderMessage structure contains five fields (minnow/util/tcp_sender_message.hh):
*
* 1) The sequence number (seqno) of the beginning of the segment. If the SYN flag is set,
* this is the sequence number of the SYN flag. Otherwise, it's the sequence number of
* the beginning of the payload.
*
* 2) The SYN flag. If set, this segment is the beginning of the byte stream, and the seqno field
* contains the Initial Sequence Number (ISN) -- the zero point.
*
* 3) The payload: a substring (possibly empty) of the byte stream.
*
* 4) The FIN flag. If set, the payload represents the ending of the byte stream.
*
* 5) The RST (reset) flag. If set, the stream has suffered an error and the connection
* should be aborted.
*/
struct TCPSenderMessage
{
Wrap32 seqno { 0 };
bool SYN {};
std::string payload {};
bool FIN {};
bool RST {};
// How many sequence numbers does this segment use?
size_t sequence_length() const { return SYN + payload.size() + FIN; }
};
/*
* The TCPReceiverMessage structure contains three fields (minnow/util/tcp_receiver_message.hh):
*
* 1) The acknowledgment number (ackno): the *next* sequence number needed by the TCP Receiver.
* This is an optional field that is empty if the TCPReceiver hasn't yet received the
* Initial Sequence Number.
*
* 2) The window size. This is the number of sequence numbers that the TCP receiver is interested
* to receive, starting from the ackno if present. The maximum value is 65,535 (UINT16_MAX from
* the <cstdint> header).
*
* 3) The RST (reset) flag. If set, the stream has suffered an error and the connection
* should be aborted.
*/
struct TCPReceiverMessage
{
std::optional<Wrap32> ackno {};
uint16_t window_size {};
bool RST {};
};
其中关于这两个结构体的信息文档中的注释已经给的很清楚了,我就不过多解释。其中需要注意的是ackno使用了std::optional,因为当接收方没有接收到 SYN 为 true 的报文时不应该返回任何应答序号。
一下是TCPReceiver的框架代码,其中我们只需要实现receive和send方法:
class TCPReceiver
{
public:
// Construct with given Reassembler
explicit TCPReceiver( Reassembler&& reassembler ) : reassembler_( std::move( reassembler ) ) {}
// The TCPReceiver receives TCPSenderMessages from the peer's TCPSender.
void receive( TCPSenderMessage message );
// The TCPReceiver sends TCPReceiverMessages to the peer's TCPSender.
TCPReceiverMessage send() const;
// Access the output (only Reader is accessible non-const)
const Reassembler& reassembler() const { return reassembler_; }
Reader& reader() { return reassembler_.reader(); }
const Reader& reader() const { return reassembler_.reader(); }
const Writer& writer() const { return reassembler_.writer(); }
private:
Reassembler reassembler_;
};
由于每条TCP连接的ISN都是一个随机值,所以我们必然要在类的私有变量中维护一个ISN值,这个ISN值就是我们在上个步骤中编写的Wrap32类。此外,由于我们还需要得知SYN请求是否已经到达接收方,只有当SYN请求从发送方传输到接收方时,才可以开始正式的TCP连接建立,即TCP三次握手中的第一次握手:客户端发送一个带有SYN和初始序列号的TCP报文,接收方会记录这个初始序列号,并设置SYN_标志为true。在TCPreceiver类中我们还需要实现TCP第二次握手:服务器收到SYN后,发送一个带有SYN和ACK标志的报文,服务器的序列号也是一个随机值,确认号(ackno)是客户端的ISN+1。其中由于ackno的类型是std::optional<Wrap32>,所以我们可以验证ackno.hasValue()来确定是不是已经收到服务端发送的SYN标志。
我们为了计算绝对序列号,还需要维护一个正在期待的下一个字节的序号作为 checkpoint,而且这个值也需要回传给发送方。但是我们在这里不需要再额外设置一个uint64的私有变量进行维护,因为我们可以使用Writer::bytes_pushed()方法检查当前已经有多少字节被写入流中,并且这个值恰好就是一个表示正在期盼的下一个字节的 absolute seqno 序号。
还需要注意标志着TCP连接断开的FIN,FIN 关闭请求要在写端方法 Writer::is_closed() 为 true 时才能响应,否则会导致错误,并且FIN和SYN一样,都要计入绝对序列号的计数中。
下面是我实现的代码:
void TCPReceiver::receive( TCPSenderMessage message )
{
if ( message.RST ) {
// 如果发送方RST为ture,为接收方的读端口设置错误
reassembler_.reader().set_error();
return;
} else if ( message.SYN ) {
// 接受到SYN之后正式开始字节的传输,设置seq_为ISN
SYN_ = true;
seq_ = message.seqno;
} else if ( message.seqno == seq_ ) {
// 如果收到的序列号与ISN相同,由于SYN需要占一个字节,所以无法在当前序列号插入数据
return;
}
// checkpoint表示已经传输到重组器中的字节总数
const uint64_t checkpoint = reassembler_.writer().bytes_pushed() + SYN_;
uint64_t absolute_seqnum = message.seqno.unwrap( seq_, checkpoint );
// 由于字节流序号中没有ISN占位,所以计算出绝对序列号之后还需要进行处理
absolute_seqnum = absolute_seqnum == 0 ? absolute_seqnum : absolute_seqnum - 1;
reassembler_.insert( absolute_seqnum, std::move( message.payload ), message.FIN );
// printf("====%ld\n", reassembler_.writer().bytes_pushed());
}
TCPReceiverMessage TCPReceiver::send() const
{
// 剩余的容量为重组器writer的剩余空间
uint64_t capacity = reassembler_.writer().available_capacity();
// 要返回出去告诉发送方的窗口大小
uint16_t window_size = capacity > UINT16_MAX ? UINT16_MAX : capacity;
uint64_t expected_index = reassembler_.writer().bytes_pushed() + SYN_;
// printf("====%ld\n", expected_index);
if ( !SYN_ ) {
return { {}, window_size, reassembler_.writer().has_error() };
}
// 如果FIN到达接收方,接收方的重组器关闭,而且FIN还需要一个占位符(可以使用reassembler_.writer().is_closed()表示)
return { Wrap32::wrap( expected_index + reassembler_.writer().is_closed(), seq_ ),
window_size,
reassembler_.writer().has_error() };
}
/*tcp_receiver.hh*/
private:
Reassembler reassembler_;
// 检测报文中的SYN数据是否已经到达接收方
bool SYN_ { false };
// 表示报文开头的第一个字节的序列号,用于unwrap函数转化为绝对序列号
Wrap32 seq_ { 0 };