美文网首页
TCP的那些事儿

TCP的那些事儿

作者: 木叶苍蓝 | 来源:发表于2020-04-21 23:20 被阅读0次

    摘抄至 TCP的那些事儿

    数据传输中的 Sequence Number

    下图是我从Wireshark中截了个我在访问coolshell.cn时的有数据传输的图给你看一下,SeqNum是怎么变的。

    tcp_data_seq_num.jpg
    你可以看到,SeqNum的增加是和传输的字节数相关的。上图的,三次握手后,来了两个Len:1440的包,而第二个包的SeqNum就成了1441。然后第一个ACK回的是1441,表示第一个1440收到了。
    注意:如果你用Wirseshark抓包程序看3次握手,你会发现SeqNum总是为0,不是这样的,Wireshark为了显示更友好,使用了Relative SeqNum——相对序号,你只要在右键菜单中 protocol preference 中取消就可以看到Absolute SeqNum
    TCP重传机制

    TCP要保证所有的数据包都可以到达,所以,必须要有重传机制。
    注意:接收端给发送端的Ack确认只会确认最后一个连续的包。比如发送端发了1,2,3,4,5一共5份数据,接收端收到了1,2于是回ACK=3,然后收到了4(注意此时3没有收到),此时的TCP会怎么办?我们要知道,因为正如前面说的,SeqNum和ACK是以字节数为单位的,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包。不然发送端就以为之前都收到了。

    超时重传机制】

    一种是不回ACK,死等3,当发送方发现收不到3的ACK超时后,会重传3.一旦接收方收到3后,会回ACK=4,意味着3和4都收到了。
    但是,这种方式会有比较严重的问题,那就是因为死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事情,因为没有收到ACK。发送方可能会悲观地认为也丢了,所以有可能也会导致4和5重传。
    对此有两种选择:

    • 一种是仅重传timeout的包,也就是第3份数据。
    • 另一种是重传timeout后所有的数据,也就是3,4,5这三份数据。
      这两种方式有号也有不好。第一种会节省带宽,但是慢。第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长。
    快速重传机制

    于是,TCP引入了一种Fast Retransmit的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ACK最后那个可能被丢了的包,如果发送方连续收到了3次相同的ACK,就重传,Fast Retransmit的好处是不用等timeout了再重传。
    比如:如果发送方发出了1,2,3,4,5 个数据包,第一个份先送到了,于是就ACK回2,结果2因为某些原因没收到,3到达了,于是还是ACK回2,后面的4和5都到了,但是还是ACK回2,因为2还是没有收到,于是发送端收到了三个ACK=2的确认,知道了2还没有到,于是就马上重传2。然后,接收到收到了2,此时3,4,5都收到了,于是ACK回6。

    FASTIncast021.png
    Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是重传一个还是重传所有的问题。对于上面的实例来说,是重传2呢还是重传2,3,4,5呢?因为发送端并不清楚这个连续的3个ACK=2是谁传回来的,也许发送端发了20份数据,是6,10,20传来的呢。这样,发送端很有肯能要重传2到20的这堆数据(实际上有些TCP就是这样实现的),可见,这是把双刃剑。
    SACK方法

    另外一种更好的方式叫: Selective Acknowledgment(SACK),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎片。

    tcp_sack_example-900x507.jpg
    这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit的算法。当然,这个协议需要两边都支持。在Linux下,可以通过tcp_sack参数打开这个功能(Linux2.4后默认打开)。
    这里还需要注意一个问题——接收方 Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端的SACK里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化。但是接收方这么做可能会有一些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-out,如果后续的ACK没有增长,那么还是要把SACK的东西重传。另外,接收端这边永远不能把SACK的包标记为ACK。
    注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗发送端的资源。
    Duplicate SACK——重复收到数据的问题

    Duplicate SACK又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了。
    D-SACK使用了SACK的第一个段来做标志。

    • 如果SACK的第一个段的范围被ACK所覆盖,那么就是D-SACK
    • 如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是D-SACK
    • 实例 :ACK丢包 *
      下面的实例中,丢了两个ACK,所以,发送端重传了第一个数据包(3000-3499)
      ,于是接收端发现重复收到了,于是回了一个SACK=3000-3500,因为ACK都到了4000意味着收到了4000之前的所有数据,所以这个SACK就是D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道数据包没有丢,丢的是ACK包。
    Transmitted  Received    ACK Sent
    Segment      Segment     (Including SACK Blocks)
     
    3000-3499    3000-3499   3500 (ACK dropped)
    3500-3999    3500-3999   4000 (ACK dropped)
    3000-3499    3000-3499   4000, SACK=3000-3500
    
    • 实例:网络延误 *
      下面的实例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到ACK,而后面到达的三个包触发了Fast Retransmit算法触发的重传不是因为发出去的包丢了,也不是因为回应的ACK包丢了,而是因为网络延迟了。
    Transmitted    Received    ACK Sent
    Segment        Segment     (Including SACK Blocks)
     
    500-999        500-999     1000
    1000-1499      (delayed)
    1500-1999      1500-1999   1000, SACK=1500-2000
    2000-2499      2000-2499   1000, SACK=1500-2500
    2500-2999      2500-2999   1000, SACK=1500-3000
    1000-1499      1000-1499   3000
                   1000-1499   3000, SACK=1000-1500
    

    可见,引入了D-SACK,有以下几个好处:

    1. 可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
    2. 是不是直接的timeout太小了。导致重传的。
    3. 网络上出现了先发的包后到的情况(又称reordering)
    4. 网络上是不是把我的数据包复制了。
      知道这些东西可以很好的帮助TCP了解网络情况,从而可以更好的做网络上的流控。Linux下的tcp_dsack参数开启这个功能。(Linux 2.4后默认打开)
    TCP的RTT算法

    从前慢的TCP重传机制我们知道Timeout的设置对于重传非常重要。

    • 设长了,重发就慢,丢了老半天才重发,没有效率,性能差。
    • 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
      而且,这个超时时间再不同的网络情况下,根本没有办法设置一个死的值。只能动态的设置。为了动态的设置,TCP引入了RTT——Round Trip Time,这就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置Timeout——RTO(Retransmission TimeOut),以让我们的重传机制更高效。听起来似乎很简单,好像就是在发送端发包时记下t0,然后接收端再把这ACK回来时再记一个t1,于是RTT = t1 - t0。没有那么简单,这只一个采样,不能代表普遍情况。
    经典算法

    RFC793中定义的经典算法是这样的:

    1. 首先,先采用RTT,记下最近好几次的RTT值。
    2. 然后做平滑计算SRTT(Smoothed RTT)。公式为:
    SRT = (α * SRTT) + ((1-α) * RTT)
    `其中的α取值在0.8-0.9之间,这个算法英文叫做Exponential weighted movingaverage,中文叫: 加权移动平均`
    
    1. 开始计算RTO,公式如下:
    RTO = min[UBOUND, max[LBOUND, (β * SRTT)]]
    `UBOUND是最大的timeout时间,上限值`
    `LBOUND是最小的timeout时间,下限值`
    `β 值一般在1.3-2.0之间`
    
    TCP滑动窗口

    需要说明一下,如果你不了解TCP的滑动窗口这个事,你等于不了解TCP协议。我们都知道,TCP必需要解决的可靠传输以及包乱序(reordering)的问题。所以TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才能不会引起网络拥塞,导致丢包。
    所以,TCP引入了一些技术和设计来做网络流控,Sliding Window是其中一个技术。前面我们说过,TCP头里有一个字段叫做Window,又叫做Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓存区可以接受数据。于是发送端就可以根据这接收端的处理能力来发送数据,而不会导致接收端处理不过来。为了说明滑动窗口,我们需要先看一下TCP缓冲区的一些数据结构:


    sliding_window-900x358.jpg

    上图中,我们可以看到:

    • 接收端LastByteRead指向了TCP缓冲区读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。
    • 发送端的LastByteAcked指向了被接收端ACK过的位置(表示成功发送确认),LastByteSent表示发出去了,但是没有收到成功确认的ACKLastByteWritten指向是上层应用正在写的地方。
      于是:
    • 接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer - LastByteRcvd - 1
    • 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。
      下面我们来看看发送方的滑动窗口示意图:


      tcpswwindows.png

      上图中分成可四个部分,分别是:(其中那个黑模型就是滑动窗口)

    • 1 已收到ACK确认的数据。

    • 2 已发还没有收到ACK。

    • 3 在窗口中还没有发出的(接收方还有空间)。

    • 4 窗口以为的数据(接收方没空间)。

    下面是个滑动后的示意图(收到36的ack,并发出了46-51的字节)


    tcpswslide.png

    下面我们来看一个接受端控制发送端的图示:


    tcpswflow.png
    Zero Window

    上图,我们可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP Sligind Window给降成0的。此时,你一定会问,如果Window变成0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想象成Window Closed,那你一定还会问,如果发送端不发数据了,接收方过一会有Window size可用了,怎么通知发送端呢?
    解决这个问题,TCP使用了Zero Window Probe 技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ACK他的Window尺寸,一般这个值会设置成3次,每次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会RST把链接断开。
    注意:只要有等待的地方都可能出现DDoS攻击,Zero Window也不例外,一些攻击者会在和HTTP建好链接发完GET请求后,就把Window设置为0,然后服务端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。

    相关文章

      网友评论

          本文标题:TCP的那些事儿

          本文链接:https://www.haomeiwen.com/subject/qqcjzctx.html