CS 144 Lab Three-- the TCP sender

news2024/12/25 8:59:41

CS 144 Lab Three -- the TCP sender

  • TCPSender 功能
  • 如何检测丢包
  • TCPSender 要求
  • TCPSender 状态转换图
  • TCPSender 实现
  • 测试


对应课程视频: 【计算机网络】 斯坦福大学CS144课程

Lab Three 对应的PDF: Lab Checkpoint 3: the TCP sender


TCPSender 功能

TCP Sender 负责将数据以 TCP 报文的形式发送,其需要完成的功能有:

  • 将 ByteStream 中的数据以 TCP 报文形式持续发送给接收者。
  • 处理 TCPReceiver 传入的 ackno 和 window size,以追踪接收者当前的接收状态,以及检测丢包情况。
  • 经过一个超时时间后仍然没有接收到 TCPReceiver 发送的针对某个数据包的 ack 包,则重传对应的原始数据包。

如何检测丢包

TCP 使用超时重传机制。TCPSender 除了将原始数据流分解成众多 TCP 报文并发送以外,它还会追踪每个已发送报文(已被发送但还未被接收)的发送时间。如果某些已发送报文太久没有被接收方确认(即接收方接收到对应的 ackno),则该数据包必须重传

需要注意的是,接收方返回的 ackno 并不一定对应着发送方返回的 seqno(也不和 seqno 有算数关系),这是因为发送的数据可能会因为内存问题,被接收方截断。

接收方确认某个报文,指的是该报文的所有字节索引都已被确认。这意味着如果该报文只有部分被确认,则不能说明该报文已被完全确认。

TCP 的超时机制比较麻烦,这是因为超时机制直接影响到应用程序从远程服务器上读取数据的响应时间,以及影响到网络拥堵的程度。以下是实现 TCPSender 时需要注意的一些点:

  • 每隔几毫秒,TCPSender的 tick 函数将会被调用,其参数声明了过去的时间。这是 TCPSender 唯一能调用的超时时间相关函数。因为直接调用 clock 或者 time 将会导致测试套件不可用。

  • TCPSender 在构造时会被给予一个重传超时时间 RTO的初始值。RTO 是在重新发送未完成 TCP 段之前需要等待的毫秒数。RTO值将会随着时间的流逝(或者更应该说是网络环境的变化)而变化,但初始的RTO将始终不变。

  • 在 TCPSender 中,我们需要实现一个重传计时器。该计时器将会在 RTO 结束时进行一些操作。

  • 当每次发送包含数据的数据包时,都需要启动重传计时器,并让它在 RTO 毫秒后超时。若所有发送中报文均被确认,则终止重传计时器。

  • 如果重传计时器超时,则需要进行以下几步(稍微有点麻烦)

    • 重传尚未被 TCP 接收方完全确认的最早报文(即最低 ackno所对应的报文)。这一步需要我们将发送中的报文数据保存至一个新的数据结构中,这样才可以追踪正处于发送状态的数据。

    • 如果接收者的 window size 不为 0,即可以正常接收数据,则

      • 跟踪连续重传次数。过多的重传次数可能意味着网络的中断,需要立即停止重传。
      • 将RTO的值设置为先前的两倍,以降低较差网络环境的重传速度,以避免加深网络环境的拥堵。
      • 重置并重启重传计时器。

接收者 window size 为 0 的情况将在下面说明。

  • 当接收者给发送者一个确认成功接收新数据的 ack 包时(absolute ack seqno 比之前接收到的 ackno 更大):

    • 将 RTO 设置回初始值
    • 如果发送方存在尚未完成的数据,则重新启动重传定时器
    • 连续重传计数清零。

TCPSender 要求

