【斯坦福计网CS144项目】Lab3: TCPSender

本节进行 TCP 协议中发送端 TCPSender 的实现。Sender 拥有一个输入的 ByteStream,代表用户提供的待发送的数据,而 Sender 要负责将其组装成 TCP 数据包并发送出去。所谓发送,就是将数据包 push 到一个队列 _segments_out 中即可,下一节实现的 TCPConnection 类(也就是 Sender 和 Receiver 的所有者)会负责从该队列中取走数据包并实际发出。

除了保证 TCPSegment 中数据、序号以及 SYN,FIN 等标识设置正确,Sender 主要还需要考虑两个问题。一是接收窗口,Receiver 一次性能够接收的数据量是有限的,会通过 Header 中 Window Size 字段向 Sender 动态反馈这一信息。Sender 应保证发送途中(即已发送且未被确认接收)的数据量小于等于该窗口。

二是数据重传,由于数据包在发送途中可能丢失,Sender 在发送一个包后不能立即将其丢弃,而是放在暂存区中(称为 outstanding data),如果一段时间内没有收到 Receiver 的确认信号(通过 ackno)则需要重传。讲义中规定了将真实 TCP 协议略简化后的一系列规则如下:

  1. 时间的感知。为了保证程序测试的确定性,Sender 实现中不主动调用任何实际的时间 API,而是被动地通过外部调用其 tick 函数感知时间的经过,该函数的参数为距离上次调用 tick 后经过的毫秒数。
  2. 定时器的概念。需要实现一个 Timer,其可以被启动,经某个设定的时间间隔后触发,也可以手动停止。由于时间的感知是被动的,因此 Timer 只可能在 tick 函数中触发。
  3. Sender 构造时接受一个初始的 重传超时(retransmission timeout, RTO) 参数,该初始参数固定,同时有一个当前的 RTO 参数。每发送一个数据包,如果 Timer 未启动,启动并使其于当前 RTO 毫秒后触发。当所有暂存区数据都被 ack 时,停止 Timer。
  4. 当 Timer 在 tick 中触发时,将没有被完全 ack 的最早的(即序列号最小的)暂存数据包重传。同时,如果窗口大小非0,记录连续重传的次数,该信息将供下节实现的 TCPConnection 用于判断是否要结束连接,并使当前 RTO 翻倍(指数退避)。 然后重新根据当前 RTO(可能刚更新过)启动 Timer。
  5. 当收到对新发送数据的 ack 时,将当前 RTO 重置为初始 RTO,并将连续重传次数归零。如果暂存区非空,启动 Timer。

实现上,主要是 Timer + 三个主要函数。参考网上很多同学的代码 Timer 功能是放在了 Sender 类中实现,我这里还是单独做了一个类如下:

// Helper class to determine whether a given timeout has reached (i.e., expired) since started.
// It won't emit any signal but provide accessor for caller to check its state
// Also the class won't call any system time function but rely on its update() called to
// know time elapsed and whether timeout is reached.
class Timer {
  public:
    // start the timer with given timeout.
    // call start() on a started timer acts like reset() called first
    void start(unsigned int timeout);
    // update the timer with info of time elapsed since start/last update
    void update(unsigned int time_elapsed);
    // reset(stop) the timer
    void reset();
    // check whether the timer is active
    bool active() const { return _active; }
    // check whether the timer has timed out (if the timer is inactive, return value is false)
    bool expired() const { return _active && _expired; }

  private:
    bool _active{false};
    bool _expired{false};
    unsigned int _current_time{0};
    unsigned int _timeout{0};
};

主要函数之一: void ack_received(const WrappingInt32 ackno, const uint16_t window_size)。窗口大小用一个类的成员 _window_size 记录更新,该参数在后面的发送函数中用到。ackno 经 unwrap 后与暂存区中的数据包序号一一比较,将完全被确认的数据包移除(注意可以利用暂存区中数据包序号是从小到大有序的性质)。如果能够移除,按规则重置 RTO 和重传计数。如果移除后暂存区为空,将 Timer 停止。由于暂存区可能腾出了空位,最后调用 fill_window(见下文)尝试发送新数据包。代码如下:

void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
    _window_size = window_size;
    // use next seqno as checkpoint
    uint64_t ack_seqno = unwrap(ackno, _isn, _next_seqno);
    // it's impossible that ackno > _next_seqno, because that byte hasn't been sent yet!
    if (ack_seqno > _next_seqno) {
        return;
    }

    // remove completely ack-ed segments from the retransmission buffer
    // because the segment in retrans buffer is ordered by seqno,
    // it's ok to break once current seg can't be erased (subsequent seg has larger seqno)
    for (auto it = _retrans_buf.begin(); it != _retrans_buf.end();) {
        if (unwrap((*it).header().seqno, _isn, _next_seqno) + (*it).length_in_sequence_space() <= ack_seqno) {
            it = _retrans_buf.erase(it);
            _retrans_timeout = _initial_retransmission_timeout;
            _timer.start(_retrans_timeout);
            _consec_retrans_count = 0;
        } else
            break;
    }
    // stop the timer if retransmission buffer is clear
    if (_retrans_buf.empty())
        _timer.reset();

    // refill the window
    fill_window();
}

主要函数之二:void tick(const size_t ms_since_last_tick)。如果 Timer 已启动,则根据参数进行时间的更新,然后检查是否触发。如果触发,则按规则进行重传,增加重传计数,使 RTO 翻倍,并重启 Timer。代码如下:

void TCPSender::tick(const size_t ms_since_last_tick) {
    if (_timer.active())
        _timer.update(ms_since_last_tick);
    if (_timer.expired()) {
        _segments_out.emplace(_retrans_buf.front());
        if (_window_size > 0) {
            _consec_retrans_count++;
            _retrans_timeout *= 2;
        }
        _timer.start(_retrans_timeout);
    }
}

主要函数之三:void fill_window()。即开头一笔带过的 “发送数据、序号以及 SYN,FIN 等标识设置正确的数据包”。概括来说其流程就是:判断窗口是否未被填满 → 如果未被填满,造一个 TCPSegment,具体又分为判断放 SIN 标识,放数据,判断放 FIN 标识三步 → 将 TCPSegment 加入到 _segments_out 和暂存区中,更新序列号和剩余空间大小。循环直至窗口被填满。具体细节见下面的源文件,就不粘贴了。

完整代码链接:
tcp_sender.hh
tcp_sender.cc

通关截图
在这里插入图片描述

你可能感兴趣的:(#,CS144,计算机网络,网络)