1.TCP概述
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP提供全双工服务即A和B都可以主动给对方发送数据(可以和HTTP协议做对比,一般情况是只能客户端给服务器发送数据【H5新加入的websocket可以双向通信】)。TCP的控制信息存储在TCP报文的首部,其格式如下图所示
每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接(IP地址+端口号也即Socket)。
序号是32 bit的无符号数,序号到达232
-1后又从0开始。也就是说序号是用mod 232
运算的。TCP是面向字节流的,在一个TCP连接中传送的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。TCP报文首部中的序号就是此报文段所发送数据的第一个字节的序号。例如,一个报文段的序号字段值是1,共携带1440个字节数据数据,则此报文段的第一个字节序号为1,最后一个字节的序号为1440。显然下一个报文段(如果还有的话)序号应当从1441开始。序号是用来解决网络包乱序问题而确认号用于确认收到,用来解决丢包问题。如果发端在一定时间内没有收到收端的确认则会将数据重传。(需要注意的是确认号,许多文章都简单的介绍了确认号=发端序号+1,其实确认号是应当是发端数据最后一个字节的序号+1,还是上面的例子发送时序号=1,确认号会是1441,即确认号是和发送数据长度有关的)。
TCP首部的6个标志位分别是:
TCP标志位中:
ACK:TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1.
SYN:在连接建立时用于同步序号。当SYN=1 ACK=0时,表示这是一个连接请求报文,若对方同意建立连接,则响应报文中应使SYN=1 ACK=1.因此,SYN置1表示这是一个连接请求或连接接受报文。
FIN:即完成终结的意思,用来释放连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
窗口(Advertised-Window):与本文最后会介绍的滑动窗口有关,它用于传送过程中数据流的控制。
2.建立连接(TCP三次握手)
首先客户端发出连接请求,置TCP标志位SYN=1 ACK=0,TCP规定SYN=1时不能携带数据,但要消耗一个序号,因此声明自己的序号seq=x,发送连接请求后客户端状态由Closed变为SYN-SENT,此即第一次握手。服务器收到请求后发现SYN=1知道是一个连接请求,进行回复确认即置SYN=1 ACK=1 声明自己的序号seq=y,确认号为ack=x+1,此时服务器状态由LISTEN变为SYN-RCVD,此即第二次握手。客户端收到服务器的连接确认后,检查确认号ack是否正确即第一次发送的序号+1(x+1),以及标志位ACK是否为1,若正确,则向服务器发送确认包置标志位ACK=1 SYN=0,序号seq=x+1,确认号ack=y+1,发送确认信息后客户端状态由SYN-SENT变为ESTABLISHED,此即第三次握手。服务器收到确认信息后状态由SYN-RCVD变为ESTABLISHED,至此连接建立。(注:文中大写ACK表示TCP标志位,小写ack表示确认号)
为什么要进行三次握手呢(两次确认,服务器确认、客户端确认)?客户端的再一次确认是为了防止已失效的连接请求报文段突然又传送到服务器,从而产生错误。所谓"已失效的连接请求报文段"是这样产生的。考虑一种正常情况,客户端发出连接请求,但因连接请求报文丢失而未收到服务器的恢复确认。于是客户端重传一次连接请求,收到确认后建立连接,开始数据传输,数据传送完毕后释放了连接,此例中客户端共发送了两个连接请求,一个丢失一个到达服务器端。没有"已失效的连接请求报文段"产生。现假定出现一种异常情况,即客户端发出的第一个连接请求没有丢失而且在某些网络节点长时间滞留了,以致延误到连接请求释放以后的某个时间到达服务器。本来是一个早已失效的报文段,但服务器收到此失效报文段后,误认为是客户端又发出一个新的连接请求,于是向客户端发出回复确认,同意建立连接。假定不采用三次握手,那么只有服务器端发出确认,新的连接就建立了。由于现在客户端并没有发出建立连接的请求,因此不会理财服务器的确认,也不会向服务器发送数据。但服务器以为新的连接已经建立,并一直等待客户端发来数据,从而造成服务器资源的浪费,但采用三次握手的方法可以防止上述现象的发生。例如在刚才的情况下,客户端不会向服务器的确认再次确认,服务器由于收不到确认,就知道客户端没有要求建立连接。
3.断开连接(TCP四次挥手)
当客户端已经没有数据要发送时就要释放它的连接,此时它会发送一个报文(没有数据),置标志位FIN=1 ACK=1设置序号seq=u,确认号ack=v,此时状态由ESTABLISHED变为FIN-WAIT-1。服务器接收到信息后了解到客户端请求释放连接,但此时可能还有数据没有传送完成,所以先回复ACK报文,置ACK=1,序号为seq=收到的确认号=v,确认号ack=收到的序号+1=u+1,此时状态由ESTABLISHED变为CLOSE-WAIT。客户端收到服务端的ACK报文后状态由FIN-WAIT-1变为FIN-WAIT-2并继续等待服务器的FIN信号,此期间客户端还是可以接收服务端发送的数据的。当服务端所有数据传送完成后会发送FIN报文,置FIN=1 ACK=1,序号seq=w,确认号ack=u+1,此时状态由CLOSE-WAIT变为LAST-ACK。客户端收到服务端的FIN报文后会发送确认报文,置ACK=1,序号seq=u+1,确认号ack=w+1,此时状态由FIN-WAIT-2变为TIME-WAIT,之后客户端在等待2MSL(最大报文生命周期)的时间后会关闭,状态变为CLOSED。服务器收到客户端最后的确认信息后会直接关闭,状态变为CLOSED。(注意当一方主动关闭后会经历TIME-WAIT一段时间才可以关闭,被动关闭方收到最后确认报文后则直接关闭)。
综合以上内容总结TCP生命周期的状态转换图如下
4.滑动窗口
将TCP与UDP这样的简单传输协议区分开来的是它传输数据的质量。TCP对于发送数据进行跟踪,这种数据管理需要协议有以下两大关键功能:
可靠性:保证数据确实到达目的地。如果未到达,能够发现并重传。
数据流控:管理数据的发送速率,以使接收设备不致于过载。
要完成这些任务,整个协议操作是围绕滑动窗口确认机制来进行的。TCP通过滑动窗口机制检测丢包,并在丢包发生时调整数据传输速率。本文开始部分提到TCP首部有一个窗口字段即对应滑动窗口,这个字段是收端告诉发端自己还有多少缓冲区可以接收数据。于是发端就可以根据收端的处理能力来发送数据,而不会导致收端无法处理造成丢包现象。为了说明滑动窗口机制,首先我们来看一下TCP缓冲区的数据结构:
图中我们可以看到:
收端LastByteRead指向了TCP缓冲区中读到的位置(即缓冲区中此位置之前的数据已被消费),NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。
发端的LastByteAcked指向了被收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。
于是:
收端在给发端的ACK中会回报自己的AdvertisedWindow(写入本文开头提到的TCP首部的窗口字段) = MaxRcvBuffer – LastByteRcvd – 1;
而发端会根据这个窗口来控制发送数据的大小,以保证收端可以正常处理。
下面我们来看一下发送方滑动窗口示意图:
图中分为四个部分,分别是:(其中那个黑模型是发端窗口,当收到确认ACK后整个黑框会整体向右移动,因此成为滑动窗口)
1已发送并收到ack确认的数据
2已发送但还未收到ack确认的数据。
3未发送但收端已准备好接收的数据(即收端窗口大小-已发送但未收到确认的数据大小=即收端窗口当前可用大小)或者叫窗口中尚未发送数据。
4未发送收端也没有准备好接收的数据或者叫窗口外数据(发端要发送的内容但是接收方缓冲区已没有空间)。
下面是个滑动后的示意图(收到36的ack,并发出了46-51的字节。注意这是收端ACK中未改变AdvertisedWindow的情况):
下面我们来看一个收端控制发端的图示(收端改变AdvertisedWindow的情况):
由上图可知,发端的窗口大小是受收端窗口大小控制的,当收端收到数据但是数据没有被消费时,由于数据在缓冲区滞留,剩余可用空间减少,为控制流量收端将通知发端减小AdvertisedWindow的大小,发端收到信号后整体滑动窗体大小缩小,直至减少为0将不再发送数据。当TCP首部窗口字段减少到0后,发端将使用Zero Window Probe(0窗口探测)技术简称ZWP,发送ZWP包告诉收到我还keep-alive,收端收到ZWP包后会报告其窗口大小,如果若干次后窗口大小还是0,则TCP实现就会发RST把链接断掉。
滑动窗口是TCP协议中一个重点也是比较难掌握的概念,下面将插入一个动画演示来让大家加深理解。
http://www.exa.unicen.edu.ar/catedras/comdat1/material/Filminas3_Practico3.swf
最后本文引用了很多作者内容加上自己的理解,如有需要说明,我将及时处理。