TCP详解

TCP(Transmission Control Protocol)传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层协议。人如其名,通俗地说TCP就是对于传输、发送、通信进行控制的协议。

开始介绍TCP之前我们先来介绍一下连接,连接是指各种设备、线路,或者是网络中进行通信的两个应用程序为了互相传递消息而专有的、虚拟的通信线路,因此也可以把连接叫做虚拟电路。一旦连接成功建立,那么需要进行通信的应用程序只需要使用这条虚拟的通信线路发送并接收数据即可,应用程序并不需要担心这条线路是如何建立、断开以及保持的管理工作,这些都是TCP来进行控制的。

TCP的特点

为了通过IP数据报实现可靠性传输,就必须要考虑到在传输过程中,数据被破坏、丢包、重复以及数据包到达时的顺序混乱的问题,如果解决不了这些问题,那么无法保证传输的可靠性了,TCP通过检验和,序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现了可靠性传输。

在之前介绍TCP和UDP报文的时候,已经介绍过了检验和、序列号、确认应答的相关知识,这里就不再赘述,感兴趣的朋友可以点击这里查看。

TCP的超时重发机制

我们先来看一下什么时候需要TCP进行超时重发。

情形一:

当主机A发送的数据因为网络堵塞等原因丢失,没有发送给主机B,那么在经过一段特定的时间间隔后,主机A仍然没有收到主机B发送来的ACK,那么主机A就会将这些数据重新进行发送。

情形二:

主机A发送的数据到达了主机B,但是主机B的确认应答ACK由于网络原因在传输过程中丢失,主机A并没有收到主机B的ACK,在一段特定的时间间隔后,主机A认为数据没有发送到主机B(因为没有等来主机B的ACK),启动重传机制,重新给主机B发送数据。

如果是情形二这种情况,假如出现了最坏的情况,就是每一次主机B收到数据后返还给主机A的确认应答ACK都丢失了,那么主机A就会一直不断向主机B发送数据,主机B这里就会收到大量的重复数据,这时我们之前提到过的TCP报头中的序列号就排上了用场,通过序列号,TCP可以快速锁定重复数据,并对它们进行去重操作,将重复的数据包丢弃。

根据两种情形,其中共同的特点都是,在一段特定时间间隔后,发送端没有收到接收端的确认应答ACK,才启动的重传,那我们如何确定这个特定的时间间隔就是TCP超时重传机制的重点了。确定这个时间间隔需要考虑:

  • 最理想的情况下,应该找到一个最小的时间段,并且保证ACK在这个时间段内一定可以返回
  • 但是这个时间段的长短,随着数据包传输的网络环境变化,是不同的,好比公路一样,堵车的地方走得慢,没车的地方走得快。
  • 如果时间段设置的太长,那么网络通信的大量时间会浪费在发送端等待ACK上面,拉低整体网络通信的效率。
  • 如果时间段设置的太短,可能ACK还没到,发送端就启动了超时重传机制,频繁向接收端发送重复数据。

TCP要求不论在何种网络环境下都提供高性能的通信,并且无论网络拥堵情况发生何种变化,都要保持这一特性。因此TCP在每次发包的时候都会计算往返时间(RTT,Round Trip Time)及其偏差(RTT波动的值、方差。也叫抖动)。将往返时间和偏差相加,超时重传的时间间隔就是比这个总和再稍微大一点的值。

在Linux、BSD Unix以及Windows系统中,超时都是以0.5秒即500毫秒为单位进行控制,因此每次判定是否需要超时重传的超时时间都是500ms的整数倍。如果重发一次后,仍然得不到应答,那么就等待2 * 500 ms后再进行重传,如果还是得不到应答,则继续等待4 * 500 ms后进行重传,以此类推,以指数形式递增。不过数据也不会是无限、反复地重新发送,当重传达到一定次数后,发送端如果还没有收到接收端的ACK,那么TCP会判定当前网络或者对端主机发生异常,会强行关闭TCP连接,并通知应用程序通信异常终止。

TCP的连接管理功能

连接管理包括建立连接以及断开连接,我们都知道TCP建立连接与断开连接被形象地描述为三次握手、四次挥手,下面是将应用层程序一同加入的三次握手四次挥手示意图

