rdt 原理 | ARQ
reliable data transport | Automatic Repeat Request 错误纠正协议
ARQ 做什么
- 上层调用可靠数据传输的API,将数据交付给下方实体
- 下方 udt 实体被rdt调用,将分组放到不可靠的信道上传输到接收方
- 接收方通过信道接收
- 接收方上层 rdt 调用deliver_data()将数据交付给上层
版本变化
| rdt 版本 | 假设 | 改进 |
|---|---|---|
| 1.0 | 下层信道完全可靠 | 只考虑封装和解封装 |
| 2.0 | 下层信道可能出错 | 使用 CRC32 CheckSum 检测比特差错使用 ACK & NACK 判断分组接收情况 |
| 2.1 | ACK & NACK 报文可能出错 | 当 ACK & NACK 报文出错,等同于 NACK |
| 2.2 | 优化 NACK,改为单 ACK,当 ACK 货不对版等同于 NACK | |
| 3.0 | 下层信道可能出错和死锁 | 新增超时重传能力 |
| 流水线协议/连续 ARQ | 通过滑动窗口,允许连续发送数据包。提升网络吞吐量。 |
发送 - 响应模型的 rdt 实现有明显缺陷,在同一个信道中,同一时间只有一个数据包,效率非常低下,带宽浪费非常夸张。
流水线协议 | 连续 ARQ
允许发送方在未得到对方确认的情况下一次发送多个分组
-
增加序号范围,用多个bit表示分组的序号
-
维护缓冲区
- 发送方缓冲:未得到确认可能需要重传
- 接收方缓存:发送顺序 ≠ 接收顺序,通过窗口进行排序
回退N步(GBN)和选择重传(SR)
发送窗口
操作系统通过四个变量维护窗口
- 窗口大小
- 窗口开始下标 (绝对指针)
- 窗口未发送可发送下标(seq4,绝对指针)
- 不可发送指针 (seq5,用开始指针加上窗口大小的一个相对指针)
注意,下面的发送窗口我写的是 seq1,seq2,并不代表一个个的 TCP 数据包,TCP 是字节流的,所以这里的 seq 代表的是一个字节范围,比如 1 - 1000 Byte。这一个 seq 内的字节会被打包成 TCP 数据包发送。
为了提高传输效率,TCP 会在滑动窗口以及MSS(TCP层最大包大小)的限制范围内,尽量的提高单个 TCP 包的体积
| 发送窗口 | ||||||
|---|---|---|---|---|---|---|
| seq0 | seq1 | seq2 | seq3 | seq4 | seq5 | seq6 |
| 已ACK | 已发送未 ACK | 可发送 | 不可发送 | 不可发送 |
- 发送窗口大小通常是由接收方的瓶颈限制的
- 发送方尽量贪心的去扩大窗口提升网络吞吐
- seq4 (这里只有一个,但是实际上可以由多个)可以连续发送
- 当前 seq1 - seq3 维护的窗口可以用于重传
- 当 seq1 ACK 后,窗口会向后滑动,如下
| 发送窗口 | ||||||
|---|---|---|---|---|---|---|
| seq0 | seq1 | seq2 | seq3 | seq4 | seq5 | seq6 |
| 已ACK | 已ACK | 已发送未 ACK | 可发送 | 不可发送 |
-
发送缓冲区的大小
- ==1 :停止等待协议(发送 - 响应模型)
-
1 : 流水线协议,合理的值,不能很大,链路利用率不会超过100%
一个前面的分组收到了确认请求,后沿向前移动,后沿不能超过前沿
接收窗口
-
接收缓冲区
- 控制哪些分组可以被接受
- 收到的分组序号落入接收窗口内才允许被接收
- 否则丢弃
-
接收窗口尺寸Wr = 1 ,只能顺序接收(GBN协议),累计确认
-
接收窗口尺寸Wr >1 ,支持乱序接收 ,独立确认
GBN VS SR
GBN & SR 在缓冲区上有区别
接收缓冲区会控制发送缓冲区的大小
相同之处
- 发送窗口 > 1
- 一次能够发送多个未经确认的分组
不同之处
-
GBN 接收窗口的尺寸为 1
- ACK 是累计确认,ACK3 表示 3 及其前面的都确认了
- 接收端只能顺序接收
- 发送端一旦一个没有发送成功,这个之后的都得重发(所以要 go back No.n)
-
SR 接收窗口尺寸 > 1
- ACK 是单独确认, ACK3 就是表示 Seq2 被接受了
- 接收端可以乱序接收
- 发送端选择性重发
TCP 概述
提供可靠的字节流的服务,但是界限需要应用进程来维护
TCP报文结构
表格 还在加载中,请等待加载完成后再尝试复制
- 端到端的通信
- 可靠的保序的字节流:但没有报文边界 (粘包问题)
- 管道化(流水线): ARQ 流水线协议(滑动窗口)
- 流量控制:通过维护双方缓冲区,防止淹没对方
- 拥塞避免:通过发送的数据包丢失情况嗅探网络的拥塞情况
- 全双工数据:同一数据连接中的数据流双向流动
- 面向连接:需要通过握手交换控制报文
rdt 实现 (滑动窗口)
TCP在 IP 不可靠服务上建立了 rdt
其实现是 GNB 和 SR 协议的混合体
- 累计确认:像 GBN
- 单个重传定时器:像 GBN
- 接收窗口 > 1 : 像 SR
- 超时触发重传:只传最早那个未确认的报文段 : 像 SR
- 是否可以接收乱序:没有规范,可以缓存也可抛弃
简化的TCP发送方
-
从应用层接收数据:
-
用nextsep创建报文段
-
序号nextsep为报文段首字节的字节流编号
-
没有定时器就启动定时器
-
与最早发送的报文关联
-
过期间隔就是TimeOutInterval
-
-
-
收到确认
-
如果是对未确认的报文段的确认:
-
更新已被确认报文序号(窗口后沿)
-
如果当前还有未被确认的报文段:重新启动定时器
-
-
简化的TCP接收方
按序收到
-
收到顺序的报文段y1
- 启动500ms辅助计时器
-
500ms内收到按序的y2
- 马上发送y2的ACK
-
500ms内未收到按序的报文段
- 发送最后到达的有序报文段的下一个期望的ACK(y1的ACK)
乱序收到
-
马上发送期望有序的那个ACK
-
比如来了1后又来了3
- 马上发送 ack =2(期望)
-
填补gap的情况:
- 发送连起来的报文段的最后一部分的期望
其实都是对最后顺序到来的报文段给ACK
快速重传
以数据驱动的重传机制
一句话就是收到连续的 3 个 ACK 的时候触发重传
但是快速重传没有说明是重传 1 个,还是重传全部
- 重传一个,收到 ACK2 就重传 seq2,但是如果 2 后面还有丢失的,就需要重新触发触发3次 ACK3 才能重传 Seq3
- 重传所有,如果 seq2 后有接受到的内容,也会被重传,属于是五十步笑百步
SACK
Selective Acknowledgment 选择性重传 (捎带)
一句话,双方通过 SACK 头部字段同步已经接收到的字段然后发送方只重传未被收到的
D-SACK
duplicate select acknowledgment (捎带)
一句话,接收方通知发送方自己收到了什么重复的seq
- 让发送方感知此次重传是由网络延迟产生的还是丢包产生的
- 在 ACK 丢失的时候可以通知发送方已经获取到对应内容的 ACK ( 但是通常不会出现这种情况,ACK都连续丢失了,这个通知也发不过去)
超时重传
以时间驱动的重传机制
两种情况触发超时重传
- 数据包丢失
- ACK 丢失
TCP只维护一个全局的重传定时器和一个重传队列,所有被在等待ACK的内容都在队列中。当一个定时器超时的时候,TCP 取头部 Seq 重传,然后更新定时器
假设当前有 ABCD,四个数据包,他们的超时时间分别为 TABCD
- 初始发送 A,定时器设置为 TA
- 在定时器结束时接收到 AACK,则将定时器更新为 TB
- 如果TB超时,则判断TB是否需要重传,如果需要则重传TB
- 然后更新定时器为 min(2TB,TC)
超时时间量化
如何量化超时时间。首先超时时间一定是比 RTT 要长的,那么就有两个问题
如何动态适应 RTT ?
连续的数据包的 RTT 时间变化肯定是平滑的,这是由网络的连续性决定的
将离散采样的 RTT 时间加权平均,拟合出一个平滑的 RTT 变化值
比 RTT 长多少 ?
通过计算出的 RTT ,再去算出 RTO
RTO (retrasimisson timeout) 的计算公式是由互联官网工程小组做实验不断试出来的。
在 RFC 6289 中给出了建议的 RTO 计算公式
简单的,可以理解为以下公式
TimeoutInterval = EstimatedRTT + 4 * DevRTT('safety margin')·
处理连续超时问题
如果连续出现超时丢包问题,下一次的 RTO 会是上一次的两倍,因为在网络环境较差的环境下频繁的重传也是对网络的负担
流量控制(滑动窗口)
注意缓冲区和滑动窗口的区别
- 缓冲区由操作系统控制,是在传输层和应用层之间的
- 滑动窗口由 TCP 控制,目的是实现 连续 ARQ & rdt 以及防止接收方的缓冲区溢出
- 接收方在 ACK 中捎带自己通过缓冲区大小判断的合适的窗口大小给发送方,甚至0
- 不能够同时减小缓冲区和发送窗口,因为网络是有延迟的,这样会导致缓冲区
糊涂窗口问题
当接收方缓冲区告警,可能通知一个很小的缓冲区,而发送方就发送了这个缓冲区大小的包。
假设缓冲区为 21 byte ,TCP 头部占去 20 byte ,那么浪费率接近 100% 了,这不是开玩笑嘛,所以 TCP 采用了一些策略防止这种情况发生:
- 接收方不通告小窗口,必须满足以下条件才通告窗口,否则通告 0
canCall = win > Math.min( MSS , cacheMemorySize / 2 )
2. 发送方避免发送小数据 (Nagle算法)必须满足以下条件才发送数据包
condition1 = ( win > MSS ) && ( dataSize > MSS )
condition2 = 收到之前发送的数据包的回包 // 因为有这个条件,所以如果只开Nagel不开接收方策略是不行的
canSend = condition1 || condition2
拥塞控制
拥塞控制治理的是网络带宽的不稳定问题
拥塞避免治理的是接收方缓冲区溢出的问题
网络辅助的拥塞控制
路由器 / 交换机 提供给端系统以反馈信息, ATM 网络使用
-
单个bit置位: RM(资源管理)信元
-
有发送端发送 RM,在数据信元中间隔插入
-
RM 中的 bit 被交换机设置(网络辅助):
- NT bit :no increase in rate
- CI bit : congestion indication 拥塞指示
- ER bit :最小带宽
-
RM信元被接收端返回,接收端不做任何改变
-
-
显示提供发送端可以采用的速率 ABR :available bit rate
- 轻载:发送方可使用可用带宽
- 拥塞: 发送方限制速度到一个最小保障速率上
端到端的拥塞控制
端系统根据延迟和丢失事件推断是否有拥塞 ,TCP网络使用 AIMD
路由器的负担比较低
符合网络核心简单的TCP/IP构建原则
真实发包个数 = Math.min( 滑动窗口限制 , 拥塞窗口限制 )
-
慢启动
维护一个拥塞窗口,一个门限
刚开始,拥塞窗口小于门限,指数级增加拥塞窗口
-
拥塞避免
当拥塞窗口大于等于门限的时候,进入拥塞避免阶段
在这个阶段,每收到一个ACK,拥塞窗口增加一个 MSS
-
收到3个冗余ACK (快速重传)
- 轻微拥塞
触发快速重传的,时候,判断当前网络是轻微拥堵
- 拥塞窗口重置为当前拥塞窗口的 1/2
- 降低门限为当前滑动窗口 (相当于跳过慢启动)
-
快速恢复
-
同时进入快速恢复阶段
- 门限 = 当前门限 + 3
- 拥塞窗口 = 门限
- 快速恢复是为了尽快将丢失的数据包发给目标,所以拥塞窗口反而增大
- 接收到新的 ACK 则:
- 拥塞窗口 = 拥塞窗口 / 2
- 门限 = 拥塞窗口
cwnd = ssthresh + 3:接着将拥塞窗口(cwnd)设置为慢开始阈值(ssthresh)加3。这里的加3是因为在快速重传时已经确认接收到了3个重复的数据包,这表明网络中可能存在一定程度的拥塞,但又不至于非常严重,所以通过这种方式对拥塞窗口进行一个相对合理的调整,既减小了拥塞窗口以避免进一步的网络拥塞,又不至于让拥塞窗口变得过小而影响数据传输效率。
继续重传丢失的数据包,如果再收到重复的 ACK,那么 cwnd 增加 1:在调整拥塞窗口后,继续重传之前丢失的数据包。如果在这个过程中又收到了重复的确认应答(ACK),就将拥塞窗口(cwnd)增加1。这是因为每个收到的重复的ACK包,都意味着有一个之前发送出去的数据包已经成功离开了网络,网络的拥塞情况可能有所缓解,所以可以通过适当增加拥塞窗口来提高数据传输效率,但每次只增加1,以谨慎地避免再次引发网络拥塞。
如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,恢复过程结束:当收到针对新数据的确认应答(ACK)后,表明丢失的数据包已经成功传输并且网络状态已经恢复正常,此时将拥塞窗口(cwnd)设置为最初计算的慢开始阈值(ssthresh)的值,快速恢复过程就此结束。这样可以使得拥塞窗口在一个相对合理的大小上,既能保证数据传输的效率,又能避免再次出现网络拥塞。
-
TCP 分组超时 (超时重传)
- 拥塞
- 检验不通过:误杀
触发超时的时候
- 降低门限为当前门限的 1/2
- 拥塞窗口重置为 1
重新进入慢启动阶段
拥塞控制和流量控制是同时进行的,一体化处理,他们两个控制的最小的发送速率是实际发送速率,同时满足拥塞控制和流量控制的要求
从数学上分析,TCP拥塞控制让TCP的带宽分配变得公平
连接管理
最大连接数
讨论最大的连接数,理论上我们需要从服务端和客户端两方面考虑
-
客户端:
- IP 的数量最多为 2 ^ 32 (IPv4)
- 端口是一个 16 位的无符号整数,理论上是 2 ^ 16 个
- 所以客户端能提供 2 ^ 48 个二元组
-
服务端(我们这里讨论一个服务端的最大连接数)需要讨论承受能力
- 每一个 TCP 连接在操作系统中是一个文件,操作系统能够同时打开的文件数量是有限的
- 打开每一个 TCP 连接需要在内存中维护缓冲区和保存相关数据,所以数量还受制于内存数量
三次握手
什么是连接 ?RFC793 的定义中,用于保证可靠性,实现流量控制而维护一些状态信息组合,包括socket,序列号和窗口大小这些统称为连接
建立连接,其实就是一个同步双方的简单状态机的问题 👇
流程
- SYNbit 标志位,标识这个握手是试图建立起状态同步的请求(连接)
- ACKbit 标志位,标识这个请求是对以前的确认
- SequenceNumber 服务端和客户端各自随机选择的一个 number,通过这个 number 判断返回的 ACK 归属于哪一个请求
- ACKnum = 对方的 SequenceNumber + 1,标识哪一个请求的 ACK
防止历史连接
假设 TCP 只有两次握手
客户端
- 客户端首先发起连接请求,但是由于网络超时或者客户端宕机重启,这个请求连接被抛弃, 重新发起一个请求连接
- 第二个请求发出之后,客户端收到了第一个请求的 ACK,由于没有标识,客户端也会误以为连接(第二个)建立成功。进而造成连接混乱
服务端
- 第一个请求连接已经被服务端接收,建立起一个完整的连接。在ACK到达客户端之前,客户端就重发了,废弃了第一个握手请求。到此由于服务端太容易建立起连接,造成了一个无效的连接建立
- 第二个请求发出后,服务端收到,又建立起一个重复连接
在三次握手的前提下,如果服务端尝试建立起重复的请求,就会因为 ACKnumber 于当前期待的 SeqenceNumber + 1 不匹配而被拒绝,发送断开当前连接的请求,服务端就不用维护半连接了。
- 也因此,这里的序号必须是随机的,而不是按顺序的,否则重传的序号可能和第一次穿的一样,也会错误的建立起连接
- 这个随机的序号算法是
ISN = M(每四微妙 + 1 ) + F (四元组)
- 错误接收
但是序号随机并不是完全避免了错误接收的可能,只是概率比较低,网络不能解决所有问题
同步双方初始序列号
序列号 seqNum 是可靠传输的关键属性,可以说序列号是滑动窗口的核心属性
- 通过序列号去重
- 通过序列号按序接收
- 标识那些是已经被 ACK 的
三次握手也可以看作是双方相互同步序列号的必然要求,只不过原本的4次挥手捎带优化了一下变成了3次
SYN攻击 DDOS
对于服务端而言
- 在收到第一个 SYN 请求的时候,将对应的半连接对象推入半连接队列
- 收到 SYNACK 的时候,将对应半连接对象推入全连接队列中
如果很多伪造的 ip 去把半连接队列打满,由于服务器无法找到对于 ip,也就无法发送 SYNACK,进而无法消费半连接队列,一直重传直到超时。造成资源浪费,服务器宕机
解决方案
- 增大TCP半连接缓队列
- 当半连接队列满,新到来的连接通过算法从四元组算出一个 cookies 保存,发送给客户端(ACKSYN)
- 接收到 ACKSYN 的时候,判断 cookies 合法性,直接进入全连接队列
四次挥手
-
客户端和服务器分别拆除
- 对称释放
-
-
客户端发送拆除请求
- 服务器同意
- 客户端到服务器的传输断开
-
-
-
服务器向客户端发送拆除请求
- 客户端同意
- 服务端向客户端的传输断开
-
这里的四次挥手是双方同步连接断开意图的过程,按道理说四次挥手可以通过捎带优化成三次,就如同三次握手那样。但是一方断开连接另一方可能尚有数据等待发送,所以他们不是一个一定紧随的状态。
对于握手,则一方统一建立后另一方必然要紧随着同意/拒绝,所以可以晒带优化
两军问题
这是两次通信的连接拆除:不可避免的会有一方释放了但是另一方未释放的情况,因为这是不可靠的
截断攻击
连接拆除容易被伪造,被攻击后连接确实会拆除,被攻击方仅可感知到攻击,但是连接不可避免的被拆除:
解决方案: 连接拆除后启动一个定时器,定时器结束之前没有请求到来就是真正释放了
笔者才疏学浅,请各位读者多多指教。 部分插图来自网络,侵删。 特别感谢: @小林coding