CS144-2021|Lab3 个人实验记录

113 阅读4分钟

lab3

参考:CS144计算机网络 Lab3 | Kiprey's Blog

个人实验留档:cs144-2021

Overview

image.png

在之前的实验中,我们已经实现了TCPReceiver以及其内部的模块,在这个实验中,我们将要完成的是上图的TCPSender。

Lab 3: The TCP Sender

TCPsender主要负责:

  • 追踪发送方的window size,接收并处理tcp报文中的ackno和window size
  • 发送tcp报文,合理设置标记位(SYN、FIN),将从ByteStream中读到的数据填充到报文中的payload中
  • 追踪已经发送的报文但是没有被ack的报文
  • 超时重传没有被ack的报文

检测超时的报文

当接收到接收方发送来的ack时,只有seqno+payload szie≤ack的数据报文才算是被正确ack。

  • 设置超时时间(RTO)
  • 一旦有报文被发送,就可以开始计时
  • 当已经发送的报文中没有未被ack的报文时,就可以停止计时
  • 当计时器超时:
    • 重传最早没有被ack的报文
    • 将超时时间加倍 → exponential backoff(指数退避)
    • 重置计数器
    • 记录连续重传次数
  • 当已经发送的报文中有数据被ack时:
    • 重设RTO到初始值
    • 如果已经发送的报文中还存在没有被ack的值,则重启计数器
    • 将连续重传次数值为0

Implementing the TCP sender

  • 私有变量设置:添加下面的私有变量 选择使用std::queue<std::pair<uint64_t, TCPSegment>>来存储已经发送的报文。
		// 超时时间上限
    int _timeout;
    // 计数器
    int _timecounter;
    // 追踪发出去的但是还没有收到ack的tcpsegment
    std::queue<std::pair<uint64_t, TCPSegment>> _outstanding_queue;
    // How many sequence numbers are occupied by segments sent but not yet acknowledged
    size_t _bytes_in_flight;
    // 对方的window_size
    size_t _remote_window_sz;
    // 是否发送了含有syn标记位的报文
    bool _sent_syn;
    // 是否发送了含有fin标记位的报文
    bool _sent_fin;
    // 连续重传次数
    size_t _consecutive_retransmissions_count;
  • void fill window() 这个函数的主要作用从ByteStream中读取数据、填充tcp报文并发送。 此外,还需要合理设置标记位。 对于窗口大小的处理: 当对方窗口大小为0时,当作对方窗口大小为1; 窗口大小应该是要包括在已经发送的报文中没有被ack的数据报文,也就是说,新发送的数据报文大小+已经发送的报文中数据报文大小不能超过window size

image.png

函数实现如下:

void TCPSender::fill_window() {
    // 如果已经发送了FIN报文,则不再填充窗口
    if (_sent_fin) {
        return;
    }
    // 对方的window size为0时,需要将其看作1
    size_t window_sz = _remote_window_sz == 0 ? 1 : _remote_window_sz;
    // 填充窗口
    while (window_sz > _bytes_in_flight) {
        TCPSegment seg;
        // 如果没有发送过syn报文,则需要设置SYN标记位。第一个发送的一定是SYN报文
        if (!_sent_syn) {
            seg.header().syn = true;
            _sent_syn = true;
        }
        seg.header().seqno = next_seqno();
        // 将需要发送的数据转入payload
        size_t len = min(window_sz - _bytes_in_flight - seg.header().syn,
                         min(TCPConfig::MAX_PAYLOAD_SIZE, _stream.buffer_size()));
        seg.payload() = move(_stream.read(len));
        // 如果还有空间且输入结束,则设置fin标志位
        if (!_sent_fin && _stream.eof() && window_sz > seg.length_in_sequence_space() + _bytes_in_flight) {
            seg.header().fin = true;
            _sent_fin = true;
        }

        // 没有数据需要发送,则跳出填充窗口的过程
        if (seg.length_in_sequence_space() == 0) {
            break;
        }

        // 如果没有正在等待的包,则需要重启计数器
        // if the timer is not running, start it running
        if (_outstanding_queue.empty()) {
            _timeout = _initial_retransmission_timeout;
            _timecounter = 0;
        }

        // 发送报文
        _segments_out.push(seg);

        _outstanding_queue.push(std::make_pair(_next_seqno, seg));
        _next_seqno += seg.length_in_sequence_space();
        _bytes_in_flight += seg.length_in_sequence_space();

        // 如果发送了FIN报文,则跳出循环
        if (_sent_fin) {
            break;
        }
    }
}
  • void ack received( const WrappingInt32 ackno, const uint16 t window size) 收到了接收方发来的ack以及window size,则需要:

    • 记录对方的window size
    • 检查已经发送的报文中有没有被ack的报文,如果有,则需要将该报文从已经发送的报文中移除
    • 如果有报文被确认了,就可以重置RTO、计数器以及连续重传次数
    • 由于对方更新了window size,则最后需要调用 fill_window()发送数据

    实现如下:

    //! \param ackno The remote receiver's ackno (acknowledgment number)
    //! \param window_size The remote receiver's advertised window size
    void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
        uint64_t abs_ackno = unwrap(ackno, _isn, _next_seqno);
        _remote_window_sz = window_size;
        // 如果接收到的非法的ackno,直接返回
        if (abs_ackno > _next_seqno) return;
        // 否则,查看 outstanding_queue 中还没有被确认的 segment
        bool reset_flag = false;
        while(! _outstanding_queue.empty()){
            auto pair = _outstanding_queue.front();
            // 如果abs_ackno >= abs_seq(pair.first) + length_in_sequence_space(),表明被正确接收
            if(abs_ackno >= pair.first + pair.second.length_in_sequence_space()){
                _bytes_in_flight -= pair.second.length_in_sequence_space();
                _outstanding_queue.pop();
                if (! reset_flag){
                    reset_flag = true;
                }
            } else {
                break;
            }
        }
        // 如果有报文被确认了,就可以重置RTO、计数器以及连续重传次数
        if (reset_flag){
            _timeout = _initial_retransmission_timeout;
            _timecounter = 0;
            _consecutive_retransmissions_count = 0;
        }
        fill_window();
    }
    
  • void tick( const size t ms since last tick ) 改函数被调用时,其参数为距离上一次该函数被调用过去了多长时间。 这个函数主要负责超时重传以及记录连续重传时间 实现如下:

    //! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
    void TCPSender::tick(const size_t ms_since_last_tick) {
        // 这个函数会被定时调用
        _timecounter += ms_since_last_tick;
        // 当超时了并且有数据需要重传时
        if (_timecounter >= _timeout && !_outstanding_queue.empty()){
            auto pair = _outstanding_queue.front();
            _segments_out.push(pair.second);
            if (_remote_window_sz > 0){
                _timeout *= 2;
            }
            // 记录连续重传次数并且将计数器重置
            _consecutive_retransmissions_count ++;
            _timecounter = 0;
        }
    }
    
  • void send empty segment() 生成并发送一个在seq空间长度为0的数据报文(即不设置标记位以及payload)的报文,主要用途是发送一个空的ack报文。

    void TCPSender::send_empty_segment() {
        // 发送一个空的segment
        TCPSegment seg;
        seg.header().seqno = next_seqno();
        _segments_out.push(seg);
    }
    

TCPSender的状态转换

image.png

测试

image.png