前言

在 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 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; // 超时需要重发第一个报文段,同时将超时时间翻倍
_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 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 队列中绝对序列号小于绝对确认应答号的报文段。如果不存在未确认的报文段了就关闭定时器,否则得再次启动定时器,为重传下一个报文段做准备。

//! \param ackno The remote receiver's ackno (acknowledgment number)
//! \param window_size The remote receiver's advertised window size
//! \returns `false` if the ackno appears invalid (acknowledges something the TCPSender hasn't sent yet)
bool TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
auto ack_seq = unwrap(ackno, _isn, _ack_seq); if (ack_seq == 0)
return true; // absolute ackno 不能落在窗口外
if (_is_syned && ack_seq > _next_seqno)
return false; _is_syned = true;
_window_size = window_size;
_ack_seq = ack_seq; // 重置超时时间为初始值
_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_

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

总结

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

CS144 计算机网络 Lab3:TCP Sender的更多相关文章

  1. 计算机网络及TCP/IP知识点(全面,慢慢看)

    TCP/IP网络知识点总结 一.总述 1.定义:计算机网络是一些互相连接的.自治的计算机的集合.因特网是网络的网络. 2.分类: 根据作用范围分类: 广域网 WAN (Wide Area Networ ...

  2. 计算机网络 之 TCP协议报文结构

    前言:上学期实训课,由于要做一个网络通信的应用,期间遇到各种问题,让我深感计算机网络知识的薄弱.于是上网查找大量的资料,期间偶然发现了roc大神的博客,很喜欢他简明易懂的博文风格.本文受roc的< ...

  3. 计算机网络要点---TCP

    计算机网络要点---TCP 浏览器在通过域名通过dns服务器找到你的服务器外网ip,将http请求发送到你的服务器,在tcp3次握手之后(http下面是tcp/ip),通过tcp协议开始传输数据,你的 ...

  4. CS144 计算机网络 Lab2:TCP Receiver

    前言 Lab1 中我们使用双端队列实现了字节流重组器,可以将无序到达的数据重组为有序的字节流.Lab2 将在此基础上实现 TCP Receiver,在收到报文段之后将数据写入重组器中,并回复发送方. ...

  5. 计算机网络:TCP协议建立连接的过程为什么是三次握手而不是两次?【对于网上的两种说法我的思考】

    网上关于这个问题吵得很凶,但是仔细看过之后我更偏向认为两种说的是一样的. 首先我们来看看 TCP 协议的三次握手过程 如上图所示: 解释一下里面的英文: 里面起到作用的一些标志位就是TCP报文首部里的 ...

  6. 计算机网络12 TCP

    1 TCP简介 CP的全称是Transmission Control Protocol,即传输控制协议,TCP工作在传输层上 其职责是:实现主机间进程到进程的通信,其次还需要保证可靠性(不是安全性,换 ...

  7. CS144 计算机网络 Lab0:Networking Warmup

    前言 本科期间修读了<计算机网络>课程,但是课上布置的作业比较简单,只是分析了一下 Wireshark 抓包的结构,没有动手实现过协议.所以最近在哔哩大学在线学习了斯坦福大学的 CS144 ...

  8. 计算机网络(7)-----TCP协议概述

    传输控制协议(Transmission Control Protocol) 概念 一种面向连接的.可靠的.基于字节流的传输层通信协议,由IETF的RFC 793定义.在简化的计算机网络OSI模型中,它 ...

  9. Day 6-1计算机网络基础&TCP/IP

    按照功能不同,人们将互联网协议分为osi七层或tcp/ip五层或tcp/ip四层(我们只需要掌握tcp/ip五层协议即可) 每层运行常见物理设备: TCP/IP协议: Transmission Con ...

  10. 计算机网络知识—(TCP)

    计算机网络在IT行业的重要性 IT即互联网技术,从事的工作和网络有很大的关系,前端要负责和后台(服务器)进行交互,其必然得经过网络,所以懂点网络知识有很大的帮助. 网络模型数据处理过程 传输层协议的作 ...

随机推荐

  1. GoAccess - 可视化 Web 日志分析工具

    Centos安装: yum -y install goaccess 使用goaccess命令生成HTML文件 LANG="en_US.UTF-8" bash -c 'goacces ...

  2. 使用ansible批量推送公钥

    准备两个yml文件 send-pubkey.yml - hosts: all remote_user: root # 连接远程主机的用户,密码就是文件中设置好的 ansible_ssh_pass 的值 ...

  3. 深入理解css 笔记(完)

    一个网站,从看起来还可以,到看起来非常棒,差别在于细节.在实现了页面里 某个组件的布局并写完样式之后,不要急着继续,有意识地训练自己,以挑剔的眼光审视刚刚完成的代码.如果增加或者减少一点内边距是不是看 ...

  4. Java VSCode 基础教学

    VSCode 超全设置1.下载2.插件安装3.项目创建4.设置5.快捷键6.优化7.导出 Jar 包 VSCode 超全设置 VSCode(Visual Studio Code) 是一款 Micros ...

  5. UAC的详细讲解(转载)

    win32中也有对UAC的操作方法 网址:https://blog.csdn.net/zuishikonghuan/article/details/46965159?locationNum=7& ...

  6. pytorch之科学计算

    一.简介 torch作为深度学习的主流框架,其根本在于1.具有强大的GPU加速的张量计算功能.2.包含自动求导系统的深度神经网络.自动求导功能由torch.Autograd模块实现,而科学计算部分则直 ...

  7. jmeter设置中文

    jmeter.properties #language=enlanguage=zh_CN

  8. wxml2canvas爬坑之路

    效果图: 前提: 公司要求生成一分报告并转为图片并保存,之前用canvas画过,但这次是在不想用canvas一点点画了,再往上找了n久,爬了n多坑,终于搞出来了 插件: wxml2canvas 一:下 ...

  9. Feign调用报错The bean 'XXX.FeignClientSpecification', defined in null, could not be registered....的解决办法

    问题描述: 创建了两个远程调用类,一个是调用退款的,一个是调用折扣的 但是两个调用类是调用的同一个微服务 都叫@FeignClient(value = "xxx-shop") 如何 ...

  10. P7213 [JOISC2020] 最古の遺跡 3 乱写

    不想写题解了,把写在草稿纸上的东西整理了一下 感谢 crashed 大佬的题解与对本人问题的回答,没有他我就不会搞懂这道神仙计数题.