在该实验中,我们需要完成 TCPSender 的以下四个接口:

  • fill_window:TCPSender 从 ByteStream 中读取数据,并以 TCPSegement 的形式发送,尽可能地填充接收者的窗口。但每个TCP段的大小不得超过 TCPConfig::MAX PAYLOAD SIZE

    • 若接收方的 Windows size 为 0,则发送方将按照接收方 window size 为 1 的情况进行处理,持续发包。

    • 因为虽然此时发送方发送的数据包可能会被接收方拒绝,但接收方可以在反向发送 ack 包时,将自己最新的 window size 返回给发送者。否则若双方停止了通信,那么当接收方的 window size 变大后,发送方仍然无法得知接收方可接受的字节数量。

    • 若远程没有 ack 这个在 window size 为 0 的情况下发送的一字节数据包,那么发送者重传时不要将 RTO 乘2。这是因为将 RTO 双倍的目的是为了避免网络拥堵,但此时的数据包丢弃并不是因为网络拥堵的问题,而是远程放不下了。

  • ack_received:对接收方返回的 ackno 和 window size 进行处理。丢弃那些已经完全确认但仍然处于追踪队列的数据包。同时如果 window size 仍然存在空闲,则继续发包。

  • tick:该函数将会被调用以指示经过的时间长度。发送方可能需要重新发送一些超时且没有被确认的数据包。

  • send_empty_segment:生成并发送一个在 seq 空间中长度为 0正确设置 seqno 的 TCPSegment,这可让用户发送一个空的 ACK 段。


TCPSender 状态转换图

我们无需定义新的状态变量,只需合理利用好各个公共接口的状态,即可快速确认当前的状态:
在这里插入图片描述


TCPSender 实现

注意点:

  • 当 SYN 设置后,payload 应该在尽可能装的基础之上,少装入 1byte,因为这个 byte 大小被 SYN 占用。
    • 而在 payload 尽可能装的基础上,若 FIN 装不下了,则必须在下一个包中装入 FIN 。
  • FIN 包的发送必须满足三个条件:
    • 从来没发送过 FIN。这是为了防止发送方在发送 FIN 包并接收到 FIN ack 包之后,循环用 FIN 包填充发送窗口的情况。
    • 输入字节流处于 EOF
    • window 减去 payload 大小后,仍然可以存放下 FIN
  • 当循环填充发送窗口时,若发送窗口大小足够但本地没有数据包需要发送,则必须停止发送。
    • 若当前 Segment 是 FIN 包,则在发送完该包后,立即停止填充发送窗口。
  • 重传定时器追踪的是发送者距离上次接收到新 ack 包的时间,而不是每个处于发送中的包的超时时间。因此除 SYN 包以外(它会启动定时器),其他发包操作将不会重置 重传定时器,同时也无需为每个数据包配备一个定时器。
    • 同时,只有存在新数据包被接收方确认后,才会重置定时器。
    • tick 函数也是类似,只有存在处于发送状态的数据包时,重传定时器才起作用。若重传定时器超时,则重传的是第一个 seqno 最小且尚未重传的数据包。
  • 当接收方的 window size 为 0 时,仍旧按照 window size 为 1 时去处理,发送一字节数据。但是,若远程没有发送 ack 包的时候,不要将 RTO 双倍,还是重置为之前的 RTO。

此部分可参考<<自顶向下学习计算机网络>> 3.5 小节
在这里插入图片描述

首先,我们来看一下代码中涉及到相关类:

  • TCPSegment : tcp报文内存中的数据载体,主要负责Buffer和Tcp数据报结构体之间的序列号与反序列化
// tcp_segment.hh
class TCPSegment {
  private:
    TCPHeader _header{};
    // Buffer就是对String字符串相关操作的封装
    Buffer _payload{};

  public:
    // Parse the segment from a string
    ParseResult parse(const Buffer buffer, const uint32_t datagram_layer_checksum = 0);

    // Serialize the segment to a string
    BufferList serialize(const uint32_t datagram_layer_checksum = 0) const;

    const TCPHeader &header() const { return _header; }
    TCPHeader &header() { return _header; }

