深入理解TCP协议下

本文为作者原创内容,未经许可,禁止转载。如您发现侵权行为,请联系我们

上篇文章《深入理解TCP协议上》,我们探讨了TCP协议的首部以及在抓包工具下看到的TCP传输。这篇文章将围绕TCP的各种特性进行讨论。

首先,我们需要知道为什么TCP提供的服务是可靠的,这便与它拥有的特性息息相关。罗列如下:


一、确认应答(ACK):     


确认应答从字面意思不难理解, 就是“接收方”告知“发送发”:我已经收到了你的信息。这就像生活中,我们跟别人交流的时候,别人通过点头来回应我们他懂了我们的意思一样。所以,在网络中也是如此,当发送端没有收到服务端发来的“确认应答”,作为发送端就可以认为数据已经丢失,并进行重发,因而即使产生了丢包,仍然能够保证数据到达对端。下文中确认应答消息统称为ACK。
那么,这其中的许多细节需要我们确认,比如,网络通信中,接收方的ACK在返回时丢失,发送方进行重发,接收方如何避免重复接收?多久进行重发?是否一直重发?


二、连接管理:    


TCP在传输数据之前,需要先在线路中建立连接, 其实质就是先做好通信两端的准备工作。连接是指各种设备、线路,或网络中进行通信的两个应用程序为了相互传递消息而专有的、虚拟的通信线路,也叫做虚拟电路。所以,一旦建立了连接,进行通信的应用程序只使用这个虚拟的通信线路发送和接收数据,就可以保障信息的传输。

那么建立连接的过程需要“三次握手”,如下图:



手动抓包如下:    


首先,客户端发送SYN标志的连接请求,服务端响应,并返回针对连接请求的确认应答,然后客户端针对服务端的响应再一次确认应答。总共三次建立了连接。我是这么总结的,带有功能性质的一条传送都需要一个对应的ACK,客户端发起的SYN是建立连接功能,服务端返回的针对SYN的确认也是一种对连接响应的功能,同时包含了ACK。

TCP的结束需要“四次握手”,因为TCP的连接是双全工,即在线路上数据可以在两个方向上同时传递,所以TCP存在半关闭状态,即一方完成它的数据发送任务之后就能发送一个FIN来终止这个方向的连接,停止数据流动,另一方依然可以发送数据,所以想要全部结束TCP,需要两个方向发起关闭请求,再收到两个确认应答即可完成关闭操作。如下图:




三、超时重发:    


当发送方接收到ACK的时候说明数据已经到达了对端, 如果一段时间之后,没有接收到ACK,则说明数据丢失的可能性很大,但却不是一定丢失了,因为,也可能因为ACK丢失了,而数据实际已经到达了对端。那么所谓的一段时间,是多久呢,前提是这个时间能保证ACK在这个时间内一定能返回,但是,这个时间的长短又受到了网络环境的影响,而TCP的要求就是无论在何种网络环境下都能提供高性能的通信。

所以,下面我们来看看TCP如何判定超时的。查阅相关资料得知:
 
因为超时重发需要符合具体的网络场景,所以超时时间RTO(retransmission timeout)是根据往返时间RTT(round trip time)进行设置的,且RTO肯定是需要比RTT稍微大一点的,因为如果重发等待的时间小于网络上的往返时间,肯定会引起不必要的重发。如果RTO远大于RTT也不行,这样如果需要重发,就可能会浪费更多的时间在等待,导致网络的利用率降低。可见,RTO如何设置是影响着TCP的性能的。

由上述得知,RTO = RTT + △t ,其中△t 大于零,且不能太大。现在,需要求得RTT和△t即可。

  • 第一次进行连接时,并不知道往返时间,所以初始值为6秒左右。在BSD的Unix以及Windows系统中,都以500ms为单位进行控制,故重发超时都是500ms的整数倍。
  • 第一次连接,知道通信的RTT之后(根据时间戳进行相减),并不能直接使用,原因是防止RTT抖动太大,所以要加一个平滑因子。即乘以一个权值α,α的取值范围建议设置为0.1 ~ 0.2。
  • TCP每次都会根据上次的连接进行测量得到RTT,记为RTTnew,还有个全局变量即实际用于使用的RTT,记为SRTT,即平滑后的RTT,所以,TCP每次去更新SRTT即可,然后加上△t就能算出RTO。
  • 有公式SRTT=(1−α)×SRTT+α×RTTnew,假设α的值为0.125,没有更新之前的SRTT=200ms,最近一次的线路测量RTTnew=800ms,那么更新之后,SRTT= 200×0.875+800×0.125=275ms。
  • RFC 2988 规定 Δt=4×RTTD,而 RTTD=(1−β)×RTTD+β×|SRTT−RTTnew|,RTTD是均值偏差,表示了某个样本到总体平均值的距离,类似高中数学中的求方差。如何计算不再罗列。且 RFC 推荐 β=0.25。

