RDT 可靠通信和网络重传侠 TCP

111 阅读18分钟

rdt 原理 | ARQ

reliable data transport | Automatic Repeat Request 错误纠正协议

ARQ 做什么

image.png

  • 上层调用可靠数据传输的API,将数据交付给下方实体
  • 下方 udt 实体被rdt调用,将分组放到不可靠的信道上传输到接收方
  • 接收方通过信道接收
  • 接收方上层 rdt 调用deliver_data()将数据交付给上层

版本变化

rdt 版本假设改进
1.0下层信道完全可靠只考虑封装和解封装
2.0下层信道可能出错使用 CRC32 CheckSum 检测比特差错使用 ACK & NACK 判断分组接收情况
2.1ACK & NACK 报文可能出错当 ACK & NACK 报文出错,等同于 NACK
2.2优化 NACK,改为单 ACK,当 ACK 货不对版等同于 NACK
3.0下层信道可能出错和死锁新增超时重传能力
流水线协议/连续 ARQ通过滑动窗口,允许连续发送数据包。提升网络吞吐量。

发送 - 响应模型的 rdt 实现有明显缺陷,在同一个信道中,同一时间只有一个数据包,效率非常低下,带宽浪费非常夸张。

image.png

流水线协议 | 连续 ARQ

允许发送方在未得到对方确认的情况下一次发送多个分组

  • 增加序号范围,用多个bit表示分组的序号

  • 维护缓冲区

    • 发送方缓冲:未得到确认可能需要重传
    • 接收方缓存:发送顺序 ≠ 接收顺序,通过窗口进行排序

回退N步(GBN)和选择重传(SR)

发送窗口

操作系统通过四个变量维护窗口

  1. 窗口大小
  2. 窗口开始下标 (绝对指针)
  3. 窗口未发送可发送下标(seq4,绝对指针)
  4. 不可发送指针 (seq5,用开始指针加上窗口大小的一个相对指针)

注意,下面的发送窗口我写的是 seq1,seq2,并不代表一个个的 TCP 数据包,TCP 是字节流的,所以这里的 seq 代表的是一个字节范围,比如 1 - 1000 Byte。这一个 seq 内的字节会被打包成 TCP 数据包发送。

为了提高传输效率,TCP 会在滑动窗口以及MSS(TCP层最大包大小)的限制范围内,尽量的提高单个 TCP 包的体积

发送窗口
seq0seq1seq2seq3seq4seq5seq6
已ACK已发送未 ACK可发送不可发送不可发送
  • 发送窗口大小通常是由接收方的瓶颈限制的
  • 发送方尽量贪心的去扩大窗口提升网络吞吐
  • seq4 (这里只有一个,但是实际上可以由多个)可以连续发送
  • 当前 seq1 - seq3 维护的窗口可以用于重传
  • 当 seq1 ACK 后,窗口会向后滑动,如下
发送窗口
seq0seq1seq2seq3seq4seq5seq6
已ACK已ACK已发送未 ACK可发送不可发送
  • 发送缓冲区的大小

    • ==1 :停止等待协议(发送 - 响应模型)
    • 1 : 流水线协议,合理的值,不能很大,链路利用率不会超过100%

一个前面的分组收到了确认请求,后沿向前移动,后沿不能超过前沿

接收窗口

  • 接收缓冲区

    • 控制哪些分组可以被接受
    • 收到的分组序号落入接收窗口内才允许被接收
    • 否则丢弃
  • 接收窗口尺寸Wr = 1 ,只能顺序接收(GBN协议),累计确认

  • 接收窗口尺寸Wr >1 ,支持乱序接收 ,独立确认

GBN VS SR

GBN & SR 在缓冲区上有区别

接收缓冲区会控制发送缓冲区的大小

image.png

相同之处

  • 发送窗口 > 1
  • 一次能够发送多个未经确认的分组

