CS144Lab:Lab4

目录

简单函数的实现

connect()函数的实现

segment_received()函数的实现

active()函数的实现

tick()函数的实现

剩下函数补全

[CS144 TCP协议问题](#CS144 TCP协议问题)


在Lab0-Lab3的基础上实现TCPConnection,其可以等同于TCP协议的一个端点,包含TCPSender和TCPReceiver,基本覆盖了TCP应该有的完整功能。

我说怎么前四个实验难度感觉不是很大,原来是全部堆到这来了。。。

我们要做的工作就是实现TCPSocket的核心组件-TCPConnection。可以看到Lab0实现的ByteStream分别在TCPSender和TCPReceiver内部作为入站流和出战流用来和应用层(socket)交互。此外Lab1实现的排序器作为TCPReceiver的核心组件,用来接收并存储乱序字节,最终Lab2和Lab3实现的TCPReceiver和TCPSender则是TCP Connection用来接收和发送TCP端的核心组件。如下图所示:

在这里我们需要注意入站流和出站流两个概念,入站和出站针对的是应用层而言,这个要和Socket处理网络中的tcp端的方向注意区分。考虑对于应用层而言,入站即进入应用层,所以应该是TCPReceiver中的排好序的可靠字节流ByteStream,即途中的inbound。出站即从应用出来,应该是TCPSender中代发送字节的ByteStream即outbound。

当前这个实验TCPConnection主要作为TCPSender和TCPReceiver的粘合剂,将两者有机的组合并实现TCP的基本功能。如下图所示:

实验手册也很长,我的做法就是根据手册提示,先实现简单函数(能直接调用TCPReceiver/TCPSender中现有的函数),随后不断make check,发现第一个问题,解决第一个问题如此往复,这样最终完成整个测试。

简单函数的实现

刚入手时实现以下函数:

cpp 复制代码
//返回出站流的容量,直接返回出站流容量即可。
size_t TCPConnection::remaining_outbound_capacity() const { 
     return _sender.stream_in().remaining_capacity();
}

//已经发送但尚未确认的字节数,即TCPSender中未确认队列中的字节数
size_t TCPConnection::bytes_in_flight() const { 
    return _sender.bytes_in_flight(); 
}

//已经收到,但未排序的字节数,即TCPReceiver中未排序字节数
size_t TCPConnection::unassembled_bytes() const { 
    return _receiver.unassembled_bytes();
}

//自从上次收到tcp段,已经过去的时间,未实现
size_t TCPConnection::time_since_last_segment_received() const { 
    return _time_since_last_received_counter;
}

其中最后一个函数表示自从上次收到tcp段已经过去的时间,用一个变量表示,每次tick时累计该值,每次收到tcp段后更新为0即可。

connect()函数的实现

随后实现connect函数,该函数用以发送第一个SYN报文,这里由于初始化时TCPSender的窗口大小默认是1,此时只能发送不带数据的SYN报文,所以直接让TCPSender填充窗口,随后将TCPSender的输出队列搬运到TCPConnection的输出队列中(由于此过程经常用到,所以封装成单独函数)。注意搬运过程,要给所有要发送的tcp段加上当前TCPReceiver的ackno和窗口大小。

cpp 复制代码
//发送一个SYN数据段
//由于TCPSender的初始化时窗口大小只有1,所以直接调用fill window窗口节课。
void TCPConnection::connect() {
    //第一步使其填充窗口,由于初始化窗口大小只有1,所以就是发送SYN报文
    _sender.fill_window();

    //第二步,将TCPSender发送的TCP段转至TCPConnection的发送队列,调用函数即可
    push_sender();
}

//将TCPSender发送的TCP段转至TCPConnection的发送队列,并加上必要标识
void TCPConnection::push_sender() {
    //sender的发送端不空
    while(!_sender.segments_out().empty()) {
        //先从TCPSender中取出,再将其从TCPSender中剔除
        TCPSegment seg = _sender.segments_out().front();
        _sender.segments_out().pop();
        //在交给OS前,TCPConnection需要填充TCPSender填充不了的字段,比如ackno和window
        //ackno需要有才能赋值
        if(_receiver.ackno().has_value()) {
            seg.header().ackno = _receiver.ackno().value();
            seg.header().ack = true;
        }
        //window_size是TCPReceiver自身的属性,直接复制即可。
        seg.header().win = _receiver.window_size();
        _segments_out.push(seg);
    }
}

segment_received()函数的实现

发送完第一个SYN报文后,会收到对方回的ACK报文,我们接下来处理segment_received()函数逻辑。

  1. 首先应该是检查对方是否设置了RST标志,若设置则关闭入站流和出站流(给他们的ByteStream设置set_error即可)。否则往下。

  2. 之前的time_since_last_segment_received()函数需要统计自从上次收到tcp段经过的时间,所以这里更新该变量的值为0.

  3. 接下来将TCP段取出给TCPReceiver。

  4. 这里有个小坑点,如果数据非法我们不能被激活,只有当有效数据来临后我们才能被激活并显示发送消息,所以我这里写了判断当前Receiver是否被激活(通过isn是否有值判断),如果没被激活直接返回,这样不会勿扰后面的操作。

    cpp 复制代码
    //这里添加一个函数判断当前是否已经激活receiver了
    bool  active() const {return _isn.has_value() ? true : false;}
  5. 之后如果当前tcp段设置了ACK标志,则应该将其ackno和window_size更新到TCPSender中去。

  6. 随后,如果对方发来的tcp段占用了序列号(数据、SYN、FIN)根据TCP的可靠性原则,应该显性回复对方。这里的想法是直接让TCPSender填充窗口,看有无数据段能顺带捎上我们的回复信息,如果没有数据段能顺带捎上我们的ackno的话则发送空段,最后显性推一下。

  7. 根据提示,可能会有"保持活动"的tcp段过来,即发送方不占用任何字节,且发送方序列号=ackno-1即已经发送过的最大字节,对于这种情况,我们只需要发送空段,告知其自身seqno和回复其ackno和window_size即可。

  8. 最后要加入一点判断,判断当前端点是主动关闭还是被动关闭,这点后面会讲。

最终代码有点冗长,如下:

cpp 复制代码
//接收一个TCP段
void TCPConnection::segment_received(const TCPSegment &seg) { 
    //如果设置了重置标记,则将入站和出战设置为错误状态,并终止连接。
    if(seg.header().rst) {
        //入站流关闭
        _receiver.stream_out().set_error();
        //出战流关闭
        _sender.stream_in().set_error();
        return;
    }

    //否则
    //这里先更新一下时间
    _time_since_last_received_counter = 0;
    //将tcp段给TCPReceiver
    _receiver.segment_received(seg);
    //这里判断如果是非法数据,应该直接丢弃
    if(!_receiver.active()) return;
    //如果设置了ACK,同时更新信息到TCPSender
    if(seg.header().ack) {
        _sender.ack_received(seg.header().ackno, seg.header().win);
    }
    //如果对方发来的tcp段,占用了序列号,就需要显性回应。
    //采取的措施首先是尝试fill window.如果sender没有要发送的数据捎带ackno和当前窗口信息,则发送空段回复
    if(seg.length_in_sequence_space() != 0) {
        _sender.fill_window();
        //判断sender有无数据可以捎带ackno和窗口信息,如果没有发送空段
        if(_sender.segments_out().empty()) {
            _sender.send_empty_segment();
        }
        push_sender(); //显性的推一下
    }

    //收到"保持活动"的tcp段,即发送方不占用任何字节,且当前已经设置了ackno,且发送方序列号=ackno-1
    //对方只是想知道当前的seqno和window_size
    if(_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0  
       && seg.header().seqno == (_receiver.ackno().value() - static_cast<uint32_t>(1))) {
        _sender.send_empty_segment();  //先发送一个空的段
        push_sender();                 //显性推送一下
    }

    //如果入站流在出战流到达eof前收到结束信号,说明当前是被动关闭放,需要将_linger_after_streams_finish变量置为false
    if(_receiver.stream_out().input_ended() && !_sender.stream_in().input_ended()) {
        _linger_after_streams_finish = false;
    }
}

active()函数的实现

这个函数表示当前连接要需要存续吗?若只需要存续返回true,否则返回false。

这涉及到TCP何时结束的问题,一个就是当网络情况太糟糕,连续重传次数过多时,发送RST报文,主动断开两个端点的连接,一个就是两个端点通信结束,"干净"的结束。

RST的情况比较好判断,我们直接判断当前TCPConnection的入站流和出站流是否关闭即可。接下来判断是否是干净的结束,根据文档中的描述和四次挥手的实际情况,需要在满足两个端点都满足3个先决条件的情况下,主动关闭的一方需要等待一段时间,被动关闭的一方可以直接关闭。具体的先决条件和为什么这样设计详见文档,这里TCPConnection提供了变量帮助我们辨别当前时主动关闭的还是被动关闭的。该变量需要在收到tcp段的函数中判断是主动关闭还是被动的一方,当收到fin数据报后,导致我们入站流关闭,出站流未关闭的是被动的一方,这符合四次挥手中服务器端收到客户端发送的FIN的情况。则具体函数实现如下:

cpp 复制代码
//TCP连接是否应该存活
//1.如果一旦收到RST或者自己主动发送RST
//2.所有正常流程结束,正常结束。
//否则返回true。
bool TCPConnection::active() const { 
    //1.首先判断是否接受到RST,不正常断开连接
    if(_sender.stream_in().error() || _receiver.stream_out().error()) {
        return false;
    }

    //2.干净的结束
    bool prerequisites = _receiver.stream_out().eof() && _receiver.unassembled_bytes() == 0 &&   //条件1,入站流已经结束且排序器也为空
                         _sender.stream_in().eof() && _sender.fin_sent() &&                      //条件2 出战流结束且fin报文发送过了
                         _sender.bytes_in_flight() == 0;                                         //条件3,出战流全部被确认
    if(prerequisites) {
        //如果自己不是主动关闭的一方,直接关闭即可
        if(!_linger_after_streams_finish) 
            return false;
        else { //否则若是主动关闭的一方,需要等待时间。
            TCPConfig cfg;
            if(time_since_last_segment_received() >= 10 * cfg.rt_timeout)
                return false;
            else 
                return true;
        }
    }
    return true;
}

tick()函数的实现

该函数表征的是TCP对时间的流动,逻辑如下:

  1. 首先累计_time_since_last_received_counter的时间。
  2. 将时间概念传导给TCPSender,其内部机制有超时重传。
  3. 如果连续重传次数过多,则说明当前网络堵塞,设置并发送RST报文。这里首先将自身入站流和出站流关闭,随后将原本Sender的输出队列清空(因为已经决定要断开连接了,不需要再发送数据了),在发送空段,在其中设置rst发送。
  4. 最后将2/3积压的报文推送到TCPConnection的输出队列中去。

最终代码如下所示:

cpp 复制代码
//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
//tick函数实现
void TCPConnection::tick(const size_t ms_since_last_tick) { 
    //累计上次收到received的时间
    _time_since_last_received_counter += ms_since_last_tick;
    //更新发送起的时间
    _sender.tick(ms_since_last_tick);
    //如果重传次数过多则终止连接
    if(_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS) {
        //首先将入站流和出站流关闭
        _sender.stream_in().set_error();
        _receiver.stream_out().set_error();
        //这里直接将sender的输出队列清空,因为不需要了
        _sender.clear_sender();
        //其次向对方发送带有rst标志的空段
        _sender.send_empty_segment();
        //这里发送的空段在队尾,设置rst=1
        _sender.segments_out().back().header().rst = true;
    }
    //这里可能导致sender超时重传,所以需要显性的推一下,这里统一推一下
    push_sender();
}

剩下函数补全

将以上几个重要函数全部实现以后,剩下简单函数直接补全即可。

cpp 复制代码
//应用层的数据发送
size_t TCPConnection::write(const string &data) {
    //向发送器的可靠字节流中写入
    size_t tot_writen=_sender.stream_in().write(data);
    _sender.fill_window();
    push_sender();            //向前推。
    return tot_writen;
}

//关闭入站字节流
void TCPConnection::end_input_stream() {
    //首先关闭入站字节流
    _sender.stream_in().end_input();
    _sender.fill_window();           //填充窗口,将FIN字段送入写方输出字节流
    push_sender();                   //显性推送
}

TCPConnection::~TCPConnection() {
    try {
        if (active()) {
            cerr << "Warning: Unclean shutdown of TCPConnection\n";
            // Your code here: need to send a RST segment to the peer
            //这里直接将sender的输出队列清空,因为不需要了
            _sender.clear_sender();
            //其次向对方发送带有rst标志的空段
            _sender.send_empty_segment();
            //这里发送的空段在队尾,设置rst=1
            _sender.segments_out().back().header().rst = true;
            push_sender();
        }
    } catch (const exception &e) {
        std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
    }
}

最终测试,只能通过前49个测试,查阅其他人的博客,得知是环境问题在捣鬼。。由于我用的是docker做的,是在没心情在用virtual box再弄一遍了,就直接放着吧。

完结撒花!!!这里就放到49的截图吧。

CS144 TCP协议问题

留坑待填。。

相关推荐
gcfer10 天前
CS144 Lab:Lab2
cs144·tcp/ip协议簇·tcpreceiver
cccyi723 天前
Linux Socket 编程全解析:UDP 与 TCP 实现及应用
linux·tcp socket·udp socket
gcfer1 个月前
CS144 学习导航
cs144
Rinai_R6 个月前
CS144 - LAB0
c语言·windows·计算机网络·cpp·计算机基础·cs144