聊聊TCP

        TCP(Transmission Control Protocol 传输控制协议协议)现在是如此广泛的被使用,其重要性不言而喻,然而他又是一个超级复杂的协议,也是每个程序员必备的基本功,在此记录我的一些理解。本篇文章不仅告诉你tcp是怎么样的,还努力告诉你为什么是这样的。

TCP的目标

        首先我们需要搞明白TCP协议设计的目标是什么,只有这样我们才能更好的理解其协议内容。我认为其目标主要包括:可靠性、高性能,和必要的安全性。TCP所有的设计考量都是为这三个目标而做出的努力。TCP经常被这么描述:TCP协议提供一种面向连接的,可靠的字节流服务。

TCP的可靠性

         那么TCP的可靠性指什么?要回答这个问题,我们首先要意识到TCP是工作在整个协议栈的第四层,是基于IP协议的。其封装过程如下图所示。

       如下图所示,当机器A上的应用程序A想将数据发送到机器B上的应用程序B时,数据流从应用程序A通过它所在主机的TCP/IP栈向下传输,经过几台中间路由器,通过应用程序B所在主机 的TCP/IP栈向上传输,最后抵达应用程序B。一个TCP段离开应用程序A所在主机的TCP层时,会被封装到一个IP数据报中,传送给其对等实体主机。 它所走的路由可能要经过很多路由器。我们知道IP协议是一个不可靠的协议,ip分组可能会丢失,会乱序,同时数据经过很多中间设备,数据可能会被篡改。

        现在我们来尝试回答上面的问题,TCP可靠性到底指什么?是指"TCP能够保证它所发送数据的可靠传输“吗?不,这种说法尽管很常见,但并不恰当。其实,只要稍微想一下就会知道这种说法是不对的,比如,如果将机器A的网线拔掉,不论TCP如何努力,都不可能将数据传送到机器B。那么,如果TCP不能保证将提交给它的所有数据都传送出去,它又能保证什 么呢?第一个问题是向谁保证?

        第一个可以讨论确保可靠传输问题的地方就是应用程序B所在主机的TCP层。当一 个段抵达应用程序B所在主机的TCP层时,唯一可以确定的就是这个段已经到达了,但它可能损坏了,可能是重复的数据,可能是错序的,或者是由于其他一些原 因无法接受的。注意,发送端TCP无法对这些抵达接收端TCP的段做出任何保证。

        另一个可以讨论确保可靠传输问题的地方是应用程序B。我们知道,无法保证应用程序A发送的所有数据都会到达。TCP的可靠性是指,TCP能够向应用程序B保证,所有到达的数据都是按序且未受损的。(注意:已经到达TCP层,并且已经被ACK的数据不一定能交付给应用程序,比如,接收端主机B可能在刚刚对数据进行了ACK,但应用程序B还没有将其读走之前,就崩溃了。TCP向发送端提供的唯一一个数据接收通知就是这个ACK。发送端应用程序A无法从TCP自身判断对等实体应用程序B是否真的收到数据了)

TCP如何保证其可靠性

       我们已经了解了TCP可靠性意味着什么,那么TCP是如何保证其可靠性的呢?

首先,TCP引入确认和重传机制解决分组丢失的问题。其次,TCP为发送的每一个字节进行编号,保证发送数据的有序和不重复。最后,TCP通过校验和以及(option 19 option29)(后面详细解释)来保证无损。另外,TCP还支持流量控制来保证一个快的发送方不会把一个慢的接收方淹没。

我们来看看TCP具体是怎么做的。请记住,tcp既要保证可靠性,又要兼顾性能和安全,有些时候你会看到各种取舍。

TCP连接的建立

       TCP要为发送的每一个字节进行编号,因此在传输数据之前,通信的两端需要协商一个初始化序列号(缩写为ISN:Inital Sequence Number)。初始化序列号有什么讲究呢,能不能每次都从一个固定值 比如1 开始呢,这样就不用每次都协商了,而直接开始数据传输。想法是美好的,然而实际是不可行的。想像一下,如果ISN固定从1开始,会有什么问题。假设Client和Server建立好一条TCP连接后,Client连续给Server发了10个包,这10个包恰好被中间的某个路由器缓存了(路由器会毫无先兆地缓存或者丢弃任何的数据包),这个时候碰巧Client挂掉了,然后Client用同样的端口号重新连上Server,Client又连续给Server发了5个包,序列号号变成了5。接着,之前被路由器缓存的10个数据包全部被路由到Server端了,Server给Client回复确认号10,这个时候,Client就懵了,什么情况,怎么到10了,我的序列号才到5啊。这就全乱了!因此,RFC793中,建议ISN和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始,这需要4小时才会产生ISN的回绕问题,这几乎可以保证每个新连接的ISN不会和旧连接的ISN产生冲突。这种递增方式的ISN,很容易让攻击者猜测到TCP连接的ISN,现在的实现大多是在一个基准值的基础上随机进行的。

         通过上面分析,可以知道,每次通信之前,TCP都必须先建立一个连接来协商初始化序列号,然后才能进行数据传输。这个连接建立的过程就是著名的三次握手(当然,三次握手还会交换一些其他初始化信息,比如窗口大小等等,我们这里先忽略)。那么话又说回来,为什么是三次?我们来分析下要完成协商的目的需要的步骤:

  1. Client端首先发送一个SYN包告诉Server端我的初始序列号是X;
  2. Server端收到SYN包后回复给Client一个ACK确认包,告诉Client说我收到了;
  3. 然后Server端也需要告诉Client端自己的初始序列号,于是Server也发送一个SYN包告诉Client我的初始序列号是Y;
  4. Client收到后,回复Server一个ACK确认包说我知道了。

       整个过程需要4次交互,但是仔细考虑下,会发现问题:A) 第三步中,Client收到Server端的SYN包后,如何区分这是对之前自己发起请求的连接的响应者SYN还是一个新发起连接的SYN呢?B)第二步中的ACK包和第三步的SYN包,完全可以合在一起发送,这样可以少一次交互,提高效率,同时也解决了问题A).

        所以,建立一个TCP连接,最少需要三次交互,即三次握手。其过程如下图所示:

现在我们来考虑一些特殊场景:

场景一:如果通信的两端同时发起连接请求会怎么样?

场景二:如果Client刚发送完SYN包之后就挂了会怎么样?

