CS144 计算机网络 Lab3:TCP Sender

前言

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

代码实现

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 sentstd::queue<TCPSegment> _segments_out{};// 未被确认的报文段std::queue<std::pair<TCPSegment, uint64_t>> _outstand_segments{};//! retransmission timer for the connectionunsigned int _initial_retransmission_timeout;//! outgoing stream of bytes that have not yet been sentByteStream _stream;//! the (absolute) sequence number for the next byte to be sentuint64_t _next_seqno{0};// ackno checkpointuint64_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 TCPSenderTCPSender(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 writerByteStream &stream_in() { return _stream; }const ByteStream &stream_in() const { return _stream; }//! \brief A new acknowledgment was receivedbool 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 possiblevoid fill_window();//! \brief Notifies the TCPSender of the passage of timevoid 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 rowunsigned 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 sentuint64_t next_seqno_absolute() const { return _next_seqno; }//! \brief relative seqno for the next byte to be sentWrappingInt32 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 是直接将其丢弃掉了。如果我们重传的包被成功接收了,并且使接收方成功重组了整个发送窗口内的数据,就不需要重传后续的报文段了。如果没有成功重组,仍有部分数据缺失,接收方会回复一个它想要的报文段的序号,到时候重传这个报文段就行了。

发送报文段

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

假设最后一个红色矩形就是上次发送的最后一个报文段,那么 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 stringBuffer(std::string &&str) noexcept : _storage(std::make_shared<std::string>(std::move(str))) {}//! \name Expose contents as a std::string_viewstd::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 stringsize_t size() const { return str().size(); }//! \brief Make a copy to a new std::stringstd::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_

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式之详解

概念 在软件工程中&#xff0c;设计模式是指软件设计问题的推荐方案。 设计模式一般是描述如何组织代码和使用最佳实践来解决常见的设计问题。 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。 好处 设计模式可以提高代码的可重用性和可读…

1.RabbitMQ介绍

一、MQ是什么&#xff1f;为什么使用它 MQ&#xff08;Message Queue&#xff0c;简称MQ&#xff09;被称为消息队列。 是一种用于在应用程序之间传递消息的通信方式。它是一种异步通信模式&#xff0c;允许不同的应用程序、服务或组件之间通过将消息放入队列中来进行通信。这…

Linux Day11---mbash项目(二)

观看本文之前请先阅读Linux Day10的相关内容 1.touch 1.1 open系统调用 int open(const char*path,int oflags,mode_t mode); oflags参数&#xff1a; O_APPEND:把写入数据追加在文件的末尾 O_TRUNC:把文件长度设置为0&#xff0c;丢弃已有的内容 O_CREAT:如果需要&#…

vue+file-saver+xlsx+htmlToPdf+jspdf实现本地导出PDF和Excel

页面效果如下&#xff08;echarts图表按需添加&#xff0c;以下代码中没有&#xff09; 1、安装插件 npm install xlsx --save npm install file-saver --save npm install html2canvas --save npm install jspdf --save2、main.js引入html2canvas import htmlToPdf from …

【java】LinkedList 和 ArrayList的简介与对比

Java LinkedList和 ArrayList 在使用上&#xff0c;几乎是一样的。由于LinkedList是基于双向链表的&#xff0c;会多出list.getFirst();获取头部元素等方法 链表&#xff08;Linked list&#xff09;是一种常见的基础数据结构&#xff0c;是一种线性表&#xff0c;但是并不会按…

JavaWeb学习-Day10

SpringBootWeb案例 准备工作 开发流程&#xff1a; 开发接口步骤&#xff1a; 删除部门&#xff1a; 新增部门&#xff1a; 简化代码&#xff1a; limit:分页展示&#xff0c;公式&#xff1a;&#xff08;页数-1&#xff09;*页面总数&#xff0c;页面总数 目前出现的问题&am…

VR全景:助力乡村振兴,实现可持续发展

引言&#xff1a; 随着科技的飞速发展&#xff0c;虚拟现实&#xff08;VR&#xff09;全景技术正在以惊人的速度改变着我们的生活方式和产业格局。全景技术不仅在娱乐、教育等领域取得了巨大成功&#xff0c;也为乡村振兴提供了全新的机遇。通过以乡村为背景的VR全景体验&…

AR室内导航技术之技术说明与效果展示

随着科技的飞速发展&#xff0c;我们周围的环境正在经历着一场数字化的革命。其中&#xff0c;AR室内导航技术以其独特的魅力&#xff0c;为我们打开了一扇通往全新数字化世界的大门。本文将为您详细介绍这一技术的实现原理、工具应用以及成品展示&#xff0c;带您领略AR室内导…

opencv-全景图像拼接