    const Buffer &payload() const { return _payload; }
    Buffer &payload() { return _payload; }
    
    // Segment's length in sequence space
    // Equal to payload length plus one byte if SYN is set, plus one byte if FIN is set
    size_t length_in_sequence_space() const;
};
// tcp_segment.hh
//    buffer string/Buffer to be parsed
//    datagram_layer_checksum pseudo-checksum from the lower-layer protocol
//    datagram_layer_checksum 是当前层下一层对数据报计算得到的校验和 
ParseResult TCPSegment::parse(const Buffer buffer, const uint32_t datagram_layer_checksum) {
    // 对buffer内容计算校验和,与下面一层传入的校验和进行比对,如果不一致直接返回
    InternetChecksum check(datagram_layer_checksum);
    check.add(buffer);
    if (check.value()) {
        return ParseResult::BadChecksum;
    }
    // 解析拿到tcp协议请求头和请求体
    NetParser p{buffer};
    _header.parse(p);
    _payload = p.buffer();
    return p.get_error();
}


// 请求体大小+syn标志位占据的一个序列号+fin标志位占据的一个序列号
size_t TCPSegment::length_in_sequence_space() const {
    return payload().str().size() + (header().syn ? 1 : 0) + (header().fin ? 1 : 0);
}

//  datagram_layer_checksum pseudo-checksum from the lower-layer protocol
BufferList TCPSegment::serialize(const uint32_t datagram_layer_checksum) const {
    // 拿到TCP请求头  
    TCPHeader header_out = _header;
    header_out.cksum = 0;

    // calculate checksum -- taken over entire segment
    // 计算TCP报文的校验和 --- 如果TCP报文校验和与传入的校验和计算一致,那么check.value()返回值应该为0
    InternetChecksum check(datagram_layer_checksum);
    check.add(header_out.serialize());
    check.add(_payload);
    header_out.cksum = check.value();
    
    // 将TCP报文添加到BufferList中
    BufferList ret;
    ret.append(header_out.serialize());
    ret.append(_payload);

    return ret;
}

SYN和FIN标志需要占用一个序列号,tcp使用序列号来标识一段字节流,但是序列号和流重组器中的index流索引之间并不是一一对应的关系,序列号和index流索引进行转换时,需要去掉SYN和FIN标志占用的序列号。

  • libsponge/tcp_sender.hh
//! Accepts a ByteStream, divides it up into segments and sends the
//! segments, keeps track of which segments are still in-flight,
//! maintains the Retransmission Timer, and retransmits in-flight
//! segments if the retransmission timer expires.
class TCPSender {
  private:
    int _timeout{-1};
    int _timecount{0};
    // 记录已经发送但是还没有确认的TCP报文段及其起始序列号---该集合是有序的
    std::map<size_t, TCPSegment> _outgoing_map{};
    // 记录已经发送但是还没有确认的字节数量
    size_t _outgoing_bytes{0};
    // 记录接收端上一次传回来的窗口大小
    size_t _last_window_size{1};
    // 是否设置SYN标志
    bool _set_syn_flag{false};
    // 是否设置FIN标志
    bool _set_fin_flag{false};
    // 连续重传计数
    size_t _consecutive_retransmissions_count{0};
    // 初始序列号
    WrappingInt32 _isn;
    // outbound queue of segments that the TCPSender wants sent
    // 将需要发送的TCP数据报塞入这个队列即可发送出去
    std::queue<TCPSegment> _segments_out{};
    // 重传计时器初始的重传时间
    unsigned int _initial_retransmission_timeout;
    // 等待被发送的字节流
    ByteStream _stream;
    // 下一个发送的字节对应的序列号
    uint64_t _next_seqno{0};
  public:
      ...
};
  • libsponge/tcp_sender.cc
// 返回已经发送但是没有手动ack的字节数量
uint64_t TCPSender::bytes_in_flight() const { return _outgoing_bytes; }