不同之处

  • GBN 接收窗口的尺寸为 1

    • ACK 是累计确认,ACK3 表示 3 及其前面的都确认了
    • 接收端只能顺序接收
    • 发送端一旦一个没有发送成功,这个之后的都得重发(所以要 go back No.n)
  • SR 接收窗口尺寸 > 1

    • ACK 是单独确认, ACK3 就是表示 Seq2 被接受了
    • 接收端可以乱序接收
    • 发送端选择性重发

TCP 概述

提供可靠的字节流的服务,但是界限需要应用进程来维护

TCP报文结构

表格 还在加载中,请等待加载完成后再尝试复制

  1. 端到端的通信
  2. 可靠的保序的字节流:但没有报文边界 (粘包问题)
  3. 管道化(流水线): ARQ 流水线协议(滑动窗口)
  4. 流量控制:通过维护双方缓冲区,防止淹没对方
  5. 拥塞避免:通过发送的数据包丢失情况嗅探网络的拥塞情况
  6. 全双工数据:同一数据连接中的数据流双向流动
  7. 面向连接:需要通过握手交换控制报文

image.png

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 的时候触发重传

image.png

但是快速重传没有说明是重传 1 个,还是重传全部

  1. 重传一个,收到 ACK2 就重传 seq2,但是如果 2 后面还有丢失的,就需要重新触发触发3次 ACK3 才能重传 Seq3
  2. 重传所有,如果 seq2 后有接受到的内容,也会被重传,属于是五十步笑百步

SACK

Selective Acknowledgment 选择性重传 (捎带)

一句话,双方通过 SACK 头部字段同步已经接收到的字段然后发送方只重传未被收到的

D-SACK

duplicate select acknowledgment (捎带)

一句话,接收方通知发送方自己收到了什么重复的seq

  1. 让发送方感知此次重传是由网络延迟产生的还是丢包产生的
  2. 在 ACK 丢失的时候可以通知发送方已经获取到对应内容的 ACK ( 但是通常不会出现这种情况,ACK都连续丢失了,这个通知也发不过去)

超时重传

以时间驱动的重传机制

两种情况触发超时重传

  1. 数据包丢失
  2. ACK 丢失

TCP只维护一个全局的重传定时器和一个重传队列,所有被在等待ACK的内容都在队列中。当一个定时器超时的时候,TCP 取头部 Seq 重传,然后更新定时器

假设当前有 ABCD,四个数据包,他们的超时时间分别为 TABCD

  1. 初始发送 A,定时器设置为 TA
  2. 在定时器结束时接收到 AACK,则将定时器更新为 TB
  3. 如果TB超时,则判断TB是否需要重传,如果需要则重传TB
  4. 然后更新定时器为 min(2TB,TC)

超时时间量化

如何量化超时时间。首先超时时间一定是比 RTT 要长的,那么就有两个问题

如何动态适应 RTT ?

连续的数据包的 RTT 时间变化肯定是平滑的,这是由网络的连续性决定的

将离散采样的 RTT 时间加权平均,拟合出一个平滑的 RTT 变化值

比 RTT 长多少 ?

通过计算出的 RTT ,再去算出 RTO

RTO (retrasimisson timeout) 的计算公式是由互联官网工程小组做实验不断试出来的。

在 RFC 6289 中给出了建议的 RTO 计算公式

简单的,可以理解为以下公式

