简单聊聊TCP的可靠性

前言

首发自 blog.cc1234.cc/html

传输控制协议(缩写:TCP)是一种面向链接的、可靠的、基于字节流传输层通讯协议,由IETFRFC 793定义。算法

TCP在不可靠的IP协议之上实现了可靠性, 从而使得咱们没必要再去关注网络传输中的种种复杂性,所谓的可靠就是让咱们去信任它便可缓存

信任归信任,可咱们仍是的得去了解它,知道它为什么值得信任,信任主要体如今哪些方面,换句话说就是安全

  • TCP的可靠性是什么
  • TCP如何实现的可靠性

上面的问题就是本文讨论的核心点markdown

TCP的可靠性实则是一个很大的话题,不少细节都值得深究,因为本人水平有限,文中不少描述都没有深刻甚至可能有错误,读者如有不一样观点,尽可提出网络

什么是可靠性

其实在RFC 7931.5 Operation专门对Reliability(可靠性)作了说明异步

总结下来以下学习

确保一个进程从其接受缓存中读出的数据流是无损坏,无间隔,非冗余和按序的数据流;即字节流与链接的另外一方端系统发送出的字节流是彻底相同的spa

须要解决的问题

前面说到的可靠性,提到了无损坏,无间隔,非冗余和按序等几个关键词, 而在网络中要实现这些指标,咱们都有对应的问题须要去解决计算机网络

其中最典型的几个问题以下

  • 干扰

    网络的干扰多是由于硬件故障致使数据包受到破坏, 也有多是网络波动致使数据包的某些bit位产生了变化

    题外话:这里不的干扰并不包含恶意攻击,恶意攻击是属于传输安全的范畴了,好比咱们熟知的SSL/TLS就是一个成熟的网络传输安全问题的解决方案

    以下图,发送的111 因为干扰变成了101

  • 乱序

    发送方连续前后发送两个数据包, 后发送的数据包可能先到达接收方,若是接收方按接收顺序处理数据包,这就会致使接收的数据包与发送的数据包不一致。

    形成这样的缘由是由于每个数据包都会根据当时的网络状况选择不一样的路由进行传输, 就像从开车从上海到北京有不少路线可选,不必定你先出发就能先到(我没去过北京,请不要杠我......)

    以下图,发送方顺序发送了A -> B -> C三个数据包, 然而接收方多是以A -> C -> B这样的顺序接收的报文,很明显 B 和 C两个个报文的顺序不符合指望,产生了乱序

  • 丢包

    网络丢包是一个很常见的现象,形成的缘由也多种多样,比较常见的有

    1. 接收方因为缓存溢出,致使没法再处理到来的数据包了,直接丢弃从而形成丢包

    2. 网络拥塞致使数据包丢包

    3. 数据包被检测到损坏了,被接收方丢弃形成了丢包

    4. ......

    下图展现了这种状况,发送的数据CBA因为A产生了丢包,致使接收方只收到了CB

  • 冗余

    发送方可能由于某些缘由重复发送了同一个数据包,接收方要有能力处理这种冗余数据包

    好比发送方发送的一个数据包由于网络拥塞迟迟没有被接收方收到, 发送方认为产生了丢包就又重发了一次,结果最终接收方收到了两个一样的数据包,产生了数据冗余

在继续往下看以前,能够先思考一下: 你会如何去解决这些问题?

0x01 解决干扰

为了可以检测到数据包在传输过程当中是否发生了差错,TCP引入了checksum

checksum的具体细节能够查阅RFC1071

下图是TCP的报文结构,蓝色部分就是checksum

checksum是一个16bit长的字段,发送方在计算checksum时会先将报文中的Checksum置零,而后基于整个报文(头部 + 数据部分)计算出checksum

实际上还会加上96bit的伪头部,能够参考RFC 793 Header Format 一节

接收方在收到报文后也会计算checksum

  • 若是计算结果符合指望值,说明数据包没有收到干扰/损坏
  • 若是不符合指望,通常会直接丢弃该数据包

TCP的校验和也有必定的限制,并不必定100%能检测到数据包产生的错误

这就和咱们日常作API开发时的签名同样

0x02 解决乱序和冗余

请先回顾如下前面谈到乱序时的一个示例图