场景三:如果有个怀着恶意的Client,不停的给Server发送SYN包,却不响应最后一个ACK会怎么样?(即所谓的SYN Flood攻击)

场景一:

        尽管概率很小,但是两个应用程序彼此同时执行主动打开时可能的。如下图所示,通信的双方在接收到对方的SYN包之前,都进行了主动打开的操作并发出了自己的SYN包。同时打开的存在可能会引发一种比较有意思的现象,即tcp自连接:源ip、源端口和目的ip、目的端口完全相同的情况。

场景二:

      如果Client刚刚发送完SYN包,然后就挂了 ,会怎么样呢?我们可以来实际模拟一下。采用hping3 工具可以容易的模拟出这种场景,比如在机器 192.168.1.222上执行如下命令,同时在机器192.168.1.118 上用tcpdump 抓包。结果如下

  [root@test222 zy]# hping3 -I eth0 -a 192.168.10.99 -S 192.168.1.118 -p 8001 -i 1000 -c 1

        可以看到Server端会重发第二步的ACK+SYN包,细心的同学会发现这里一共重发了5次,每次重发的间隔时间依次是 1s 2s 4s 8s 16s ,总共31s,第5次发出后还要等32s才知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会断开这个连接目前Linux默认会重发5次,可以通过tcp_synack_retries参数来调整重发次数。(这里先提一个问题,为什么重发的时间间隔刚好是2指数次方?)。

场景三:

        从场景二我们看到如果Client端发送完SYN之后立马就挂掉了,Server端默认情况下需要63秒才会断开这个连接,想象一下,如果这段时间内有大量的这种请求发过来,会有什么问题?由于这个时间长达63秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的SYN包给Server,这就有可能会耗尽Server的SYN队列,导致正常的Client的三次握手请求不能完成。这样Server就没法再继续提供服务了。这就是所谓的SYN Flood攻击。

       刚刚我们提到一个SYN队列,这是什么东西呢?如下图所示,在三次握手协议中,Server维护一个半连接队列,该队列为每个客户端的SYN包维护必要的资源(Server在接收到SYN包的时候,就已经创建了request_sock结构,存储在半连接队列中),并向Client端发出确认(SYN+ACK),正在等待Client的ACK包。该队列即为SYN 队列,队列长度通过tcp_max_syn_backlog参数控制,默认为128。当这个队列满了,不开启syncookies的时候,Server会丢弃新来的SYN包,而Client端在多次重发SYN包得不到响应而返回(connection time out)错误。当Server端开启了syncookies=1,那么SYN半连接队列就没有逻辑上的最大值了,并且/proc/sys/net/ipv4/tcp_max_syn_backlog设置的值也会被忽略。syncookies是linux内核为解决SYN Flood攻击而引入的解决方案,其原理,我们后面再说明。我们先来模拟一下syn flood攻击的情况


为了模拟syn flood攻击,我们需要先调整下内核参数,关闭syn cookies 和减小 tcp_max_syn_backlog的值。通过下面的命令可以临时调整内核参数

      [root@test118 zy]# sysctl -w net.ipv4.tcp_syncookies=0

      [root@test118 zy]# sysctl -w net.ipv4.tcp_max_syn_backlog=8

然后我们还是使用hping3工具来模拟syn flood攻击,步骤如下:

1)先用nc工具打开一个监听端口,模拟一个server端

     [root@test118 zy]# nc -l 10899

2)用hping3工具发起syn 攻击,(注意,我们指定发起7个 ,因为,虽然队列大小配置为8 但是看上去第8个请求就无法成功了(我的测试环境,linux内核是 centos 2.6.32,不知道不同的内核是否表现不同))

     [root@test222 zy]# hping3 -I eth0 -a 192.168.10.99 -S 192.168.1.118 -p 10899 --fast -c 7

3)查看端口使用情况,结果如下

     [root@test118 zy]# netstat -ntalp|grep 10899

4)模拟正常用户发起请求

     [zy@test118 ~]$ telnet 192.168.1.118 10899

     Trying 192.168.1.118...

     telnet: connect to address 192.168.1.118: Connection timed out

可以看到此时已经无法正常连接成功了。再次查看端口使用情况,发现正常的请求一直处理SYN_SEND状态,即,发送的syn包没有被server端ack。

5)关闭所有端口,重发上述操作,只是将syn攻击数改为6 ,会发现telnet的时候能正常连接。

        由上面的分析和模拟可以知道,tcp三次握手过程本身存在这样的缺陷,为了防止syn flood攻击,建议不要将tcp_max_syn_backlog设置的太小,tcp_syncookies也不要关闭。所有我们模拟完成后记得将内核参数调整回去。不过,也不要认为tcp_syncookies是万金油,他并不能解决所有问题。

accept队列:

           出于完整性考虑,上面的图中出现了accept 队列。这里也简单介绍下。上面已经说过,TCP的连接状态从Server响应客户端(SYN+ACK)后,到ClientACK报文到达Server之前,一直保留在半连接状态中;当Server接收到Client的ACK报文后,该条目将从半连接队列搬到accpet队列(也叫完成队列)尾部,该队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128,而 backlog 的值是由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 可以由我们的应用程序去定义。进入这个队列的连接,已经完成三次握手,其tcp状态为 ESTABLISHED,但是此时,由于还没有分派给对应的应用程序,所以此时,如果你用netstat命令去查看会看到最后一列的所属进程为"-"。那么这个已完成连接是怎么分派给应用程序的呢,写过socket编程的同学都知道,server端的套路是,socket bind、listen、accept,对,就是这个accept函数,他的作用就是从队列取出一个连接,生成一个新的的sockfd,此后通信两端就可以正常的收发数据了。

        想一想,如果完成队列满了,并且应用程序忙于其他事情,来不及从队列中取走,此时Client 又发起新的连接会发生什么?此时,server会根据/proc/sys/net/ipv4/tcp_abort_on_overflow来决定如何返回,为0表示直接丢弃该ACK(意味着server认为没有收到ack,因此会重发syn+ack给Client,重发次数由 tcp_synack_retries决定),1表示发送RST通知client;相应的,client则会分别返回read timeout 或者 connection reset by peer错误。

         那么,有同学会想,这个对列的长度是不是越大越好呢?也不是,回想一下,epoll的处理循环一般是怎么写的,他会一次取出所有的完成队列的数据取处理,太长的队列会导致一些前端超时。所以这个值具体设为多大,还是需要根据应用业务具体考虑。

         最后,介绍一下如何查看这些队列的相关信息:

查看SYN queue 溢出:

    [root@test118 zy]# netstat -s | grep LISTEN

    1100030 SYNs to LISTEN sockets ignored

     [root@test118 zy]# netstat -s | grep listen

    1100030 times the listen queue of a socket overflowed

查看Accept queue 溢出:

    [root@test118 zy]# netstat -s | grep TCPBacklogDrop

   TCPBacklogDrop: 88298997

查看应用程序完成队列大小:

    [root@test118 zy]# ss -l

    Recv-Q Send-Q              Local Address:Port                                                           Peer Address:Port   

    0      4096                               :::7749                                                                              :::*       

    0      1                                     ::ffff:127.0.0.1:8005                                                         :::*       

    0      100                                 :::8009                                                                               :::*       

    0      2048                              192.168.1.118:6666                                                        *:*      

    在LISTEN状态,其中 Send-Q 即为Accept queue的最大值,Recv-Q 则表示Accept queue中等待被服务器accept()的值。

TCP的确认和重传

         确认和重传的基本原则非常简单,发送端请求发发送一段数据,接收端在成功接收到一个有效数据包后发送一个确认应答数据包给发送端。如果发送端在发送完某一段数据之后一定时间内,还没收到接收端的确认,发送端将重发这些数据。然而,这里面有非常多的细节需要考虑和理解,来看看tcp如何做的。

TCP的序号规则

        在说tcp确认和重传的一些细节之前,我们先来看一下tcp的序列号,前面已经说了,tcp为发送的每一个字节编号。三次握手的一个目的就是协商初始序列号,三次握手建立完成后,数据序列号又是怎么变化的呢?抓个包来看看。

       我们模拟的请求包为:通过telnet建立连接,然后 发了三次数据,分别为:

       111111111111111111111

        2222222222222222222

        444444444444

         这样分析seq的变化不是很直观,因此我们借用wireshark的一个辅助功能, 生成一个更直观的图,操作方式为:Statistics ->Flow Graph...->TCP flow ->OK。

(默认情况下,wireshark的序号是相对序号,为了便于说明,我这里改成了绝对(实际)序号,设置方式为:Edit -> Preferences ->protocols ->TCP取消勾选Relative sequence number即可)

  1. Client发起SYN请求,初始序号为Seq=3368607022。
  2. Server返回SYN+ACK 对应的序号为Seq=4073533551Ack=3368607023. 确认序号为(收到的)请求seq的值加1
  3. Client返回对Server的确认 Seq=3368607023Ack=4073533552,请求序号等于(收到的)ack序号,确认序号等于(收到的)请求的seq值加1
  4. Client发送数据到服务端,内容为"111111111111111111111\r\n" 长度为23, Seq=3368607023Ack=4073533552。请求序号等于(收到的)ack序号,确认序号等于(收到的)请求的seq值加1
  5. Server端对收到的数据进行确认, Seq=4073533552Ack=3368607046 。请求序号等于(收到的)ack序号,确认序号等于(收到的)请求的seq值加数据长度 3368607046  = 3368607023 + 23
  6.  Client发送数据到服务端,内容为"2222222222222222222\r\n" 长度为21, Seq=3368607046Ack=4073533552。请求序号等于(收到的)ack序号,确认序号等于(收到的)请求的seq值加1
  7. Server端对收到的数据进行确认, Seq=4073533552Ack=3368607067 。请求序号等于(收到的)ack序号,确认序号等于(收到的)请求的seq值加数据长度 3368607067  = 3368607046 + 21
  8.  Client发送数据到服务端,内容为"444444444444\r\n" 长度为14, Seq=3368607067Ack=4073533552。请求序号等于(收到的)ack序号,确认序号等于(收到的)请求的seq值加1
  9. Server端对收到的数据进行确认, Seq=4073533552Ack=3368607081 。请求序号等于(收到的)ack序号,确认序号等于(收到的)请求的seq值加数据长度 3368607081  = 3368607067 + 14

可以看到:

A)Client和Sever端各自都有自己的请求序列号,并且其初始值为随机的。

B)确认序号是逐渐递增的,确认序号表示的是接收端希望收到的下一个字节的序号。(不要得出确认序号等于请求seq+len的结论,因为这个例子里面没有出现丢包和重传的情况)

C)下一次的请求序列号等于上一次接收到的确认序列号

当然上述变化都是在正常情况下的一个情况,如果出现丢失、重传则其变化就不是这样了。

TCP的确认机制

好了,上面对tcp的序号有了一个初步了解,我们现在可以来看看tcp的确认重传机制的一些细节了。

TCP采用的是一种被称为累积确认的确认机制。累积确认有两层含义:

  1. 应答序号是逐渐递增的,应答序号表示的是接收端希望收到的下一个字节的序号
  2. 不可进行跨越式数据应答。

累积确认的优点:

  1. 易于产生。
  2. 不必对发送的每一个报文段进行确认。可以节省带宽资源(没有发送重传的情况下)

累积确认的缺点:

  1. 发送方不能接收到所有成功传输的报文段的确认信息。

上图演示了,tcp的确认过程:

在收到对第一个包的确认之前,发送方连续发了5个包,这是允许的,但是这一点也受到诸多限制,后面我们会看到。

1.第一个包正常到达,所以接收方返回ack=2;

2.第二个包丢掉了,第三个包到达接收方时,接收方返回ack=2,因为此时,接收方期望收到的seq是2,但是到来的却是3 ,根据累计确认原则,不能跨越确认,因此返回ack=2

3.第四个包,第5个包依次到达,接收方情况和收到2时一样。

4.发送端收到接收端返回的ack2 于是重传,这个示意图并没有指明是怎么样重传的,是重传了2-5的所有包,还是只重传了第二个包。也没有指明,接收方收到,第 3 4 5个包时 是怎么处理这些包的,是直接扔掉呢,还是先缓存起来。我们能确认的是,这一次传输之后,2 3 4 5包都被接收方正确接收了,所以他返回ack=6。

可以看到,并没有对 3 4 5 包都发一个单独的确认,而是直接确认5号包,ack=6意味着,6之前的所有包,我都收到了,我希望的下一个包序号是 6