由以上算法,TCP可动态的计算RTO,以适应各种网络环境。需要说明的是,在一次等待RTO之后重传,依旧没有收到ACK,那么下次的RTO将会翻倍,即 RTOn=2^(n−1)×RTO1, 依次进行R1次后停止,放弃该连接。或者超出R2秒后也放弃该连接,R1和R2在不同的系统中默认值不同,也可以进行设置。 

   

四、字节编码与分段传送:


如上,我们探讨了确认应答以及重发的机制,但是这里有个明显的漏洞。当作为接收方的服务器接收到数据之后,返回一个ACK给发送方,但是ACK由于网路堵塞迟迟没有被客户端接收到,过了重发超时时间就会触发重发机制,这个时候服务端就会接收大量重复相同的数据,而且,如果不对数据进行“标记”,服务端是无法知道哪些数据是重复的,因为相同的数据也可能是原始数据的一部分,这样就造成了数据的紊乱。所以需要给每次TCP传送的数据进行唯一标记,标记为特有的序列号。 


                                                        图片摘自书籍《图解TCP/IP》


如上图所示,序列号是按顺序给发送数据的每一个字节都标上号码的编号。假设每次传输1000个字节,那么第一次传输的序列号就是1~1000,而接收端在查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答号传回去,那么第一次返回的确认应答号应该是1001,即下次只接受从1001开始的数据。

而实际中,分段的大小,自然不是1000个字节,而是根据链路中最大能传送的数据大小决定的,在TCP三次握手建立连接的时候,会在SYN报文中使用MSS 选项,协商交互双方能接收的最大段长,而MSS = MUT - 20(ip首部) - 20(TCP首部),MUT即是最大传输单元的含义,在以太网中,最大传输单元是1500字节,如果两台主机要通过不同的网络进行通信,那么每个网络的链路层可能就有不同大小的MUT,而其中最小的MUT就是这条通信线路的MUT,叫做“路径MUT”,有技术“路径MUT发现”来获取路径MUT。故,MSS在以太网下,为 1500 - 20 -20 = 1460字节。如下图:


当TCP中的数据按照MSS进行分段之后,为了适应丢包重发的特性,发送方在发送数据之后,期待获得接收方的ACK,而接受方在接受到数据之后,根据接收到的序号,得到下一次接收期望获得的数据段的开始,放在ACK中,传给发送端。如图: 


   如上的传输逻辑又会带来一个新的问题,当TCP将大量的数据以MSS为单位分割成一段一段的数据段进行传送的时候,按照发送的顺序,每一段的数据都需要一个ACK,收到前一段的ACK之后,客户端进而发送下一段的数据。这样形成了一个排队阻塞的发送逻辑,当通信条件差的时候,数据往返的时间即网络的IO增加,就会对整个通信性能影响甚大。


五、滑动窗口机制


为了解决这个问题,TCP引入了窗口这个概念,加入TCP要传输的数据被MSS分为16个数据包,现假定每个窗口包含4个数据包,当这4个数据包进行发送的时候,不需要等待ACK,即同时并发传输4个数据包,接收方在接收了4个数据包之后,自然就返回4个ACK,发送方获得ACK之后,判断是否全部到达对端,然后重置窗口,第二次发送自然就从第5个数据包开始。也就是窗口从1 ~ 4 滑动到了 5 ~ 8 ,直到16个数据包全部发送完毕。如图: 
   


现定义一个窗口上的移动方向分别为,合拢、收缩、张开。如下:    


笔者用一段文字描述整个过程,在TCP根据MSS进行分段之后,假设分成了20段,本来是应该从第一段顺序发送到最后一段,现在有了窗口机制,可以提高并发能力了,假设窗口的大小为5,那么初始窗口下的内容就是1~5段的内容。在窗口下,每个发送数据的TCP均无需等待ACK,可以继续下一个TCP的发送,但必须是在窗口下。在发送前,发送方会将数据缓存,在没有收到ACK之前,缓冲区相应的数据包并不会被清除,如果超时没有收到ACK会进行重发。当分段1发送成功之后,窗口会进行合拢和张开操作,形成一个滑动的效果。


六、快速重发机制    


上面的滑动窗口在一定程度上解决了TCP发送数据包时阻塞的问题,但是由于TCP的重发机制,上述的滑动窗口还存在一个隐患,在数据被MSS分为很多个数据包的时候,窗口的大小决定其并发的效率,如果窗口过小,对TCP阻塞的问题并没有得到缓解,如果窗口太大,在网络环境不好的情况下,单个窗口下重发的概率增大,窗口下由重发造成阻塞也将破坏滑动窗口设计的初衷。