在这里插入图片描述

void TCPSender::fill_window() {
    // 如果远程窗口大小为 0, 则把其视为 1 进行操作
    size_t curr_window_size = _last_window_size ? _last_window_size : 1;
    // 循环填充窗口
    while (curr_window_size > _outgoing_bytes) {
        // 尝试构造单个数据包
        // 如果此时尚未发送 SYN 数据包,则立即发送
        TCPSegment segment;
        if (!_set_syn_flag) {
            segment.header().syn = true;
            _set_syn_flag = true;
        }
        // 设置 seqno --- 当前这批数据字节流的起始序号
        segment.header().seqno = next_seqno();

        // 装入 payload 
        const size_t payload_size =
            min(TCPConfig::MAX_PAYLOAD_SIZE,
            // 减去SYN可能占据的一个序列号大小 -- SYN和FIN虽然不占据实际payload空间,但是会占据一个序列号
            curr_window_size - _outgoing_bytes - segment.header().syn);
        // 从待读取字节流中读取payload_size大小的字节数据出来    
        string payload = _stream.read(payload_size);

        /**
         * 读取好后,如果满足以下条件,则增加 FIN
         *  1. 从来没发送过 FIN
         *  2. 输入字节流处于 EOF
         *  3. window 减去 payload 大小后,仍然可以存放下 FIN
         */
        if (!_set_fin_flag && _stream.eof() && payload.size() + _outgoing_bytes < curr_window_size)
            _set_fin_flag = segment.header().fin = true;
        // 将payload载入tcp报文结构体对象
        segment.payload() = Buffer(move(payload));

        // 如果没有任何数据,则停止数据包的发送
        if (segment.length_in_sequence_space() == 0)
            break;

        // 如果没有正在等待的数据包,则重设重传计时器的超时时间
        if (_outgoing_map.empty()) {
            _timeout = _initial_retransmission_timeout;
            _timecount = 0;
        }

        // 发送组装好的TCP报文
        _segments_out.push(segment);

        // 追踪这些数据包 --> 累加记录已经发送但是还没有ack的字节数
        _outgoing_bytes += segment.length_in_sequence_space();
        _outgoing_map.insert(make_pair(_next_seqno, segment));
        // 更新待发送 abs seqno
        _next_seqno += segment.length_in_sequence_space();

        // 如果设置了 fin,则直接退出填充 window 的操作
        if (segment.header().fin)
            break;
    }
}

//! \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) {
    size_t abs_seqno = unwrap(ackno, _isn, _next_seqno);
    // 如果传入的 ack 是不可靠的,则直接丢弃
    if (abs_seqno > _next_seqno)
        return;
    // 遍历数据结构,将已经接收到的数据包丢弃
    for (auto iter = _outgoing_map.begin(); iter != _outgoing_map.end();) {
        // 如果一个发送的数据包已经被成功接收
        const TCPSegment &seg = iter->second;
        // 当前数据包的起始序列号加上总字节数 <= 接收端传回的ackno,说明当前数据包已经被成功接收了
        if (iter->first + seg.length_in_sequence_space() <= abs_seqno) {
            // 已经发出但是还未确认的字节数减去对应的大小
            _outgoing_bytes -= seg.length_in_sequence_space();
            // 从map集合中移除当前数据包
            iter = _outgoing_map.erase(iter);

            // 如果有新的数据包被成功接收,则清空超时时间
            _timeout = _initial_retransmission_timeout;
            _timecount = 0;
        }
        // 如果当前遍历到的数据包还没被接收,则说明后面的数据包均未被接收,因此直接返回
        else
            break;
    }
    // 重传次数归零
    _consecutive_retransmissions_count = 0;
    // 更新当前接收方窗口大小
    _last_window_size = window_size;
    // 尝试传输数据
    fill_window();
}

在这里插入图片描述