TimeoutInterval = EstimatedRTT + 4 * DevRTT('safety margin'

处理连续超时问题

如果连续出现超时丢包问题,下一次的 RTO 会是上一次的两倍,因为在网络环境较差的环境下频繁的重传也是对网络的负担

流量控制(滑动窗口)

注意缓冲区和滑动窗口的区别

image.png

  • 缓冲区由操作系统控制,是在传输层和应用层之间的
  • 滑动窗口由 TCP 控制,目的是实现 连续 ARQ & rdt 以及防止接收方的缓冲区溢出
  • 接收方在 ACK 中捎带自己通过缓冲区大小判断的合适的窗口大小给发送方,甚至0
  • 不能够同时减小缓冲区和发送窗口,因为网络是有延迟的,这样会导致缓冲区

糊涂窗口问题

当接收方缓冲区告警,可能通知一个很小的缓冲区,而发送方就发送了这个缓冲区大小的包。

假设缓冲区为 21 byte ,TCP 头部占去 20 byte ,那么浪费率接近 100% 了,这不是开玩笑嘛,所以 TCP 采用了一些策略防止这种情况发生:

  1. 接收方不通告小窗口,必须满足以下条件才通告窗口,否则通告 0
canCall = win > Math.min( MSS , cacheMemorySize / 2 ) 

2. 发送方避免发送小数据 (Nagle算法)必须满足以下条件才发送数据包

condition1 = ( win > MSS ) && ( dataSize > MSS ) 
condition2 = 收到之前发送的数据包的回包 // 因为有这个条件,所以如果只开Nagel不开接收方策略是不行的
canSend = condition1 || condition2

拥塞控制

拥塞控制治理的是网络带宽的不稳定问题

拥塞避免治理的是接收方缓冲区溢出的问题

网络辅助的拥塞控制

路由器 / 交换机 提供给端系统以反馈信息, ATM 网络使用

  1. 单个bit置位: RM(资源管理)信元

    1. 有发送端发送 RM,在数据信元中间隔插入

    2. RM 中的 bit 被交换机设置(网络辅助):

      • NT bit :no increase in rate
      • CI bit : congestion indication 拥塞指示
      • ER bit :最小带宽
    3. RM信元被接收端返回,接收端不做任何改变

  2. 显示提供发送端可以采用的速率 ABR :available bit rate

    1. 轻载:发送方可使用可用带宽
    2. 拥塞: 发送方限制速度到一个最小保障速率上

端到端的拥塞控制

端系统根据延迟和丢失事件推断是否有拥塞 ,TCP网络使用 AIMD

路由器的负担比较低

符合网络核心简单的TCP/IP构建原则

真实发包个数 = Math.min( 滑动窗口限制 , 拥塞窗口限制 ) 
  1. 慢启动

维护一个拥塞窗口,一个门限

刚开始,拥塞窗口小于门限,指数级增加拥塞窗口

  1. 拥塞避免

当拥塞窗口大于等于门限的时候,进入拥塞避免阶段

在这个阶段,每收到一个ACK,拥塞窗口增加一个 MSS

  1. 收到3个冗余ACK (快速重传)

  • 轻微拥塞

触发快速重传的,时候,判断当前网络是轻微拥堵

  • 拥塞窗口重置为当前拥塞窗口的 1/2
  • 降低门限为当前滑动窗口 (相当于跳过慢启动)
  1. 快速恢复

  • 同时进入快速恢复阶段

    • 门限 = 当前门限 + 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)的值,快速恢复过程就此结束。这样可以使得拥塞窗口在一个相对合理的大小上,既能保证数据传输的效率,又能避免再次出现网络拥塞。

  1. TCP 分组超时 (超时重传)

  • 拥塞
  • 检验不通过:误杀

触发超时的时候

  • 降低门限为当前门限的 1/2
  • 拥塞窗口重置为 1

重新进入慢启动阶段

拥塞控制和流量控制是同时进行的,一体化处理,他们两个控制的最小的发送速率是实际发送速率,同时满足拥塞控制和流量控制的要求

从数学上分析,TCP拥塞控制让TCP的带宽分配变得公平

连接管理

最大连接数

讨论最大的连接数,理论上我们需要从服务端和客户端两方面考虑

  1. 客户端:

    1. IP 的数量最多为 2 ^ 32 (IPv4)
    2. 端口是一个 16 位的无符号整数,理论上是 2 ^ 16 个
    3. 所以客户端能提供 2 ^ 48 个二元组
  2. 服务端(我们这里讨论一个服务端的最大连接数)需要讨论承受能力

    1. 每一个 TCP 连接在操作系统中是一个文件,操作系统能够同时打开的文件数量是有限的
    2. 打开每一个 TCP 连接需要在内存中维护缓冲区和保存相关数据,所以数量还受制于内存数量

三次握手

什么是连接 ?RFC793 的定义中,用于保证可靠性,实现流量控制而维护一些状态信息组合,包括socket,序列号和窗口大小这些统称为连接