上图中服务器端的状态转化:
  • CLOSED -> LISTEN 服务器调用listen( )后进入LISTEN状态,等待客户端连接。
  • LISTEN -> SYN_RCVD 一旦监听到连接请求(同步报文段)就将该连接放入内核等待队列中,并向客户端发送SYN确认报文。
  • SYN_RCVD -> ESTABLISHED 服务器端一旦收到客户端的ACK,就进入ESTABLISHED状态,即连接建立,可以进行数据读写。
  • ESTABLISHED -> CLOSE_WAIT 当客户端主动关闭连接(调用close),服务器会收到结束报文段(FIN)服务器会返回确认报文段(ACK)并进入CLOSE_WAIT。
  • CLOSE_WAIT -> LAST_ACK 进入CLOSE_WAIT后说明服务器准备关闭连接,需要将之前的数据全部处理完毕,当服务器来调用close关闭连接是,会再向客户端发送一个结束报文段(第二个FIN),此时服务器进入LAST_ACK状态,等待最后一个ACK。
  • LAST_ACK -> CLOSED 服务器收到了客户端对于FIN的ACK,彻底关闭连接,完成四次挥手
上图中客户端的状态转化
  • CLOSED -> SYN_SENT 客户端调用connect,发送同步报文段SYN。
  • SYN_SENT -> ESTABLISHED connect调用成功,进入ESTABLISHED状态,开始读写数据。
  • ESTABLISHED -> FIN_WAIT_1 客户端主动调用close时,向服务器发送结束报文段FIN,并进入FIN_WAIT_1阶段。
  • FIN_WAIT_1 -> FIN_WAIT_2 客户端收到服务器对FIN的ACK,则进入FIN_WAIT_2状态,开始等待服务器发送来的FIN。
  • FIN_WAIT_2 -> TIME_WAIT 客户端收到服务器发来的FIN,进入TIME_WAIT,并发送LAST_ACK。
  • TIME_WAIT -> CLOSED 客户端需要等待一个2MSL(Max Segment Life,报文最大生存时间),才会进入CLOSED状态

再谈谈TCP中的三次握手四次挥手

四次挥手中双端的状态变化(建议结合图示阅读)

在图示以及上面已经详细解释了四次挥手过程中服务器端和客户端的状态变化。客户端先发出关闭请求,发送FIN,刚发送出来,客户端立刻进入FIN_WAIT_1状态;当服务器收到FIN,并发送出ACK,在ACK发出的瞬间,服务器进入CLOSE_WAIT状态。到这里,认为客户端向服务器方向的通信信道关闭,以后客户端不会再向服务器发送数据了,这里不再发送数据相当于客户端应用层用于网络通信的文件描述符被close了,应用层的数据无法再发送了。当客户端收到服务器的ACK后,进入FIN_WAIT_2状态。服务器会再向客户端发送一条FIN,发送后服务器立马进入LAST_ACK状态;当客户端收到该FIN后,立马发送ACK完成四次挥手的最后一次挥手,并进入TIME_WAIT状态 ,当服务器收到客户端发来的ACK后,立马CLOSED,即断开连接。如果服务器端不调用close(connetcfd),那么永远不会主动向客户端发送FIN,即不会进入LAST_ACK状态而一直停留在CLOSE_WAIT状态,因此如果查看服务器网络状态,发现挂满了CLOSE_WAIT状态的连接,我们就需要查看一下是不是服务器端代码忘记在 最后调用close( )方法而导致服务器不能主动发送FIN完成四次挥手。

主动断开连接的一方需要进入TIME_WAIT状态进行等待,虽然通常客户端是主动断开连接的一方,但是TCP是全双工通信,所以服务器也可以主动断开连接。TIME_WAIT的意义就是保证四次挥手最后一次挥手的ACK被对方收到,如果不进行等待直接进入CLOSED状态断开了连接,那么如果最后一条ACK对方没有收到,而重新发送了FIN,但是本端已经关闭了连接。就会造成连接断开不彻底的后果。同时TIME_WAIT可以等待历史数据在网络上消散,即可能断开连接的报文先于数据报文抵达了对方, 就会有一部分数据停留在网络中无法抵达对端。通常TIME_WAIT的时间被设置为2 * MSL。

为什么一定要是三次握手?

一次握手,相当于客户端发送SYN就直接建立连接,那与UDP并没有什么区别,根本谈不上可靠性连接,同时如果是一次握手,那么我们可以雇佣一些客户端,同一时段大量向服务器发送连接请求,既然是一次握手,那么只要客户端发送请求,就会建立连接,这样服务器会在一瞬间挂满大量的连接,就直接宕机了。

两次握手,相当于客户端发送SYN,服务器返回ACK就建立连接,那么如果客户端发送的SYN在网络中阻塞了,过了一段时间,还没收到ACK,于是客户端又第二次发送了SYN,服务器返回ACK,建立连接,并完成了数据传输,最后关闭连接。此时,之前被阻塞的第一个SYN历经千辛万苦终于到达了服务器,它本该失效,但是又唤起了一次连接,就导致了连接错误与网络资源的浪费。

