CS144 计算机网络 Lab3:TCP Sender

这篇具有很好参考价值的文章主要介绍了CS144 计算机网络 Lab3:TCP Sender。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

在 Lab2 中我们实现了 TCP Receiver,负责在收到报文段之后将数据写入重组器中,并回复给发送方确认应答号。在 Lab3 中,我们将实现 TCP 连接的另一个端点——发送方,负责读取 ByteStream(由发送方上层应用程序创建并写入数据),并将字节流转换为报文段发送给接收方。

CS144 计算机网络 Lab3:TCP Sender

代码实现

TCP Sender 将负责:

  • 跟踪 TCP Receiver 的窗口,处理确认应答号和窗口大小
  • 通过从 ByteStream 中读取内容来填充发送窗口,创建新的报文段(可以包含 SYN 和 FIN 标志),并发送它们
  • 跟踪哪些分段已发送但尚未被接收方确认——我们称之为未完成报文段(outstanding segment)
  • 如果发送报文段后经过足够长的时间仍未得到确认,则重新发送未完成的报文段

由于涉及到超时处理,我们可以先实现一个简单的定时器 Timer,类声明如下所示:

class Timer {
  private:
    uint32_t _rto;          // 超时时间
    uint32_t _remain_time;	// 剩余时间
    bool _is_running;		// 是否在运行

  public:
    Timer(uint32_t rto);

    // 启动计时器
    void start();

    // 停止计时器
    void stop();

    // 是否超时
    bool is_time_out();

    // 设置过去了多少时间
    void elapse(size_t eplased);

    // 设置超时时间
    void set_time_out(uint32_t duration);
};

根据实验指导书的要求,定时器不能通过调用系统时间函数来知道过了多长时间,而是由外部传入的时长参数告知,这一点可以从 send_retx.cc 测试用例得到印证:

TCPSenderTestHarness test{"Retx SYN twice at the right times, then ack", cfg};
test.execute(ExpectSegment{}.with_no_flags().with_syn(true).with_payload_size(0).with_seqno(isn));
test.execute(ExpectNoSegment{});
test.execute(ExpectState{TCPSenderStateSummary::SYN_SENT});

// 外部指定逝去的时间
test.execute(Tick{retx_timeout - 1u});

所以这个定时器的实现就很简单,外部通过调用 Timer::elapse() 告知定时器多久过去了,定时器只要更新一下剩余时长就好了:


Timer::Timer(uint32_t rto) : _rto(rto), _remain_time(rto), _is_running(false) {}

void Timer::start() {
    _is_running = true;
    _remain_time = _rto;
}

void Timer::stop() { _is_running = false; }

bool Timer::is_time_out() { return _remain_time == 0; }

void Timer::elapse(size_t elapsed) {
    if (elapsed > _remain_time) {
        _remain_time = 0;
    } else {
        _remain_time -= elapsed;
    }
}

void Timer::set_time_out(uint32_t duration) {
    _rto = duration;
    _remain_time = duration;
}

完成定时器之后,来看看 TCPSender 类有哪些成员:

class TCPSender {
  private:
    //! our initial sequence number, the number for our SYN.
    WrappingInt32 _isn;

    //! outbound queue of segments that the TCPSender wants sent
    std::queue<TCPSegment> _segments_out{};

    // 未被确认的报文段
    std::queue<std::pair<TCPSegment, uint64_t>> _outstand_segments{};

    //! retransmission timer for the connection
    unsigned int _initial_retransmission_timeout;

    //! outgoing stream of bytes that have not yet been sent
    ByteStream _stream;

    //! the (absolute) sequence number for the next byte to be sent
    uint64_t _next_seqno{0};

    // ackno checkpoint
    uint64_t _ack_seq{0};

    // 连续重传次数
    uint32_t _consecutive_retxs{0};

    // 未被确认的序号长度
    uint64_t _outstand_bytes{0};

    // 接收方窗口长度
    uint16_t _window_size{1};

    // 是否同步
    bool _is_syned{false};

    // 是否结束
    bool _is_fin{false};

    // 计时器
    Timer _timer;