我们先来看下发送端的情况,收到ack 2 时,发送端有两种选择:

  1. 仅重传2号包。
  2. 重传2号包之后的所有包,即 2 3 4 5四个包

两种选择的优缺点都比较明显:方案(1),优点:按需重传,能够最大程度节省带宽。缺点:重传会比较慢,因为重传2号包后,需要等下一个超时才会知道3号是否需要重传。方案[2],优点:重传较快,数据能够较快交付给接收端。缺点:重传了很多不必要重传的包,浪费带宽,在出现丢包的时候,一般是网络拥塞,大量的重传又可能进一步加剧拥塞。

tcp默认情况下是按照方案2 进行的,但是也可以通过一个sack的选项来支持方案1。sack 先暂时不细说,后面单独介绍。我们先来看下tcp的延时确认。

TCP的延迟确认

         通过前面的抓包,可以看到,ack,单独发送时,需要消耗带宽和耗时半个rtt,因此大量的确认号消耗了大量的带宽,虽然大多数情况下,ACK还是可以和数据一起捎带传输,但是如果没有捎带传输,那么就只能单独回来一个ACK,如果这样的分段太多,网络的利用率就会下降。为缓解这个问题,RFC建议了一种延迟的ACK,也就是说,ACK在收到数据后并不马上回复,而是延迟一段可以接受的时间,延迟一段时间的目的是看能不能和接收方要发给发送方的数据一起回去,因为TCP协议头中总是包含确认号的,如果能的话,就将数据一起捎带回去,这样网络利用率就提高了。延迟ACK就算没有数据捎带,那么如果收到了按序的两个包,那么只要对第二包做确认即可,这样也能省去一个ACK消耗。由于TCP协议不对ACK进行ACK,RFC建议最多等待2个包的积累确认,这样能够及时通知对端我这边的接收情况。Linux实现中,有延迟ACK和快速ACK,并根据当前的包的收发情况来在这两种ACK中切换。一般情况下,ACK并不会对网络性能有太大的影响,延迟ACK能减少发送的分段从而节省带宽,而快速ACK能及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。Linux中,TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。

TCP的超时时间计算

        前面说了,发送端在某个数据包发送出去,在一段固定时间后如果没有收到对该数据包的确认应答,则(假定该数据包在传输过程中丢失)重新发送该数据包。那么问题来了,如何确定超时重传的时间?太长,可能包已经丢了半天,发送端还没反应过来,效率不高。太短,网络稍微有点波动就引发重传,导致网络中很多无效包,浪费带宽,效率也不高。因此,重传的重点和难点就是超时时间的确认。TCP采用自适应算法来动态调整超时时间,该算法的要点是TCP监控每个连接的性能(采样RTT),由此推算出合适的超时实现。

两个概念:

    RTT(Round Trip Time):一个数据包从发出去到回来的时间

    RTO(Retransmission TimeOut):重传超时时间

经典算法:

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

     1)首先,先采样RTT,记下最近好几次的RTT值。

    2)然后做平滑计算SRTT( Smoothed RTT)。公式为:(其中的 α 取值在0.8 到 0.9之间,这个算法英文叫Exponential weighted moving average,中文叫:加权移动平均)SRTT = ( α * SRTT ) + ((1- α) * RTT)

    3)开始计算RTO。公式如下:

    RTO = min [ UBOUND,  max [ LBOUND,   (β * SRTT) ]  ]

    其中:

    UBOUND是最大的timeout时间,上限值

    LBOUND是最小的timeout时间,下限值

    β 值一般在1.3到2.0之间。

经典算法中问题

         在发生重传时,由于两个报文段包含同样的信息,发送方无法区分确认到底是针对哪个报文段的,这种现象称为确认二义性。TCP应当认为确认是针对最早的那次传输还是最迟的那次传输呢?(用哪一次来计算RTT?通过时间戳选项可以解决此问题)    

Karn 算法:

    为了解决上面的问题,所以1987年的时候,提出了Karn算法,这个算法的基本思想就是是

    忽略重传,不把重传的RTT做采样    

    这样一来就避免了二义性问题。然后简单的Karn算法也存在问题:

    如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的RTO很小),于是,因为重转的不算,所以,RTO就不会被更新,使得反复的重传循环继续下去。这将是一个大灾难。

    为了适应这种情况,Karn算法要求发送方使用定时器补偿策略把超时重传的影响估计在内。补偿策略使用和经典算法类似的公式来计算RTO。但是当出现超时时,TCP就加大定时时限(为了避免无限增加,多数实现都规定了一个上限值)。

    NewRTO=β*RTO(β的经典取值是2,该公式即我们常说的指数退避)

Jacobson / Karels 算法

    上面两种算法,还存在另一个问题——如果RTT有一个大的波动的话,很难被发现,因为被平滑掉了。(新的RTT只占0.2的权重)。

    为了解决这个问题,1988年,推出来了一个新的算法,这个算法叫Jacobson / Karels Algorithm(参看RFC6289)。

    这个算法引入了最新的RTT的采样和平滑过的SRTT的差距做因子来计算。

    公式如下:(其中的DevRTT是Deviation(偏差) RTT的意思):

     SRTT = SRTT + α (RTT – SRTT)  —— 计算平滑RTT

    DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|) ——计算平滑RTT和真实的差距(加权移动平均)

    RTO= µ * SRTT + ∂ *DevRTT

    其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4  最后的这个算法在被用在今天的TCP协议中(Linux的源代码在:tcp_rtt_estimator)