建立连接,其实就是一个同步双方的简单状态机的问题 👇

流程

image.png

  • SYNbit 标志位,标识这个握手是试图建立起状态同步的请求(连接)
  • ACKbit 标志位,标识这个请求是对以前的确认
  • SequenceNumber 服务端和客户端各自随机选择的一个 number,通过这个 number 判断返回的 ACK 归属于哪一个请求
  • ACKnum = 对方的 SequenceNumber + 1,标识哪一个请求的 ACK

防止历史连接

假设 TCP 只有两次握手

客户端

  • 客户端首先发起连接请求,但是由于网络超时或者客户端宕机重启,这个请求连接被抛弃, 重新发起一个请求连接
  • 第二个请求发出之后,客户端收到了第一个请求的 ACK,由于没有标识,客户端也会误以为连接(第二个)建立成功。进而造成连接混乱

服务端

  • 第一个请求连接已经被服务端接收,建立起一个完整的连接。在ACK到达客户端之前,客户端就重发了,废弃了第一个握手请求。到此由于服务端太容易建立起连接,造成了一个无效的连接建立
  • 第二个请求发出后,服务端收到,又建立起一个重复连接

image.png

在三次握手的前提下,如果服务端尝试建立起重复的请求,就会因为 ACKnumber 于当前期待的 SeqenceNumber + 1 不匹配而被拒绝,发送断开当前连接的请求,服务端就不用维护半连接了。

  • 也因此,这里的序号必须是随机的,而不是按顺序的,否则重传的序号可能和第一次穿的一样,也会错误的建立起连接
  • 这个随机的序号算法是
ISN = M(每四微妙 + 1 )  + F (四元组)

image.png

  • 错误接收

image.png

但是序号随机并不是完全避免了错误接收的可能,只是概率比较低,网络不能解决所有问题

同步双方初始序列号

序列号 seqNum 是可靠传输的关键属性,可以说序列号是滑动窗口的核心属性

  1. 通过序列号去重
  2. 通过序列号按序接收
  3. 标识那些是已经被 ACK 的

三次握手也可以看作是双方相互同步序列号的必然要求,只不过原本的4次挥手捎带优化了一下变成了3次

SYN攻击 DDOS

对于服务端而言

  1. 在收到第一个 SYN 请求的时候,将对应的半连接对象推入半连接队列
  2. 收到 SYNACK 的时候,将对应半连接对象推入全连接队列中

如果很多伪造的 ip 去把半连接队列打满,由于服务器无法找到对于 ip,也就无法发送 SYNACK,进而无法消费半连接队列,一直重传直到超时。造成资源浪费,服务器宕机

image.png

解决方案

  1. 增大TCP半连接缓队列
  2. 当半连接队列满,新到来的连接通过算法从四元组算出一个 cookies 保存,发送给客户端(ACKSYN)
  3. 接收到 ACKSYN 的时候,判断 cookies 合法性,直接进入全连接队列

四次挥手

  • 客户端和服务器分别拆除

    • 对称释放
      • 客户端发送拆除请求

        • 服务器同意
        • 客户端到服务器的传输断开
      • 服务器向客户端发送拆除请求

        • 客户端同意
        • 服务端向客户端的传输断开

这里的四次挥手是双方同步连接断开意图的过程,按道理说四次挥手可以通过捎带优化成三次,就如同三次握手那样。但是一方断开连接另一方可能尚有数据等待发送,所以他们不是一个一定紧随的状态。

对于握手,则一方统一建立后另一方必然要紧随着同意/拒绝,所以可以晒带优化

两军问题

这是两次通信的连接拆除:不可避免的会有一方释放了但是另一方未释放的情况,因为这是不可靠的

截断攻击

连接拆除容易被伪造,被攻击后连接确实会拆除,被攻击方仅可感知到攻击,但是连接不可避免的被拆除:

解决方案: 连接拆除后启动一个定时器,定时器结束之前没有请求到来就是真正释放了

笔者才疏学浅,请各位读者多多指教。 部分插图来自网络,侵删。 特别感谢: @小林coding