运行环境 python3.6 opencv 3.4.1.15 stitcher.py import numpy as np import cv2class Stitcher:#拼接函数def stitch(self, images, ratio0.75, reprojThresh4.0,showMatchesFalse):#获取输入图片(imageB, imageA) images#检测A、B图片的SIFT关键特征点&#xff0c;并计算…

RunnerGo中WebSocket、Dubbo、TCP/IP三种协议接口测试详解

大家好&#xff0c;RunnerGo作为一款一站式测试平台不断为用户提供更好的使用体验&#xff0c;最近得知RunnerGo新增对&#xff0c;WebSocket、Dubbo、TCP/IP&#xff0c;三种协议API的测试支持&#xff0c;本篇文章跟大家分享一下使用方法。 WebSocket协议 WebSocket 是一种…

【Linux】权限问题

Linux权限 一、Linux 权限的概念二、Linux 权限管理1. 文件访问者的分类2. 文件类型和访问权限&#xff08;事物属性&#xff09;3. 文件访问权限的相关设置方法 三、默认权限1. 对文件和目录进行操作需要的权限2. 文件和目录的默认权限3. 粘滞位 一、Linux 权限的概念 Linux …

达梦数据库物化视图介绍

概述 本文将介绍达梦数据库物化视图&#xff0c;给出其概念及相关创建、使用示例。 1.物化视图概念 物化视图 (MATERIALIZED VIEW) 是目标表在特定时间点上的一个副本&#xff0c;占用存储空间&#xff0c;即将查询出来的数据存储在数据库中。当所依赖的一个或多个基表的数据…

bh002- Blazor hybrid / Maui 使用ORM和数据库快速教程

接上篇 bh002- Blazor hybrid / Maui 保存设置快速教程 源码 10. 添加引用 Index.razor.cs 添加引用 using FreeSql.DataAnnotations; #if WINDOWS using Windows.Storage; #endif 11. 简单使用freesql ORM 初始化数据,添加数据 public partial class Index {[DisplayNam…

[oneAPI] 基于BERT预训练模型的命名体识别任务

[oneAPI] 基于BERT预训练模型的命名体识别任务 Intel DevCloud for oneAPI 和 Intel Optimization for PyTorch基于BERT预训练模型的命名体识别任务语料介绍数据集构建使用示例 命名体识别模型前向传播模型训练 结果 参考资料 比赛&#xff1a;https://marketing.csdn.net/p/f3…

Unity - 制作package 插件包

1.将制作的插件包代码放置一个根目录下 2.在跟目录下创建package.json文件 //package.json {"name": "com.unity.customlibrary", //插件包名:com.组织名.包名"displayName": "CustomLibrary", //显示的插件名"v…

OpenCV项目开发实战--基于Python/C++实现鼠标注释图像和轨迹栏来控制图像大小

鼠标指针是图形用户界面 (GUI) 中的关键组件。没有它,您就无法真正考虑与 GUI 进行交互。那么,让我们深入了解 OpenCV 中鼠标和轨迹栏的内置函数。我们将演示如何使用鼠标来注释图像,以及如何使用轨迹栏来控制图像的大小 我们将使用下图来演示 OpenCV 中鼠标指针和轨迹栏功能…

保护函数返回的利器——Linux Shadow Call Stack

写在前面 提到内核栈溢出的漏洞缓解&#xff0c;许多朋友首先想到的是栈内金丝雀&#xff08;Stack Canary&#xff09;。今天向大家介绍一项在近年&#xff0c;于Android设备中新增&#xff0c;且默默生效的安全机制——影子调用栈&#xff1a;SCS&#xff08;Shadow Call St…

Kafka单节点部署

&#x1f388; 作者&#xff1a;互联网-小啊宇 &#x1f388; 简介&#xff1a; CSDN 运维领域创作者、阿里云专家博主。目前从事 Kubernetes运维相关工作&#xff0c;擅长Linux系统运维、开源监控软件维护、Kubernetes容器技术、CI/CD持续集成、自动化运维、开源软件部署维护…

iptables的使用规则

环境中为了安全要限制swagger的访问&#xff0c;最简单的方式是通过iptables防火墙设置规则限制。 在测试服务器中设置访问swagger-ui.html显示如下&#xff0c;区分大小写&#xff1a; iptables设置限制访问9783端口的swagger字段的请求&#xff1a; iptables -A INPUT -p t…

leetcode304. 二维区域和检索 - 矩阵不可变(java)

前缀和数组 二维区域和检索 - 矩阵不可变题目描述前缀和代码演示 一维数组前缀和 二维区域和检索 - 矩阵不可变 难度 - 中等 原题链接 - 二维区域和检索 - 矩阵不可变 题目描述 给定一个二维矩阵 matrix&#xff0c;以下类型的多个请求&#xff1a; 计算其子矩形范围内元素的总…