【CS144】lab2

在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 2322,下面这张表能够清楚地表示序列号、绝对序列号、数据流序列号的差异:

在这里插入图片描述

下图比较了三者差异:

在这里插入图片描述

在这部分实现中,我们要完成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};
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值