//! \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) {
    _timecount += ms_since_last_tick;

    auto iter = _outgoing_map.begin();
    // 如果存在发送中的数据包,并且定时器超时
    if (iter != _outgoing_map.end() && _timecount >= _timeout) {
        // 如果窗口大小不为0还超时,则说明网络拥堵
        if (_last_window_size > 0)
            _timeout *= 2;
        _timecount = 0;
        _segments_out.push(iter->second);
        // 连续重传计时器增加
        ++_consecutive_retransmissions_count;
    }
}

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

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

测试

在 build 目录下执行 make 后执行 make check_lab3:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/777507.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

27 Deep Belief Network

文章目录 27 Deep Belief Network——深度信念网络27.1 DBN是什么&#xff1f;27.2 为什么要使用DBN27.2.1 DBN的思想是怎么来的&#xff1f;27.2.2 RBM的叠加可以提高ELBO 27.3 训练方式 27 Deep Belief Network——深度信念网络 27.1 DBN是什么&#xff1f; DBN(Deep Belie…

【机器学习】分类算法 - KNN算法(K-近邻算法)KNeighborsClassifier

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;零基础快速入门人工智能《机器学习入门到精通》 K-近邻算法 1、什么是K-近邻算法&#xff1f;2、K-近邻算法API3、…

FastDFS与Springboot集成

&#x1f680; FastDFS与Springboot集成 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风…