三次握手,客户端和服务器都至少有一次作为发送方,有一次作为接收方,TCP是一个全双工通信,要求双方是都能发数据也能接数据的,所以三次握手可以用最小的成本验证双方的全双工通信信道。同时可以解决一次握手和二次握手所展现出来的问题。

四次握手相当于在三次握手的基础上,让服务器再给客户端发送一条ACK,我们注意到,不论是客户端还是服务器,只要是发送最后一条ACK的端,发送出去就会认为连接已经建立好了,此时如果最后一条ACK,即四次握手中最后服务器发送给客户端的ACK丢失了,那么后果就是服务器发送出这条ACK就认为连接建立完成,在等待客户端给它发送请求,而客户端接收不到ACK,认为连接还没有建立,要等待ACK到达后再发送请求,那么就会一直在浪费服务器的网络资源,我们要知道服务器是给n台客户端提供服务的,如果有大量的上述情况出现,就会导致服务器无法为其他客户端提供服务,因此我们一定要让最后一条ACK由客户端发送出来,即优先让客户端认为连接成功建立。这样最后一条ACK如果丢失的话,只会是客户端在等待连接,不会让服务器出现连接建立的误判情况,从而不会浪费服务器的资源。

滑动窗口

TCP会以段为单位,每发一个段就进行一次确认应答处理,但是这样的传输方式,如果数据包在网络中往返的时间越长,网络通信的效率就会越低。

为了解决上面的问题,TCP中引入了滑动窗口的概念。有了滑动窗口后,即使数据包在网络中往返时间较长,也可以保证网络通信效率。这种模式下ACK不再是对每个数据段分别确认,而是发送端主机在发送了一个段后不必一直等待ACK,它会继续发送。

滑动窗口的大小指的是不需要等待ACK到达,可以继续发送的数据段的大小,以上图为例,图中的窗口大小就是4000字节,即四个段。通过图示可知,主机A在发送前四个段的时候,不用等待主机B的ACK,就可以连续发送。

当主机A收到了第一个ACK之后,滑动窗口向后滑动,继续发送第五个段的数据包,并以此类推,这个滑动窗口是由操作系统内核来进行维护的,其作用就是在发送缓冲区中记录当前还有哪些数据没有收到ACK,只有被ACK确认过的额数据才能从缓冲区中删掉,这样就可以保证如果出现重传需求,发送端可以立刻在发送缓冲区的未确认部分找到历史数据,直接发送。而不用担心历史数据遗失的问题了。

滑动窗口越大,网络的吞吐率也就越高。

滑动窗口机制设计的非常巧妙,可以用顺序的方式将多个段同时发送提高通信的性能,假如上图中ACK 2001在网络中阻塞或者丢失,我们最后只收到了ACK 5001,那我们还需要重传2001~3000这个数据段吗?答案明显是不用的,因为TCP的确认应答机制明确了ACK之前编号的数据一定已经收到,因此如果我们直接收到了ACK 5001,那在上图中,我们就可以直接将滑动窗口向右移动到5001,并继续将窗口中的数据段连续发送出去!

因为滑动窗口的存在,将整个发送缓冲区分成三个部分,分别是已经发送且收到ACK的数据,已经发送还未收到ACK的数据,和还没有发送的数据,已经收到ACK这部分的数据就可以被清除了,不过缓冲区是开辟在计算机内存中的一部分空间,其容量是有限制的,这就要求滑动窗口机制可以重复使用之前用过的空间,所以发送端的缓冲区我们可以认为是一个环形队列。

那么前文介绍的TCP超时重传机制,在滑动窗口这里,该怎么使用呢,我们一样分两种情况来讨论,其实可以启用TCP重传机制的情形也只有这两种:①ACK丢失;②报文段丢失

情形一:

数据包发送到对端,但是对端返回的ACK在网络传输中丢失,这种情况其实我们刚才已经解决过了,那就是可以通过后续的ACK进行确认,TCP的确认应答机制保证ACK之前的所有数据都已经成功接收,因此即使ACK 2001丢失,如果我们后续收到了ACK 3001、ACK 4001等,就可以确认前面的数据段已经送达,滑动窗口直接移动到收到的ACK处即可。

情形二:

数据段丢失,下面的例子假设1001~2000这个数据段在传输过程中丢失,那么发送端会收到接收端的“ACK轰炸”,即接收端会一直发送ACK 1001,像是在提醒发送端,它想要的是从1001开始的数据。如果在窗口比较大并且报文段丢失的情况下,同一个序号的ACK会被重复返回。一旦发送端连续三次收到了同一个序号的ACK,就会将这个ACK所对应的数据进行重发,这种重发机制会比上面我们提到的超时重发机制更加高效,因为它不用等待,只要三连收到同一个序号的ACK,就立马重发数据,因此这种机制也被称为高速重发控制(快重传)。不过注意它是与超时重发机制互补的,两者并不冲突。

流量控制

说完了发送端,再来看看接收端,接收端处理数据的速度是有限的。如果发送端发送的太快,导致接收端的接收缓冲区被填满了,这个时候如果发送端还继续发送数据,就会造成丢包,又因为TCP是可靠性连接,所以后面又会引发丢包重传的连锁反应,因此TCP作为传输控制协议,它也要根据接收端的处理数据能力来控制一下发送端的发送速度。这种机制就是流量控制。

之前介绍TCP报文首部的时候,就提到过TCP首部中有一个“窗口大小”的字段,这个字段是接收端通过ACK报文返还给发送端的。而接收端则会根据这个字段来设置自己的滑动窗口大小,也就是说发送端的滑动窗口大小一定不能超过接收端的窗口大小,这就把双方连接起来了!

窗口大小这个字段的值越大,说明网络的吞吐量越大,如果接收端的缓冲区快要被填满了,这个字段就会被重置,变为一个更小的值在后续的ACK中返还给发送端,提醒发送端发慢一些,这样就形成了一个完整的TCP流量控制体系。

上图在接收端收到3001~4000数据段时,自己的接收缓冲区就满了,因此它在ACK中不仅告知了发送端下一次数据要从4001开始发送,同时也在窗口大小字段中标注出,自己的剩余大小为0,这时发送端就不会再发送数据了,而是等待接收端的应用层软件处理数据,清理接收端缓冲区。如果在超时重传的时间间隔中,接收端没有发送过来一个窗口大小的更新通知,那么发送端就会主动发送一个窗口探测报文,接收端收到这个窗口探测报文后,返还一个ACK,告知发送端下一次从哪里开始发送数据,并携带自己的窗口大小。当接收端的缓冲区有空闲的空间后,接收端会主动向发送端发送一个窗口更新通知,发送端收到这个窗口更新通知后,就会根据更新后的窗口大小,继续有控制地发送自己的数据。当然,这个窗口更新通知在网络传输中也有可能丢失,因此,发送端的窗口探测不止一次,而是每隔一个超时重传的时间间隔就主动发送一次。

拥塞控制

自从有了上述介绍的滑动窗口之后,TCP传播的效率大大提升,不再是一个个单独发送数据段,而是连续大量发送数据段,但是如果刚刚建立了连接就大量发送数据,很有可能引发其它问题。

计算机网络是一个共享的环境,上面连接着许多的计算机,就好比一条高速公路,大家都可以在上面行驶,如果某一时间已经堵车了,但还有大量的汽车驶上该条公路,那么就会引发整条公路的交通陷入瘫痪。计算机网络也是如此,如果网络环境已经比较拥堵,仍然发送大量的数据段,是非常危险的行为。因此TCP为了防止这种问题的出现,在通信一开始的时候会采取慢启动的机制,先探查一下当前的网络状况,再确定按照多大的速度传输数据。

为了在发送端可以进行调节发送数据的量,定义了一个**“拥塞窗口”**的概念,在慢启动开始时,拥塞窗口在发送开始的时候大小设置为1,每次收到一个ACK应答,拥塞窗口就翻倍,这样就实现了指数级增长,每次发送数据的时候,会将拥塞窗口和接收端的窗口大小字段进行比较,取两者中更小的那个作为发送端的滑动窗口大小

慢启动只是初始的时候比较慢,但是增长速度较快,其拥塞窗口的增长速度是指数级别的,但是指数增长是在是太快了,所以我们需要规定一个阈值,当拥塞窗口超过阈值后,不再以指数方式增长,而是以线性方式增长。当遇到网络阻塞后,将拥塞窗口置为1,并将阈值设置为上次出现拥塞情况时拥塞窗口大小的二分之一(现在已经不再使用了),目前使用的是快速回复,直接将拥塞窗口置为上次出现拥塞情况时拥塞窗口大小的二分之一。

总结一下,TCP协议正如它的名字一样,传输控制协议,不仅要建立、断开和保持传输所用的连接,而且要在传输过程中进行控制,不可以发的太慢,也不可以发的太快。所以TCP的可靠性为网络通信提供了安全,而TCP的对于连接的控制则大大提高了网络传输的效率。
Logo

长江两岸老火锅,共聚山城开发者!We Want You!

更多推荐