此时,自然就需要重新设计滑动窗口下的重发机制。接收方成功接收到数据包,就会将该数据包中对应的数据序号从缓存中清除,这个过程是按照序号顺序的,然后返回给发送端的ACK中标识期望下次接收数据的起始序号,引起重发的原因有二,一个是接收端收到数据,ACK包返回途中丢失,另一个就是接收端压根就没收到数据,自然也无法返回ACK。那么在窗口下,发送方在没有收到ACK不进行重发,而是等待下次的ACK返回的序号,如果三次发送方都接收了同一个序号的ACK,则对该序号为起始的数据进行重发。从发送方的角度来看,如果数据包已到达对端,而ACK在返回途中丢失时,上述新的处理逻辑不会立马重发,自然不会阻塞,下个数据段返回的ACK将这次丢失的ACK覆盖,发送方就可以断定,上个ACK已丢失,但是数据成功送达了,便可以将上个数据段标识为发送成功,且从发送方的缓存中清除。那么第二种情况,如果是数据丢失,的确没有到达接收方。此时,作为接收方,不影响接收其他数据段的内容,但是,由于某个数据包在传输过程中丢失,该数据包对应的缓存并没有清除,所以无论下次成功接收了那个数据包,都将丢失的数据包对应的序号作为ACK进行返回给发送方,告诉发送方,我“期待”接收某个数据包,提示三次,引起重发。这种方式,放弃了发送方被动等待超时,单方面重发的机制,提高了效率。这种算法也叫做“快速重传算法"。



七、流量控制


现在为止,上面的一系列设计可以解决大部分的问题。不过,发送方往往是根据自己的实际情况进行发送的,并没有考虑接收方的接收“能力”,如果接收方在高负荷的情况下,无法接收数据,就会触发重发机制,从而导致网络流量的浪费。故,TCP提供了一种流量控制的机制,可以让发送方根据接收方的实际能力来控制发送的数据量。在TCP首部中,会有一个专有字段用来通知窗口大小,接收方可以将自己能接收的缓冲区大小放入这个字段中通知给发送端。这个字段的值越大,说明网络的吞吐量越大。所以,窗口大小也是动态变化的,是有接收方的缓冲区大小而决定。其中,窗口更新通知如果在传输途中丢失会导致无法继续通信,为避免这种情况的发生,发送端会偶尔发送一个叫做窗口探测的数据段,这个数据段只包含一个字节用来获取最新窗口大小信息。    


八、拥塞控制


 有了窗口控制之后,TCP能够发送大量的连续的数据包。而网络是个共享的环境,在网线网络拥堵的时候,突然发送大量的数据包会致使网络瘫痪,所以定义了一个“拥塞窗口”,这个拥塞窗口初始设置为1MSS,即一个数据段大小,然后每收到一个ACK拥塞窗口就的大小就加一,且每与接收端主机通知的窗口大小进行比较,保证发送的量小于其中的最小值。但是,随着每次的往返,拥塞窗口也会以指数级别增加,导致拥塞的发生。所以为了防止这个情况,引入了一个阈值,叫做慢启动阈值,一旦拥塞窗口的值超过这个阈值,在每收到一次确认应答时,只会允许以某种比例放大拥塞窗口。    

注:TCP通信刚开始时,并没有设置相应的慢启动阈值。而是在超时重发时,才会设置为当时拥塞窗口一半的大小。

综上,为了确保数据到达对端,TCP引入了确认应答机制,且为了防止在通信中丢包,TCP引入超时重传机制,最大限度的保证数据到达接收方。TCP为了保证数据传输的有序性和唯一性,给数据包中的数据按照字节为单位进行序号的标注,从而每个字节都有自己的“序号”。为了提高传输效率,TCP根据线路最大“承载能力”将数据包分段传送,且避免传输大量数据的时候的阻塞,引入了窗口机制,以窗口大小为单位进行并发处理,提高了性能,且在窗口模式下,重新定了重传机制,从超时等待,变为接收方主动“提醒”,使网络环境不好的情况下,窗口模式中,不会被重传“拖后腿”,进一步提升性能。 而流量控制和拥塞控制,都是在特殊情况下TCP给出的解决方案。

至此,TCP为了实现其“可靠”性,逐步完善的特性,环环相扣。其可靠性体现在,让数据快速、无误、准确的到达接收方,还是那句话,在错综复杂的网络环境中,TCP扮演者一个靠谱的角色。


参考书籍:《图解TCP/IP》、《TCP详解券一》

超时重传时间计算方式摘自文章:https://www.jianshu.com/p/68afdcb98dfb

感谢。