139、仿真-基于51单片机一氧化碳(CO)气体检测仿真设计(程序+Proteus仿真+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、设计功能 二、Proteus仿真图​编辑 三、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 方案选择 单片机的选择 方案一&#xff1…

【K8S系列】深入解析k8s网络插件—Calico

序言 做一件事并不难&#xff0c;难的是在于坚持。坚持一下也不难&#xff0c;难的是坚持到底。 文章标记颜色说明&#xff1a; 黄色&#xff1a;重要标题红色&#xff1a;用来标记结论绿色&#xff1a;用来标记论点蓝色&#xff1a;用来标记论点 Kubernetes (k8s) 是一个容器编…

视频理解多模态大模型(大模型基础、微调、视频理解基础)

转眼就要博0了&#xff0c;导师开始让我看视频理解多模态方向的内容&#xff0c;重新一遍打基础吧&#xff0c;从Python&#xff0c;到NLP&#xff0c;再到视频理解&#xff0c;最后加上凸优化&#xff0c;一步一步来&#xff0c;疯学一个暑假。写这个博客作为我的笔记以及好文…

代码随想录算法训练营第55天|392 115

392 双指针法很简单 class Solution { public:bool isSubsequence(string s, string t) {int i0;for (int j0; j<t.size() && i<s.size(); j) {if (t[j]s[i]) {i;}}return is.size();} }; 用动态规划来写的话 逻辑其实跟1143 1035是一样的 最后返回看dp[s.size…

Vue element el-input输入框 实现 ’空格+enter’组合键:换行,enter:发送,使用keydown和keyup键盘事件来实现

需求 输入框 &#xff0c;输入内容后 &#xff0c;按enter空格键 换行&#xff0c;按enter键 发送调取接口 思路 jquery的也分为三个过程&#xff0c;在事件名称上有所不同 1、某个键盘的键被松开&#xff1a;keyup 2、某个键被按下&#xff1a;keydown 3、某个键盘的键被按…

基于查找表(lookup table,LUT)方法反演植被参数

LUT指显示查找表&#xff08;Look-Up-Table)&#xff0c;本质上就是一个RAM。它把数据事先写入RAM后&#xff0c;每当输入一个信号就等于输入一个地址进行查表&#xff0c;找出地址对应的内容&#xff0c;然后输出。 LUT的应用范围比较广泛&#xff0c;例如&#xff1a;LUT(Lo…

机器学习:Self-supervised Learning for Speech and image

review : self-supervised learning for text Self-supervised learning for speech 使用Speech版本的bert能比较好的作用于语音任务上&#xff0c;如果没有self-supervised的话&#xff0c;别的模型可能需要上万小时的数据。 Superb ytb课程&#xff1a;MpsVE60iRLM工具&…

vulnhub打靶--lampiao

目录 vulnhub--lampiao1.扫描主机端口&#xff0c;发现1898端口部署web2.打开robots.txt发现CHANGELOG.txt文件3.发现drupal更新日志&#xff0c;drupal这个版本有公开exp。利用msf打下4.执行uname -a 或者上传漏洞suggest脚本&#xff0c;可以发现有脏牛提权5.上传脚本到目标&…

2023年7月19日,锁升级,网络编程

锁升级 锁的四种状态&#xff1a;无锁、偏向锁、轻量级锁、重量级锁&#xff08;JDK1.6&#xff09; 无锁&#xff1a;操作数据时不会上锁 偏向锁&#xff1a;会偏向于第一个访问锁的线程&#xff0c; 如果在运行过程中&#xff0c;只有一个线程访问加锁的资源&#xff0c;不存…

JavaWeb+Vue分离项目实现增删改查

文章目录 前言数据库后端代码util 代码listener 代码filter 代码po 代码dao 层增删改查代码service 层增删改查代码controller 层增删改查代码 前端代码查询操作删除功能增加功能修改方法路由传参修改会话存储修改 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&…

Java037——多线程

当涉及到计算机操作系统中的并发执行时&#xff0c;进程和线程是两个核心概念。 一、程序(program) 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码&#xff0c;静态对象。 二、进程&#xff08;Process&#xff09; 进程&#xff0…

MD5数据加密方法

什么场景需要使用数据加密呢&#xff1f;比如秘密数据传输、用户密码加密存储等等 数据传输可使用密钥对的方式进行加密解密&#xff0c;使用签名方式验证数据是否可靠&#xff0c;而密码加密存储可使用MD5等一些算法对数据进行单向加密 一、MD5单向加密 1、百度说法&#x…

【基础统计学】带重叠差分置信区间的检验

一、说明 对于统计模式识别&#xff0c;需要从基本的检验入手进行学习掌握&#xff0c;本篇是对统计中存在问题的探讨&#xff1a;如果两个分布有重叠该怎么做。具体的统计学原理&#xff0c;将在本人专栏中系统阐述。 二、几个重要概念 2.1 什么是假设检验 假设检验是一种统计…

第二节 C++ 数据类型

文章目录 1. 概述1.1 数据类型的重要作用 (了解) 2. 数据类型2.1 什么是进制 ?2.1.1 存储单位 2.2 整数类型2.2.1 整数类型使用2.2.2 超出范围2.2.3 关键字 sizeof 2.3 实型(浮点型)2.3.1 setprecision()函数2.3.2 科学计数 (了解即可) 2.4 字符型2.4.1 字符型定义2.4.2 ASCII…

树-用Java托举

再讲完前面几个数据结构后&#xff0c;下面&#xff0c;我们开始对树进行一个讲解分析 树 引言 树是一种重要的数据结构&#xff0c;在计算机科学中有着广泛的应用。树是由节点和边组 成的非线性数据结构&#xff0c;具有层次结构和递归定义的特点。每个节点可以有多个子 节点…

【英杰送书第三期】Spring 解决依赖版本不一致报错 | 文末送书

Yan-英杰的主 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 问题描述 报错信息如下 报错描述 解决方法 总结 【粉丝福利】 【文末送书】 目录&#xff1a; 本书特色&#xff1a; 问题描述 报错信息如下 Description:An attempt…

Docker 命令(二)

查看 docker 版本信息 docker version #查看版本信息docker 信息查看 docker info Client:Context: defaultDebug Mode: falsePlugins:app: Docker App (Docker Inc., v0.9.1-beta3)buildx: Build with BuildKit (Docker Inc., v0.5.1-docker)Server:Containers: 0 …