  public:
    //! Initialize a TCPSender
    TCPSender(const size_t capacity = TCPConfig::DEFAULT_CAPACITY,
              const uint16_t retx_timeout = TCPConfig::TIMEOUT_DFLT,
              const std::optional<WrappingInt32> fixed_isn = {});

    //! \name "Input" interface for the writer
    ByteStream &stream_in() { return _stream; }
    const ByteStream &stream_in() const { return _stream; }

    //! \brief A new acknowledgment was received
    bool ack_received(const WrappingInt32 ackno, const uint16_t window_size);

    //! \brief Generate an empty-payload segment (useful for creating empty ACK segments)
    void send_empty_segment();

    // 发送报文段
    void send_segment(std::string &&data, bool syn = false, bool fin = false);

    //! \brief create and send segments to fill as much of the window as possible
    void fill_window();

    //! \brief Notifies the TCPSender of the passage of time
    void tick(const size_t ms_since_last_tick);

    //! \brief How many sequence numbers are occupied by segments sent but not yet acknowledged?
    size_t bytes_in_flight() const;

    //! \brief Number of consecutive retransmissions that have occurred in a row
    unsigned int consecutive_retransmissions() const;

    //! \brief TCPSegments that the TCPSender has enqueued for transmission.
    std::queue<TCPSegment> &segments_out() { return _segments_out; }

    //! \brief absolute seqno for the next byte to be sent
    uint64_t next_seqno_absolute() const { return _next_seqno; }

    //! \brief relative seqno for the next byte to be sent
    WrappingInt32 next_seqno() const { return wrap(_next_seqno, _isn); }
};

可以看到,我们 TCPSender 有以下主要成员:

  • queue<TCPSegment> _segments_out:待发送的报文段队列,外部程序会从这个队列里面取出报文段并发送出去

  • queue<pair<TCPSegment, uint64_t>> _outstand_segments:存放未被确认的报文段和它对应的绝对序列号的队列

  • uint64_t _ack_seq:上一次收到的绝对确认应答号

  • uint32_t _consecutive_retxs最早发送的但是未被确认的报文段的重传次数,用于更新超时时间

  • uint64_t _outstand_bytes:所有未被确认的报文段所占序列号空间长度,SYN 和 FIN 也要占用一个序号

  • uint16_t _window_size:接收方窗口大小,初始值为 1,由于没有实现加性递增乘性递减(AIMD)拥塞控制机制,所以不用维护发送方的拥塞窗口大小,直接维护接收方窗口大小

  • bool _is_syned:是否成功同步

  • bool _is_fin:是否关闭连接

  • Timer _timer:定时器

先来实现一些比较简单的函数:

//! \param[in] capacity the capacity of the outgoing byte stream
//! \param[in] retx_timeout the initial amount of time to wait before retransmitting the oldest outstanding segment
//! \param[in] fixed_isn the Initial Sequence Number to use, if set (otherwise uses a random ISN)
TCPSender::TCPSender(const size_t capacity, const uint16_t retx_timeout, const std::optional<WrappingInt32> fixed_isn)
    : _isn(fixed_isn.value_or(WrappingInt32{random_device()()}))
    , _initial_retransmission_timeout{retx_timeout}
    , _stream(capacity)
    , _timer(retx_timeout) {}

uint64_t TCPSender::bytes_in_flight() const { return _outstand_bytes; }

unsigned int TCPSender::consecutive_retransmissions() const { return _consecutive_retxs; }

丢包处理

根据实验指导书中的描述:

Periodically, the owner of the TCPSender will call the TCPSender’s tick method, indicating the passage of time.

外部会定期调用 TCPSender::tick() 函数来告知它过了多长时间,TCPSender 要根据传入的时间判断最早发送的包是不是超时未被确认,如果是(定时器溢出),就说明这个包丢掉了,需要重传。

同时超时也意味着网络可能比较拥挤,沿途的某个路由器内部队列满了,再次发送也有可能丢失,不仅浪费了带宽,还会进一步加剧网络的拥堵。不如耐心点,把超时时间翻倍,如果下一次成功收到确认应答号就还原成初始超时时间。这个超时时间估计机制和 CS144 第 61 集和《计算机网络:自顶而下方法》第 158 页所讲授的指数移动平均机制不太一样:

