在lab0中,我们实现了ByteStream类,接收和发送string类型的数据,也就是TCP的流式传输功能。在lab1中,我们实现了Reassembler类,将到达的数据报重新组装成有序的字节流。
在lab2中,我们要实现TCPReceiver类,这个类从连接方接收数据,通过调用Reassembler类将数据报转换成有序字节流,然后写入到ByteStream类中。最终应用程序从ByteStream对象中读取数据。
同时TCPReceiver也会向对方发送报文,具体包括
- 下一个需要收到的字节的序列号,也叫做确认序号或者ackno。
- 当前ByteStream对象空闲的缓冲区大小,也叫做窗口大小,window size
下面这张图是我的理解:
下面是一些实验tips
把64位序列号转换成32位序列号
我们在Reassembler类中实现的每个字节的序列号都是unsigned int64 类型,并且序列号始终从0开始。使用64位无符号整数作为序列号,基本上不会发生序列号溢出的情况。但是在TCP头部中,空间有限,序列号只能保存32位,需要考虑序列号溢出,并且需要随机初始化序列号。具体来说,需要考虑:
- 32位序列号到达最大值时,如何重用之前的序列号。
- 序列号需要随机初始化。为了提高鲁棒性并避免接收到历史报文,TCP需要保证序列号不能被简单地猜测出来,并且不太可能重复。因此,字节流的序列号不是从0开始的,而是一个随机的32位数字,称为初始序列号(ISN)。
- SYN报文和FIN报文各自需要占用一个序列号。除了确保接收所有字节的数据外,TCP还确保可靠地接收流的开始和结束。SYN占用的序列号就是上述初始序列号ISN。
- TCP建立起双向传输数据流,因此双方都会维护自己的序列号,这两个序列号互不影响。
假设我们要传送cat
这个字符串,并且此时发送方的初始序列号刚好是
2
32
−
2
2^{32}-2
232−2,下面这张表能够清楚地表示序列号、绝对序列号、数据流序列号的差异:
下图比较了三者差异:
在这部分实现中,我们要完成wrap
函数和unwrap
函数,这两个函数的解释如下:
wrap
函数的使用场景是,发送数据给对方:应用程序发送字节流给socket,socket知道每个字节的64位无符号整数绝对序列号,并且根据socket自身的起始序列号,将这个序列号转换成32位无符号整数序列号,保存在TCP头部字段中unwrap
函数的使用场景是,接收对方的数据:socket收到对方的字节流,获取字节流的32位无符号整数序列号,并且根据建立连接时保存的对方的起始序列号,以及socket需要接收的下一个32位无符号整数序列号,计算得到当前字节流的64位无符号整数绝对序列号,从而可以判断当前字节流是否在接收窗口范围内
实现TCP receiver
TCPReceiver将要实现的功能包括:
(1)从对端接收数据报,并且通过Reassembler类将数据流组合成有序的数据流,交由ByteStream类来写入(应用程序从ByteStream中读取有序的字节流)
(2)将包含确认号(ackno)和窗口大小的报文发送回给对方。
首先复习一下从对端接收到的数据报包含哪些项:
- 首字节序列号(seqno):如果当前是SYN报文,则这个序列号也是初始序列号
- SYN位:表示当前的报文是否是SYN报文,SYN报文需要占一个序列号
- payload:有效的数据,以字符串方式存储
- FIN位:表示当前字节流是否结束,FIN报文需要占一个序列号
接收到数据报之后,发送给对端的应答报文包括哪些项:
- 确认号(ackno):需要接收的下一个字节序列号
- 窗口大小:还可以接收的数据量
实现细节
我们可以把接收到的数据报文分成两类(SYN报文不会携带数据),这两类报文分开处理
- (1)SYN报文(FIN标志)
- (2)普通报文(FIN标志)
不管什么报文,首先记录是否带有FIN标记。
当接收到SYN报文时,需要记录发送方的初始序列号,ackno需要设置成1;如果接收完了所有的字节流,则需要把ackno设置成2。
当接收到普通报文时,首先把序列号转换成绝对序列号,然后使用reassembler类对数据进行整理,注意需要把插入的绝对序列号减一,因为SYN报文不携带数据,所以需要把多出来的一个字节的位置填上,然后把ackno设置成第一个乱序字节序列号加一;如果接收完了所有的字节流,则需要把ack设置成第一个乱序字节序列号加二。
在获取窗口大小的时候,要注意窗口大小是16位无符号整数。
#include "tcp_receiver.hh"
using namespace std;
void TCPReceiver::receive( TCPSenderMessage message, Reassembler& reassembler, Writer& inbound_stream )
{
// 记录FIN报文
if (message.FIN) _eof = true;
// SYN报文,记录初始序列号
// 并且判断了重复收到SYN的情况
if (message.SYN && !flag)
{
sender_zero_point = message.seqno;
flag = true;
reassembler.insert(0, message.payload.release(), message.FIN, inbound_stream);
checkpoint = reassembler.get_unass_base() + 1 + (_eof && inbound_stream.is_closed());
return;
}
// 获取报文数据,注意前提是一定要首先接收到SYN报文才可以执行这一步
if (flag)
{
// 把报文中的数据流(字符串)取出,然后根据序列号,转换成绝对序列号
uint64_t absolute_seqno = message.seqno.unwrap(sender_zero_point, checkpoint);
// 通过reassembler将无序的字节流整理成有序,然后写入到bytestream中
reassembler.insert(absolute_seqno - 1, message.payload.release(), message.FIN, inbound_stream);
// 下一个希望接收到的字节序列号
// 注意因为存在SYN报文,消耗了额外的一个序列号
// 同理,FIN报文也要消耗额外的序列号
checkpoint = reassembler.get_unass_base() + 1 + (_eof && inbound_stream.is_closed());
}
}
TCPReceiverMessage TCPReceiver::send( const Writer& inbound_stream ) const
{
// Your code here.
// 注意窗口大小只有16位,所以需要判断一下
uint16_t window_size = inbound_stream.available_capacity() > UINT16_MAX ? UINT16_MAX : inbound_stream.available_capacity();
// 如果还没有收到初始序列号,即SYN报文
if (!flag)
return TCPReceiverMessage{{}, window_size};
else
return TCPReceiverMessage{Wrap32::wrap(checkpoint, sender_zero_point), window_size};
}