TCP的那些事儿

2020-04-21  本文已影响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重传。
对此有两种选择:

快速重传机制

于是,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的第一个段来做标志。

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
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的设置对于重传非常重要。

经典算法

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

上图中,我们可以看到:

下面是个滑动后的示意图(收到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,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。

上一篇 下一篇

猜你喜欢

热点阅读