值得注意的是,实验指导书中只将超时作为重传的条件,而没有考虑三次冗余 ACK 触发快速重传情况。因此 Timer::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) {
    _timer.elapse(ms_since_last_tick);

    if (!_timer.is_time_out())
        return;

    if (_outstand_segments.empty()) {
        return _timer.set_time_out(_initial_retransmission_timeout);
    }

    // 超时需要重发第一个报文段,同时将超时时间翻倍
    _segments_out.push(_outstand_segments.front().first);

    _consecutive_retxs += 1;
    _timer.set_time_out(_initial_retransmission_timeout * (1 << _consecutive_retxs));
    _timer.start();
}

这里只重传了一个报文段,而不是像回退 N 步(GBN)协议那样重传整个窗口内的报文段,这是因为 Lab2 中实现的接收方会缓存所有乱序到达的报文段,而 GBN 是直接将其丢弃掉了。如果我们重传的包被成功接收了,并且使接收方成功重组了整个发送窗口内的数据,就不需要重传后续的报文段了。如果没有成功重组,仍有部分数据缺失,接收方会回复一个它想要的报文段的序号,到时候重传这个报文段就行了。

发送报文段

发送方需要根据接收方的确认应答号和窗口大小决定需要发送哪些数据,假设当前数据接收情况如下图所示,绿色和蓝色的部分是已成功接收并重组的数据,红色部分是成功接收但是因为前方有报文没达到而未重组的数据:

CS144 计算机网络 Lab3:TCP Sender

假设最后一个红色矩形就是上次发送的最后一个报文段,那么 TCPSender 的各个成员的值就是图中所标注的那样,这时候调用 TCPSender::fill_window() 发送的应该是 _next_seq ~ _ack_seq + _window_size 之间的数据。不过在发送数据之前需要完成三次握手,所以需要先判断 _is_syned 是否为 true,如果为 false 就需要发送 SYN 包与接收端进行连接。所有数据都发送完成之后需要发送一个 FIN 报文段(可以携带最后一批数据或者不懈携带任何数据)说明 TCPSender 已经没有新数据要发送了,可以断开连接了。


void TCPSender::fill_window() {
    if (!_is_syned) {
        // 等待 SYN 超时
        if (!_outstand_segments.empty())
            return;

        // 发送一个 SYN 包
        send_segment("", true);
    } else {
        size_t remain_size = max(_window_size, static_cast<uint16_t>(1)) + _ack_seq - _next_seqno;

        // 当缓冲区中有待发送数据时就发送数据报文段
        while (remain_size > 0 && !_stream.buffer_empty()) {
            auto ws = min(min(remain_size, TCPConfig::MAX_PAYLOAD_SIZE), _stream.buffer_size());
            remain_size -= ws;

            string &&data = _stream.peek_output(ws);
            _stream.pop_output(ws);

            // 置位 FIN
            _is_fin |= (_stream.eof() && !_is_fin && remain_size > 0);
            send_segment(std::move(data), false, _is_fin);
        }

        // 缓冲区输入结束时发送 FIN(缓冲区为空时不会进入循环体,需要再次发送)
        if (_stream.eof() && !_is_fin && remain_size > 0) {
            _is_fin = true;
            send_segment("", false, true);
        }
    }
}


void TCPSender::send_segment(string &&data, bool syn, bool fin) {
    // 创建报文段
    TCPSegment segment;
    segment.header().syn = syn;
    segment.header().fin = fin;
    segment.header().seqno = next_seqno();
    segment.payload() = std::move(data);

    // 将报文段放到发送队列中
    _segments_out.push(segment);
    _outstand_segments.push({segment, _next_seqno});

    // 更新序号
    auto len = segment.length_in_sequence_space();
    _outstand_bytes += len;
    _next_seqno += len;
}


void TCPSender::send_empty_segment() {
    TCPSegment seg;
    seg.header().seqno = next_seqno();
    _segments_out.push(seg);
}

这里有一个地方值得思考的问题是:把同一个报文段保存到两个队列中不会导致数据的拷贝吗?实际上不会,因为 TCPSegment::_payload 的数据类型是 Buffer,它的声明如下所示:

//! \brief A reference-counted read-only string that can discard bytes from the front
class Buffer {
  private:
    std::shared_ptr<std::string> _storage{};
    size_t _starting_offset{};