RFC6298(2011发布) 中对RTO计算过程的描述:

    初始化:

    RTO=1秒

    第一次计算:(R=本次RTT,K=4,G为时钟间隔)

     SRTT = R

    DevRTT =R/2

    RTO=SRTT + max(G,K*DevRTT )

    后续计算:(R`=本次RTT,K=4,α=1/8,β=1/4)

     DevRTT =(1-β)*DevRTT + β*|SRTT-R`|

    SRTT = (1-α)*SRTT + α*R`

    RTO = SRTT + max(G,K*DevRTT )

    RTO范围,最小值1s,最大值 至少60s

RFC6298(2011发布) 中对重传定时器的描述

    1.Every time a packet containing data is sent (including a retransmission), if the timer is not running, start it   running,so that it will expire after RTO seconds (for the current value of RTO).

    每次发送一个包含数据的tcp段(包括重传),如果重传定时器没有启动,则启动它,以便在RTO时间后超时

    2.When all outstanding data has been acknowledged, turn off the retransmission timer.

    当所有的发出去的包都被确认后,关闭重传定时器

    3.When an ACK is received that acknowledges new data, restart the retransmission timer so that it will expire after RTO seconds(for the current value of RTO)

    如果收到对新数据的ack,则更新RTO,然后重启定时器,以便在新的RTO时间后超时。RTO= µ * SRTT + ∂ *DevRTT

    When the retransmission timer expires, do the following

    当重传定时器超时时:

    4.Retransmit the earliest segment that has not been acknowledged by the TCP receiver.

    重传之前没有被确认的TCP段

    5.The host MUST set RTO <- RTO * 2 ("back off the timer").   A maximum value MAY be placed on RTO provided it is at least 60 seconds

    将RTO设置为之前的两倍,RTO最大值上限 最小不低于60s

    6.Start the retransmission timer, such that it expires after RTO seconds (for the value of RTO after the doubling operation outlined in 5)

    启动重传定时器,以便在RTO时间后超时(RTO已经被设置为之前的2倍)

     When the retransmission timer expires, do the following

     当重传定时器超时:

    7.If the timer expires awaiting the ACK of a SYN segment and theTCP implementation is using an RTO less than 3 seconds, the RTO MUST be re-initialized to 3 seconds when data transmission begins (i.e., after the three-way handshake completes).

    如果这是在等待SYN的ack,并且RTO小于3s,那么RTO必须被重设为3s

关于内核实现理解:

    我们把上面的公式做一个简单的变形,可得到如下的公式:

     SRTT = SRTT + ɑ*(RTT-SRTT)

    DevRTT = DevRTT + β*(|RTT-SRTT|-DevRTT)

    为了防止浮点运算,我们只需要做个变化,让ɑ 和 β 为1/2^n,那么上面的的等式就变为下面这个了

    RTO= µ * SRTT + ∂ *DevRTT µ =1 ,∂ =4

实际实现中n=3 m=2 则其C的伪代码可以表示为:

RTO计算的伪代码:

/∗ update Average estimator ∗/
m −= (sa >> 3); // sa扩大了8倍 m =m - sa/8 即 m = RTT-SRTT
sa += m; //sa = sa + m → sa = sa + m - sa/8  →  8sa = 8sa + m -sa
/∗ update Deviation estimator ∗/
if (m < 0)
   m = −m; |RTT-SRTT|
m −= (sv >> 2); //sv扩大了4倍 m = m - sv/2 → |RTT-SRTT|-Dev
sv += m; // sv = sv +m  → 4Dev = 4Dev + |RTT-SRTT|-Dev
rto = (sa >> 3) + sv; // RTO = SRTT + 4DevRTT

这个时候再去看内核的实现代码就简单了

TCP的流量控制

      考虑以下情况:一个较快的发送方如果不停的往一个较慢的接收方发送数据,接收方来不及处理(接收缓冲区溢出),只能丢弃数据。TCP为了保证其可靠性,要怎么处理这种情况?为了解决上述问题(即流量控制的问题),TCP采用了一种被称为滑动窗口的技术来进行流量控制。

        发送方维护一个窗口,该窗口的大小由接收端指定(动态变化),发送端不能一次发送超过滑动窗口大小的数据。当对方通告的窗口大小为0时 发送端将不能再发送数据(有两种例外情况,A、发送方需要发送紧急数据时。B、为了避免发送窗口为0后非0的窗口通告丢失而造成的死锁,发送方定期的对大小为0的窗口发送试探性报文

如下图所示,演示了TCP的滑动窗口

下面两图演示窗口的滑动情况,收到36的ACK后,窗口向后滑动5个byte

图片描述

图片描述

      下图,展示了一个发送端是怎么受接收端控制的。接受方设置接收缓存区大小为360,接受方从接收缓存读取40个字节.由于发送方发送速大于接收方读取速度窗口大小最终变为0。那么,窗口大小变为0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,那如果发送端不发数据了,接收方一会儿有窗口空间可用了,怎么通知发送端呢?解决这个问题,TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的窗口大小。俗话说,有人的地方,就有江湖,同样只要有等待的地方都可能出现DDoS攻击,ZWP也不例外,有一种叫Sockstress DDos攻击,正是利用这一点来进行的。

图片描述

         想象一下,上面的例子中,server窗口变为0了,然后客户端通过zwp机制询问server端的窗口大小,这个时候,server端的应用程序,刚好读走了一个字节,ack就会返回窗口大小为1.然后发送端可以发送1字节的数据。然后接收端缓冲区又满了,接着接收方应用程序又读走1字节。如此循环 会造成通信链路上存在很多小的数据包(有效载荷为1,而传输开销为40)。这样效率非常低下。这种现象被称为窗口糊涂综合症。造成这个问题原因有二:

1) 接收端一直在通知一个小的窗口;

2) 发送端本身问题,一直在发送小包。

那么自然对应的解决思路也有两条:

1) 接收端不通知小窗口,即通告零窗口后,要等到缓冲区可用空间至少达到总空间的一半或者达到最大报文段长度(MSS)之后才发送更新的窗口通告

2) 发送端尽量不发小包,积累一下数据再发送。这个就是著名的Nagle算法了。

Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。然而实际上很多实现并不这么严格,多数按照类似下面的条件来进行判断

(1)如果包长度达到MSS,则允许发送;

(2)如果该包含有FIN,则允许发送;

(3)设置了TCP_NODELAY选项(意在禁止nagle),则允许发送

(4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;

(5)上述条件都未满足,但发生了超时(一般设置延迟ACK,一般为200ms),则立即发送。

        第(4)点指出TCP连接上最多只能有一个未被确认的小数据包。从第(4)点]可以看出Nagle算法并不禁止发送小的数据包(超时时间内),而是避免发送大量小的数据包。由于Nagle算法是依赖ACK的,如果ACK很快的话,也会出现一直发小包的情况,造成网络利用率低。TCP_CORK选项则是禁止发送小的数据包(超时时间内),设置该选项后,TCP会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然也不会一直等,发生了超时(一般为200ms),也立即发送。Nagle算法和CP_CORK选项提高了网络利用率,但增加延时。从第三点可以看出,设置TCP_NODELAY选项,就是完全禁用Nagle算法了。这里有一点需要指出:nagle算法和延迟确认特性会相互作用,当出现(write-write-read)时会引发一个40ms的延时问题。举个例子,考虑下面场景:

        客户端在请求下载HTTP svr中的一个小文件,一般情况下,HTTP svr都是先发送HTTP响应头部,然后再发送HTTP响应body(特别是比较多的实现在发送文件的实施采用的是sendfile系统调用,这就出现write-write-read模式了)。当发送头部的时候,由于头部较小,形成一个小的TCP包发送到客户端,这个时候开始发送body,由于body也较小,这样还是形成一个小的TCP数据包,根据Nagle算法,HTTP svr已经发送一个小的数据包了,在收到第一个小包的ACK后或等待200ms超时后才能再发小包,HTTP svr不能发送这个body小TCP包;

         客户端收到http响应头后,由于这是一个小的TCP包,于是客户端开启延迟确认,客户端在等待Svr的第二个包来再一起确认或等待一个超时(一般是40ms)再发送ACK包;这样就出现了你等我、然而我也在等你的死锁状态,于是出现最多的情况是客户端等待一个40ms的超时,然后发送ACK给HTTP svr,HTTP svr收到ACK包后再发送body部分。

下面的抓包也演示了40ms延迟的问题,为什么是第14次之后才出现这个延迟,和内核的快速ack和延迟ack策略有关.


TCP的校验和

         TCP校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。TCP的校验和是必需的,而UDP的校验和是可选的。TCP和UDP计算校验和时,都要加上一个12字节的伪首部。伪首部包含如下信息:源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP是6)、TCP报文长度(报头+数据)。伪首部是为了增加TCP校验和的检错能力:如检查TCP报文是否收错了(目的IP地址)、传输层协议是否选对了(传输层协议号)等。

根据RFC 793对TCP校验和的定义,大致描述如下:

发送端:

1.把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。
2.把TCP报头中的校验和字段置为0。
3.用反码相加法累加所有的16位字(进位也要累加
4.将上述结果作为TCP的校验和 (注意,先取反再相加 和先相加,再将结果取反是一样的,一般实现的时候是直接将结果取反)

接收端:

将所有原码相加,高位叠加, 如全为1,则正确。

举例:

我们以4bit 为例来说明这个过程:

数据:   1001  0101   校验和  0000
则反码:0110  1010               1111
叠加:   0110 +1010+1111 = 0001 1111  
高于4bit的, 叠加到低4位 :0001 + 1111 = 0001 0000 再叠加到低4位 最终为0001 即为校验和
(先相加在取反则为:1001+0101+0000 = 1110,取反为0001.和先取反再相加一样)
接收端计算:
数据:  1001   0101   检验和  0001
原码:  1001   0101               0001
叠加:  1001 + 0101 +0001 = 1111  全为1,则正确。
TCP校验和提供了一种对数据是否损失的简单校验办法,但是仔细考虑下 其实可以发现这种校验并不是非常可靠,比如将某个字节+1 然后某个字节-1,校验和也会认为是成功的。因为Checksum 是被用来防止信号误差引起的错误,而对于认为篡改的防止,tcp提供了 optional 19 和optional 29来支持。

TCP连接的断开

建立一个连接需要3次交互,而断开一个连接需要4次,这是因为tcp是全双工的,双方都需要通知对方我已经发送完数据了,可以关闭我这段的传输了。下图演示了tcp连接关闭和状态变迁的过程

两端同时关闭连接也是可能的,如下图所示,同时关闭连接时,两端的状态变更过程一样:FIN_WAIT1---->CLOSEING----->TIME_WAIT

图片描述

同建立连接一样,对于关闭连接,我们也来考察一些特殊场景:

场景一:

如果Client关闭连接时,发送FIN包给Server端,但是Server端并没有ack会怎么样?

场景二:

如果Server端收到Client的FIN包,并且也发送了ack进行确认,但是他不发送FIN包会怎么样?

场景三:

Client收到Server端的FIN包,发送ACK后,为什么会进入TIME_WAIT状态,这个状态为什么需要等待2MSL才能变成CLOSED状态?这个状态会带来什么问题吗?

场景一:

        先来看,场景一,Client发送完FIN包后,会进入FIN_WAIT_1状态,正常来讲,对方应该很快返回一个ack,因此你几乎看不到FIN_WAIT_1的存在。但是,如果此时,网络有问题,或者对端主机崩溃了,FIN_WAIT_1会存在多久呢?根据tcp的重传机制,没有收到ack时,超时后,tcp会重发这个FIN包,重传次数可以由一个内核参数来指定,tcp_orphan_retries ,在我的ubuntu 14.04 内核 3.13上,该值默认为0,查阅内核代码可以知道,该值实际等同于 8。

/* Calculate maximal number or retries on an orphaned socket. */
static int tcp_orphan_retries(struct sock *sk, int alive)
{
    int retries = sysctl_tcp_orphan_retries; /* May be zero. */

    /* We know from an ICMP that something is wrong. */
    if (sk->sk_err_soft && !alive)
        retries = 0;

    /* However, if socket sent something recently, select some safe
     * number of retries. 8 corresponds to >100 seconds with
     * minimal RTO of 200msec. */
    if (retries == 0 && alive)
        retries = 8;
    return retries;
}

Linux内核官方文档对tcp_orphan_retries的描述是这样的:

This value influences the timeout of a locally closed TCP connection, when RTO retransmissions remain unacknowledged. See tcp_retries2 for more details.
The default value is 8. 

If your machine is a loaded WEB server, you should think about lowering this value, such sockets may consume significant resources. Cf. tcp_max_orphans.

如果你的机器是一个负载较重的web服务器,你可以考虑尝试降低该值,以减少FIN_WAIT_1的存在。

最后来实际模拟一下,先查看tcp_orphan_retries的值,

     [root@test222 zy]# sysctl -a |grep tcp_orphan_retries

     net.ipv4.tcp_orphan_retries = 0

在机器A(192.168.1.222)上开启一个端口,然后在机器B上(10.0.109.18)上连接该端口,然后断开机器B的网络,接着关闭,机器b上的对应程序,tcpdump抓包如下:


可以看到刚好重发8次,第一次重发间隔为200ms ,以后依次翻倍增长。所以FIN_WAIT_1的存在时长大致为:0.2*(1+2+4+8+16+32+64+128+256)=102s

场景二:

接下来,来看看场景二:

        如果Server端收到Client的FIN包,并且也发送了ack进行确认,但是他不发送FIN包会怎么样?根据状态转换图可以知道,此时,Client端处于FIN_WAIT_2状态,Server端处于CLOSE_WAIT状态。出现这种情况的最常见的原因是被动关闭的一方,没有调用close关闭TCP连接导致的。这可能由于多种因素引起的,下面列举三种常见情况:

1)应用程序问题:代码忘了调用close关闭连接;或者代码不严谨,出现死循环之类的问题,导致即便后面写了 close 也永远执行不到。

2)响应太慢或者超时设置过小,比如调用调用api的一方,逻辑很复杂,耗时,但是另一方却不耐烦,超时时间比较小,很快就把连接关闭了,而此时,调用方还忙于业务处理,来不及关闭相应连接,那么就会积累CLOSE_WAIT状态。比如我们的服务通常通过SLB调用,如果调用方反应慢,SLB是有可能主动关闭连接的。

3)accept完成队列太大,而请求方超时时间又比较小,导致接收方来不及从队列里取出消息,对方就关闭连接了。

FIN_WAIT_2的最长保存时间由tcp_fin_timeout参数决定,默认为60s。如何优雅的关闭TCP连接是一个细致而重要的问题,这里不详细讨论,记住一点就是,recv的错误处理,如果recv返回0 那么表示对端关闭了连接,此时如果你没有数据需要发送,也请调用对应api关闭tcp连接。

然而对于CLOSE_WAIT就没有那么幸运了,Linux内核没有回收该状态的参数,只要你对于应用程序不关闭,CLOSE_WAIT状态几乎不会消失(当然如果有keepalive,默认2个小时后,会检测连接异常,系统会关闭连接)。

场景三:

最后来看看场景三,即TIME_WAIT状态,这几乎是被问得最多的TCP状态了。

从状态转换图来看,主动关闭的一方会进入TIME_WAIT状态。那么为什么需要这样一个状态呢?主要有两个原因:

1)防止最后一个ack丢失,以便进行重发。如果主动关闭方不进入TIME_WAIT,那么在主动关闭方对被动关闭方FIN包的ACK丢失了的时候,被动关闭方由于没收到自己FIN的ACK,会进行重传FIN包,这个FIN包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个FIN包,协议栈会被迫回复一个RST包给被动关闭方,被动关闭方就会收到一个错误。

2) 防止链路上已经关闭的连接的残余数据包干扰正常的数据包,造成数据流不正常。如果没有这个状态,那么连接关闭后,假设又有一个新连接在同样的端口上被建立,此时,由于某些原因,之前在网络上被延迟的包,被传到了新连接上,并且其序列号也在可接受范围内,那么tcp就没办法区分了,这个时候数据就乱了。

所以,TIME_WAIT是必须的,那么其生存周期为什么又必须是2MSL呢?要回答这个问题,首先需要了解什么是MSL。MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP基于IP传输,而IP头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。TTL与MSL是有关系的但不是简单的相等的关系,MSL要大于等于TTL。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。那么2MSL等待时间后,任何迟到的报文段都将被丢弃,保证了新连接的数据可靠性。

那么TIME_WAIT有什么问题呢?前面说了TIME_WAIT状态需要等待2*MSL时间,也就是要2MSL相应的资源才会被释放。那么:

对于服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple,严重消耗着服务器的资源; 
对于客户端,短时间内大量的短连接,会大量消耗Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法再发起新的连接了;

TIME_WAIT着实令人头疼,但他又是必须的,不能直接去掉,那么应该怎么样来清除或者说管理TIME_WAIT状态呢,linux内核提供了一些参数来控制。

1)TIME_WAIT快速回收

Linux下开启TIME_WAIT快速回收需要同时打开tcp_tw_recycletcp_timestamps(默认打开)两选项。Linux下快速回收的时间为3.5*RTO(Retransmission Timeout)

2)TIME_WAIT重用

要同时开启tcp_tw_reuse选项和tcp_timestamps选项才可以开启TIME_WAIT重用,还有一个条件是:重用TIME_WAIT的条件是收到最后一个包后超过1s

3)修改TIME_WAIT运行存在的数量

tcp_max_tw_buckets控制并发的TIME_WAIT数量,默认值是180000。如果超过默认值,内核会把多的TIME_WAIT连接清掉,然后在日志里打一个警告。官网文档说这个选项只是为了阻止一些简单的DoS攻击,平常不要人为降低它

4)利用RST直接跳过TIME_WAIT状态

通过套接口的SO_LINGER选项,设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。这种方式具备危险性,而且不一定有效,实际测试发现,只有在丢弃数据时才会发生RST,不建议使用。

TCP的拥塞控制

        有了上面的各项特性 TCP似乎已经考虑方方面面了,有超时重传机制,还有端到端的流控机制。最早期的TCP确实也是这样,具体可以看RFC793的文档。注意到这篇文档的日期为1981年,TCP从此开始出现在互联网上传输数据。1986年10月,一件事情的发生使得TCP开启了一个新领域,从美国LBL到UC Berkeley的数据吞吐量从32Kbps下降到40bps,具体可以参见V. Jacobson的论文“Congestion Avoidance and Control” 是什么原因导致了数据吞吐量如此严重的下降呢?原来在TCP的控制机制里面只考虑到了接收端的接受能力,而忽略了一个很重要的方面,那就是没有考虑到网络自己的传输能力,从而造成了整个网络崩溃的发生。从这以后,TCP的研究课题就开始多了一个方向,那就是拥塞控制。拥塞控制,这些年发展非常迅速,出现了非常多的拥塞算法,比如Tahoe  Reno  NewReno  TCP Vegas Eifel HSTCP BI-TCP,CUBIC TCP(linux默认算法)、FastTCP、 TCP-Westwood 微软最新的TCP拥塞控制算法为Compound TCP (CTCP),windows 7默认不启用。拥塞算法是一个学术和商业研究的无底洞,非常复杂,这里只介绍最经典的Reno 算法,其他各种算法都是对其的各种优化。Reno算法一般包括四个方面:慢启动、 拥塞避免、快速重传、 快速恢复。

        TCP的拥塞控制主要原理依赖于一个拥塞窗口(cwnd)来控制,根据前面的讨论,我们知道有一个接收端通告的接收窗口(rwnd)用于流量控制;加上拥塞控制后,发送端真正的发送窗口=min(rwnd, cwnd)。关于cwnd的单位,一般描述是都认为是MSS(最大传输单元)

慢热启动算法–Slow Start:

慢启动体现了一个试探的过程,刚接入网络的时候先发包慢点,探测一下网络情况,然后在慢慢提速。不要一上来就拼命发包,这样很容易造成拥堵,慢启动的算法如下(cwnd全称Congestion Window):

1)连接建好的开始先初始化cwnd = N,表明可以传N个MSS大小的数据 。关于N的值,最初是1 后来19994年4月 RFC 2581将其增加到4 2013年4月,RFC 6928再次将其增加到10。 Linux 2.6.39之后采用RFC6928的建议
2)每当收到一个ACK,++cwnd; 呈线性上升 
3)每当过了一个RTT,cwnd = cwnd*2; 呈指数上升  
4)还有一个慢启动门限ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入”拥塞避免”阶段

拥塞避免算法–Congestion Avoidance:

        慢启动的时候说过,cwnd是指数快速增长的,但是增长是有个门限ssthresh(一般来说大多数的实现ssthresh的值是65535字节,约44个MSS段)的,到达门限后进入拥塞避免阶段。在进入拥塞避免阶段后,cwnd值变化算法如下:

(1) 每收到一个ACK,调整cwnd为(cwnd + 1/cwnd)*MSS个字节 
(2) 每经过一个RTT的时长,cwnd增加1个MSS大小。

这时候,cwnd的增加是线性增长的。

TCP是看不到网络的整体状况的,那么TCP认为网络拥塞的主要依据是它重传了报文段。前面我们说过TCP的重传分两种情况:

A)出现RTO超时,重传数据包。这种情况下,TCP就认为出现拥塞的可能性就很大,于是它反应非常’强烈’:

1) 调整门限ssthresh的值为当前cwnd值的1/2。 
2) reset自己的cwnd值为1 
3) 然后重新进入慢启动过程。

B) 在RTO超时前,收到3个duplicate ACK进行重传数据包。

这种情况下,收到3个冗余ACK后说明确实有中间的分段丢失,然而后面的分段确实到达了接收端,因为这样才会发送冗余ACK,这一般是路由器故障或者轻度拥塞或者其它不太严重的原因引起的,因此此时拥塞窗口缩小的幅度就不能太大,此时进入快速重传。

快速重传-Fast Retransimit:

(1) cwnd减半; cwnd = cwnd /2
(2) 将sshthresh 值设置为新的cwnd的值; sshthresh = cwnd
(3) 重新进入拥塞避免阶段。

在快速重传的时候,一般网络只是轻微拥堵,在进入拥塞避免后,cwnd恢复的比较慢。针对这个,“快速恢复”算法被添加进来,当收到3个冗余ACK时,TCP最后的[3]步骤进入的不是拥塞避免阶段,而是快速恢复阶段

快速恢复算法–Fast Recovery:

        快速恢复的思想是“数据包守恒”原则,即带宽不变的情况下,在网络同一时刻能容纳数据包数量是恒定的。当“老”数据包离开了网络后,就能向网络中发送一个“新”的数据包。既然已经收到了3个冗余ACK,说明有三个数据分段已经到达了接收端,既然三个分段已经离开了网络,那么就是说可以在发送3个分段了。于是只要发送方收到一个冗余的ACK,于是cwnd加1个MSS。快速恢复步骤如下(在进入快速恢复前,cwnd 和 sshthresh已被更新为:cwnd = cwnd /2,sshthresh = cwnd):

(1) 把cwnd设置为ssthresh的值加3,重传Duplicated ACKs指定的数据包  cwnd = sshthresh  + 3 * MSS

(2) 如果再收到 duplicated Acks,那么cwnd = cwnd +1

(3) 如果收到新的ACK,而非duplicated Ack,那么将cwnd重新设置为快速重传中(1)的sshthresh的值(cwnd = sshthresh)然后进入拥塞避免状态。

        仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于3个重复的Acks。3个重复的Acks并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到RTO超时,然后其他丢失的包就只能等待到RTO超时了。超时会导致ssthresh减半,并且退出了Fast Recovery阶段,多个超时会导致TCP传输速率呈级数下降。为解决这个问题,提出了New Reno算法。该算法是在没有SACK的支持下改进Fast Recovery算法,具体改进如下:

1) 发送端收到3个冗余ACK后,重传冗余ACK指示可能丢失的那个包segment1,如果segment1的ACK 通告接收端已经收到发送端的全部已经发出的数据的话,那么就是只丢失一个包,否则,就是有多个包丢失了。

2) 发送端根据segment1的ACK判断出有多个包丢失,那么发送端继续重传窗口内未被ACK的第一个包,直到滑动窗口内发出去的包全被ACK了,才真正退出Fast Recovery阶段。

         我们可以看到,拥塞控制在拥塞避免阶段,cwnd是加性增加的,在判断出现拥塞的时候采取的是指数递减。为什么要这样做呢?这种指数递减的方式实现了公平性,一旦出现丢包,那么立即减半退避,可以给其他新建的连接腾出足够的带宽空间,从而保证整个的公平性。

整个算法的可以用下图来示意:

tcp.fr_-1024x359


Linux下常用的网络工具

这几个命令应该是必须熟练使用的:

tcpdunp netstat nc ping traceroute telnet ss lsof strace

后记:

文章图片大部分源于网络,侵权请联系删除。

本文就到这里了,当然,TCP东西太多了,不可能面面俱到(比如文中提到的 sack,tcp option, syn cookies, quick ack ,ecn, keepalive 等等,有兴趣可以google其关键字 或者找时间再交流)而且不同的人有不同的理解,本文中可能存在一些错误或者荒谬的描述,欢迎交流、指正、批评。


参考资料:

http://blog.csdn.net/changyourmind/article/details/53127100

https://segmentfault.com/a/1190000008224853

http://elf8848.iteye.com/blog/2089414

http://blog.csdn.net/great3779/article/details/6578033

《TCP/IP详解卷一》

《用TCP/IP进行网际互联卷一》

《Web性能权威指南》

《TCP/IP高效编程的44个建议》

《unix网络编程卷一》




  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值