乱序有多个解决方案,好比发送一个数据后,我确认该发送的数据被接收方接收了我再发下一个,这样确定是有序的, 可是这样的方案对网络利用率实在是过低了

另外一个很朴实的解决方案就是为每一个报文标上序号, 这样接收方在收到报文后只须要按序号对报文排序就能够获得有序的报文了,过程以下图所示

实际上TCP协议采用的就是为报文加上序号这样的方法

TCP的报文结构上维护着一个Sequence Number(下面简称seq),以下图的红色区域所示

TCP的发送端和接收端各自独立维护一个seq, seq的初始值是在建立链接时初始化的(值是随机的)

有一个控制位SYN就是专门用来在发送方和接收方同步seq的 (这里的同步指的是让对方知道本身的seq初始值是多少)

详细内容参考RFC 793 的 3.3. Sequence Numbers

创建链接后的每个报文都会携带seq

以下图所示,假设初始seq=1,发送的第一个报文A的长度为12, 那么发送第二个报文Bseq=1+12=13

接收方接收到多个报文后,能够基于seq多数据包进行升序排序,而且经过检查seq的值,能够判断接收的数据是否有间隔,以及数据是不是有序的。

除此以外,seq使得TCP有能力处理重复数据包的问题,由于接收方能够根据seq判断出该数据包是否是已经被接收了,这样顺带还解决了数据冗余的问题

0x03 解决丢包

丢包的缘由可能各式各样,咱们从一个简单的场景开始开始分析

假设没有丢包的状况下,如何让发送方知道接收方已成功接收到数据包了呢?

这就像人与人之间交谈,你如何判断对方听见了呢?

现实生活中咱们靠的是对方的响应来作出判断

TCP也采用相似的机制,咱们通常称之为ACK(Acknowledgment):接收方在收到数据包之后会对发送方响应一个特定的数据包.