  public:
    Buffer() = default;

    //! \brief Construct by taking ownership of a string
    Buffer(std::string &&str) noexcept : _storage(std::make_shared<std::string>(std::move(str))) {}

    //! \name Expose contents as a std::string_view
    std::string_view str() const {
        if (not _storage) {
            return {};
        }
        return {_storage->data() + _starting_offset, _storage->size() - _starting_offset};
    }

    operator std::string_view() const { return str(); }

    //! \brief Get character at location `n`
    uint8_t at(const size_t n) const { return str().at(n); }

    //! \brief Size of the string
    size_t size() const { return str().size(); }

    //! \brief Make a copy to a new std::string
    std::string copy() const { return std::string(str()); }

    //! \brief Discard the first `n` bytes of the string (does not require a copy or move)
    //! \note Doesn't free any memory until the whole string has been discarded in all copies of the Buffer.
    void remove_prefix(const size_t n);
};

可以看到 Buffer 内部使用智能指针 shared_ptr<string> _storage 共享了同一份字符串,当 queue.push(buffer) 的时候调用了 Buffer(const Buffer &) 拷贝构造函数,只对 _storage 指针进行赋值而不涉及字符串复制操作。同时 Buffer(string &&str) 构造函数接受右值,可以直接把传入的字符串偷取过来,无需拷贝,效率是很高的。

确认应答号处理

当发送方收到确认应答号时,需要判断这个应答号是否合法,如果收到的确认引导号落在发送窗口以外,就不去管它。否则需要重置超时时间为初始值,并移除 _outstand_segments 队列中绝对序列号小于绝对确认应答号的报文段。如果不存在未确认的报文段了就关闭定时器,否则得再次启动定时器,为重传下一个报文段做准备。

bool TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
    auto ack_seq = unwrap(ackno, _isn, _ack_seq);

    // absolute ackno 不能落在窗口外
    if (ack_seq > _next_seqno)
        return false;

    _window_size = window_size;

    // 忽略已处理过的确认应答号
    if (ack_seq <= _ack_seq)
        return true;

    _ack_seq = ack_seq;
    _is_syned = true;

    // 重置超时时间为初始值
    _timer.set_time_out(_initial_retransmission_timeout);
    _consecutive_retxs = 0;

    // 移除已被确认的报文段
    while (!_outstand_segments.empty()) {
        auto &[segment, seqno] = _outstand_segments.front();
        if (seqno >= ack_seq)
            break;

        _outstand_bytes -= segment.length_in_sequence_space();
        _outstand_segments.pop();
    }

    // 再次填满发送窗口
    fill_window();

    // 如果还有没被确认的报文段就重启计时器
    if (!_outstand_segments.empty())
        _timer.start();
    else
        _timer.stop();

    return true;
}

测试

在命令行中输入下述代码就能编译并测试所有与发送方有关的测试用例:

cd build
make -j8
ctest -R send_

测试结果如下,发现全部成功通过了:

CS144 计算机网络 Lab3:TCP Sender

总结

相比于 Lab2,Lab3 的难度更高,因为实验指导书的说明并不是很充分,很多东西还是靠复习课本和调试测试用例搞明白的,不过有一说一,CS144 的测试用例写的是真的好,代码很整洁,也用了建造者模式等设计模式,还是很值得学习的。期待最后一个实验 Lab4,以上~~文章来源地址https://www.toymoban.com/news/detail-429938.html