仍是继续看一下TCP的结构图,注意绿色区域的Acknowledgment Number(后面简称ack

注意大写ACK和小写ack是有区别的

大写ACK通常指的是报文的类型

小写ack指的就是这个32bit长的号码

ackseq都是32bit长,前面咱们说到TCP创建链接后发送的报文都会带上seq, 接收方在收到报文后,会响应一个类型为ACK的报文

报文的acknowledgment number的值是接收方下次指望收到的报文的seq

实际上ack的值会受不少状况影响, 好比TCP的累积确认机制, 选择重传机制等等都会影响响应的ack值,细节能够参考RFC 793

有了ACK后, 发送方就能够知道报文有没有被正确接收了

请看下图,这是一个简单的交互, 发送方发送数据,接收方确认数据后作出响应

前面咱们都是基于没有丢包的状况进行分析的,ACK并无解决丢包的问题,以下图所示,发送的数据若是丢包了就没有ACKACK若是丢包了就不知道数据是否被正确接收。

此时咱们引入超时重传, 结合ACK机制一块儿来应对丢包的问题

  • 发送方发送一个未被确认的数据包后就启动一个计时器
  • 若是在指定时间内没有收到ACK, 发送方能够重传该报文

超时重传 + ACK也有一个小问题,就是最开始提到的数据冗余问题

重传数据包可能致使接收方接收到多个重复的数据包, 若是你还没忘记的话,这个能够经过前面一节说到的seq去解决

实际上TCP的拥塞控制也是处理丢包的有效机制之一,有兴趣的同窗能够去了解

0x04 基本可靠

在回顾一下咱们最开始对可靠的要求

确保一个进程从其接受缓存中读出的数据流是无损坏,无间隔,非冗余和按序的数据流......

前面咱们经过checksum, seq,ack,超时重传等机制,算是达到了一个可靠性的基本要求, 为何说是基本可靠呢?

由于到目前为止咱们的场景还都相对简单,因此会忽略掉不少变量和细节问题。

好比咱们都没有提到很重要的滑动窗口拥塞控制算法等, 但实际上这些也是TCP在复杂的网络环境中实现可靠性不可获取的东西

0x05 番外篇

本节做为番外篇,能够认为是对前面内容的一些补充,补充主要也是针对一些细节的地方,以FAQ的方式进行表述

  1. 每一个seq都须要一个ack吗?

    固然不是

    发送方能够直接发送多个报文, 接收方在接收到报文后先不急着响应ack,由于后续报文可能立刻到达, 这就是ACK延迟确认

    延迟确承认以让咱们同时对多个接受的报文进行一次确认,这个又称之为累计确认

    这样接收方就没必要对每一个报文都进行确认,接收到多个报文后若是延迟时间内没有报文到来,就发送下一个指望接收报文的ack, 以下图所示

  2. 上图中,若是同时发送多个seq报文,若中间某一个丢包,ack如何响应呢?

    通常接收方会有一个接收缓冲,会缓存接收到的报文,这些报文按seq值排序, 若是中间缺乏某段报文,那么接收方就会响应这段报文的seq值

    以下图所示,发送方发送了seq=1,seq=2,seq=3的报文, seq=2丢包, 可是接收方缓存了1和3,

    因此知道这部分报文不连续,中间缺乏2,因此响应了一个ack=2 (通常叫作最小ack)

    发送方重发seq=2的报文, 接收方发现1,2,3已经完整接收了,就响应下一个指望值, 即响应ack=4

  3. 每发送一个报文就启动一个定时器吗?

    为了保证可靠性,TCP增长了超时重传机制, 使得每一个未被确认(ACK)的报文在必定时间后能够被从新发送

    一种实现方式就为每一个未被确认的报文都单独配置一个计时器,但是这样作的话开销太大了

    RFC 6298Managing the RTO Timer中说起了一种单一计时器的管理方式(具体细节请参考文档)

    每一个已发送但未确认数据包都会被放进队列里, 这个队列持有一个单独的计时器

    当第一个数据包进入队列时,计时器启动了

    若是计时器超时,队列头部的数据包会被重发,而且计时器从新计时

    当收到ACK时,计时器也会重启

    队列的全部数据都被确认了的话,就关闭定时器

  4. 超时时间怎么设置呢?

    重传超时时间(Retransmission TimeOut), 通常简称RTO, 这个时间既不能太长也不能过短。

    • 太长可能会出现数据包已经丢了,但还要等待无谓的时间才能重传

    • 过短可能数据包还没有到达,此时发生重传,浪费了资源

    往返时延 RTT (Round Trip Time)是配置RTO的一个重要指标, 可是因为网络间端到端的RTT并非固定的,因此TCP采用了一种自适应的方法来计算RTT, 而且根据计算的值来配置RTO

    整个过程是动态的,也就是说当RTT变化时,RTO也能相应的作出调整

    具体的细节能够参考RFC 6298The Basic Algorithm

  5. 必定要超时了才重传吗?

    TCP有一个快速重传机制, 当一个接收方收到三个以上的重复ack时,接收方就会直接根据ack的值重传对应的报文而无需等待超时

    下图展现了一个简单的示例

    1. 发送方发送了seq=1, seq=2, seq=3, seq=4的报文
    
    2. seq=1的报文发送了丢包
    
    3. 接收方分别接受到了`2,3,4`的报文,接收方缓存收到的报文,而后检查seq知道报文不连续,缺乏`seq=1`的报文, 因此每次都响应`ack=2`
    (为了简化描述,咱们不考虑延迟确认和缓冲区大小)
    
    4. 接收方连续收到了3个`ack=2`的报文,因此认为`seq=1`的报文丢包了,重传`seq=1`
    
    5. 接收方收到`seq=1`的报文后,发现seq=2,seq=3,seq=4已经接收过了,直接响应`ack=5`
    复制代码

总结

咱们看见TCP在实现可靠性上作出了不少精妙的设计,这些设计在大的方面追求至简,而在细节又追求极致,绝对是很是值得学习和思考的

TCP的这些设计你也能够在如今的软件系统中看到它的影子

  • 好比消息队列有相似的ack机制去确保消息已经被投递
  • 好比为了保证异步消息的有序性也会有相似seq的机制

正如前言所说,可靠性实则是一个很大的话题,不可避免的我仍是留下了不少坑,若是有错误的地方,还望指出。

参考

  1. James F.Kurose, Keith W.Ross 著 陈鸣译。《计算机网络 自顶向下方法》 (第六版)
  2. 维基百科 TCP
  3. RFC 793
  4. RFC 1071
  5. RFC 6298
  6. 一的补数
  7. 二的补数
  8. 往返时延
  9. TCP超时重传机制
  10. 拥塞控制
相关文章
相关标签/搜索