到了这里,关于CS144 计算机网络 Lab3:TCP Sender的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《计算机网络自顶向下》Wireshark实验 Lab4 TCP

    《计算机网络自顶向下》Wireshark Lab + 套接字编程作业 + 杂项实验室编程作业 全实验博客链接 各位好 啊 学计算机想要学好真的还是挺难的 说实话 科班学计算机如果想要花大量的时间去学 只能把平时的课程大部分时间不去上 如果班级管理严格或者说 各种因素让你不得不去上

    2023年04月09日
    浏览(57)
  • Wireshark TCP实验—Wireshark Lab: TCP v7.0(计算机网络自顶向下第七版)

    What is the IP address and TCP port number used by the client computer (source) that is transferring the file to gaia.cs.umass.edu? 根据数据包中的 tcp-ethereal-trace-1 ,其源 IP 地址为 192.168.1.102 192.168.1.102 192.168.1.102 ,端口号为 1162 1162 1162 。 What is the IP address of gaia.cs.umass.edu? On what port number is it sending and re

    2023年04月09日
    浏览(33)
  • CS 144 Lab Four -- the TCP connection

    对应课程视频: 【计算机网络】 斯坦福大学CS144课程 Lab Four 对应的PDF: Lab Checkpoint 4: down the stack (the network interface) TCPConnection 需要将 TCPSender 和 TCPReceiver 结合,实现成一个 TCP 终端,同时收发数据。 TCPConnection 有几个规则需要遵守: 对于 接收数据段 而言: 如果接收到的数据包

    2024年02月13日
    浏览(24)
  • CS 144 Lab Four 收尾 -- 网络交互全流程解析

    对应课程视频: 【计算机网络】 斯坦福大学CS144课程 本节作为Lab Four的收尾,主要带领各位来看看网络交互的整体流程是怎样的。 这里以tcp_ipv4.cc文件为起点,来探究一下cs144是如何实现整个协议栈的。 首先,项目根路径中的 tun.sh 会使用 ip tuntap 技术创建虚拟 Tun/Tap 网络设备

    2024年02月04日
    浏览(30)
  • 北京大学计算机网络lab1——MyFTP

    目录 Lab目标 一、知识补充 二、具体实现 1.数据报文格式和字符串处理 2.open函数 3.auth 4.ls 5.get和put 三、总结 ps:本人靠着计网lab几乎就足够在就业行情并不好的23年找到自己满意的工作了,计网lab的教程也非常给力,对我这种恐惧写lab的菜狗都非常友好(本人写lab3确实比较

    2024年02月07日
    浏览(35)
  • Wireshark HTTP实验—Wireshark Lab: HTTP v7.0(计算机网络自顶向下第七版)

    Is your browser running HTTP version 1.0 or 1.1? What version of HTTP is the server running? 浏览器与服务器的版本均为 H T T P / 1.1 HTTP/1.1 H TTP /1.1 。 What languages (if any) does your browser indicate that it can accept to the server? 能接受简体中文以及英文。 What is the IP address of your computer? Of the gaia.cs.umass.edu serv

    2024年02月08日
    浏览(27)
  • Wireshark IP实验—Wireshark Lab: IP v7.0(计算机网络自顶向下第七版)

    修改发送数据包的大小 跟踪的地址为 www.ustc.edu.cn text{www.ustc.edu.cn} www.ustc.edu.cn 由于自己抓的包比较凌乱,分析起来比较复杂,所以使用作者的数据包进行分析 Select the first ICMP Echo Request message sent by your computer, and expand the Internet Protocol part of the packet in the packet details window.Wh

    2024年02月04日
    浏览(27)
  • 计算机网络—TCP

    源端口号和目标端口号:16位字段,用于标识TCP连接的源和目标端口号。 序列号(Sequence Number):32位字段,用于标识发送的数据字节流中的第一个字节的序号。 确认号(Acknowledgment Number):32位字段,确认收到的字节序号,即期望接收的下一个字节的序号。 数据偏移:4位字

    2024年02月13日
    浏览(39)
  • 【计算机网络】TCP协议

    实验目的 应用所学知识: 1. 熟悉 TCP 的协议格式。 2. 理解 TCP 对序列号和确认号的使用。 3. 理解 TCP 的流量控制算法和拥塞控制算法。 实验步骤与结果 1.任务一: 将Alice.txt上传到服务器: 使用wireshark捕获数据包,看到计算机和gaia.cs.umass.edu之间的一系列 TCP 和 HTTP 通信,包

    2023年04月20日
    浏览(41)
  • 计算机网络-TCP协议

    TCP被称为面向连接的,因为在应用程序开始互传数据之前,TCP会先建立一个连接,该连接的建立涉及到 三次“握手 ”。 TCP的连接不是一条真实存在的电路,而是一条逻辑链接 ,其共同状态仅保留在两个通信端系统的TCP程序中。 TCP连接也是点对点的,即TCP连接只能存在于一

    2024年02月08日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包