TCP工作在网络协议栈的传输层,在这一层上传输的数据叫段(Segment)

我们应用程序的数据会先打包到传输层,传输层再交给下层网际层,再交给下层数据链路层

wKioL1c5ZEKwVSvcAABLo38OEe0037.png

上图中有四个东西是非常重要的:

序号:用来解决数据包在网络传输中不乱序问题

确认号:解决不丢包问题

TCP flag(URG、ACK、PSH、RST、SYN、FIN):就是包的类型,用于操控TCP状态机

窗口:用来解决流量控制的(即滑动窗口)


TCP连接:

               client和server的TCP建立连接

        wKioL1c9VPDzx890AAEGTYMnrz4895.png


对于建链接的3次握手,

1.要使每一方能够知道对方的存在

2.要允许双方协商一些参数

 (1)主要是要初始化序号的初始值。通信的双方要互相通知对方自己的初始化的序号(Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据

 (2)最大窗口值

 (3)是否使用窗口扩大选项(占3个字节,其中有1个字节表示移位值S,新的窗口值等于TCP首部中的16位窗口位数加上S,即(16+S),移位值允许的最大值为14,相当于窗口最大值增加到2^(16+14)-1=2^30-1)

    是否使用时间戳选项(计算往返时间RRT;处理TCP序号超过2^32的情况,会在报文中加上这种时间戳)

3.能够对运输实体资源进行分配(如缓存大小)


一个TCP连接需要四个元组来表示是同一个连接,即TCP连接::={socket1,socket2}={(IP1:port1),(IP2:port2)}

其中套接字socket=(IP地址:端口号)

每一条TCP链接唯一地被通信两端的两个端点(即套接字)所确定。


发送第一个 SYN 的一端将执行主动打开,接收这个 SYN 并发回下一个 SYN 的另一端执行被动打开。另外, TCP 的握手协议被精心设计为可以处理同时打开,对于同时打开它仅建立一条连接而不是两条连接。因此,连接可以由任一方或双方发起,一旦连接建立,数据就可以双向对等地流动,而没有所谓的主从关系。(在连接建立之后 TCP 就不再理睬又一次的连接请求),TCP 连接建立起来后,就可以在两个方向传送数据流。


需要注意的一个问题,为什么要有第三次握手?

TCP连接采用的是客户端服务器端模式,主动发起连接的应用进程是客户端,被动等待连接请求的应用进程是服务器

假设主机A是客户端,主机B是服务器端,当A向B发出的连接建立请求因为某些原因(比如在网络中某个节点滞留的时间长了)而使B并没有收到该请求,但该报文并没有丢失,超时后A会重发请求建立连接,B此时收到并和A建立了连接,数据传输完毕后连接会关闭,B在这之后收到了之前发的第一个建立连接请求(我们称之为已经失效的连接请求报文段),B此时会认为A又一次发起了连接请求,所以会向A发送确认报文段,如果此时没有第三次握手,B就会认为连接建立好了,就会等待A发送数据,但A并不会发送数据,这样B就白白浪费了资源,但如果有第三次握手,B向A发送确认报文段时,A并不会理睬B的确认报文,此时,B就不会认为连接已建立并等待A发送数据了


还要注意的一点是:

客户端向服务器端发出建立连接请求的时候,SYN=1,同时选择一个序列号seq=x,TCP规定SYN报文段(即SYN=1的报文段)不能携带数据,但同样要消耗一个序列号,B收到连接请求报文后,如果同意连接,就向A发送确认,在确认报文中,SYN和ACK都置1,确认号是ack=x+1同时也为自己选择一个序列号seq=y,这个报文也不能携带数据,同样消耗一个序列号,此时客户端收到服务器端的确认后,要进行第三次握手的时候,ACK同样置1,确认号ack=y+1,序号seq=x+1,,TCP规定ACK报文可以携带数据,如果不携带则不消耗序列号,下一个数据报文段的序号仍是seq=x+1



                  client和server的TCP数据传输

         wKiom1c9VAaTytn4AAC94FBXdPY579.png



TCP断开连接:

                client和server的TCP连接释放

       wKioL1c9VPKyJqgkAAElg4SI9mo076.png

   当 TCP 的应用进程再没有数据需要发送时,就发关闭命令。 TCP 通过发送控制位 FIN=1 的数据片来关闭本方数据流(TCP规定,FIN报文段也要消耗一个序列号),但还可以继续接收数据,直到对方关闭那个方向的数据流,连接就关闭。

   终止一个连接要经过 4 次挥手,这是因为 TCP 的半关闭( half-close )造成的。由于一个 TCP 连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。关闭的原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接。当一端收到一个 FIN ,它必须通知应用层另一端已经终止了那个方向的数据传送。发送 FIN 通常是应用层进行关闭的结果。

如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。


主动发起释放连接的一方会进入到TIME_WAIT(时间等待)状态,现在TCP连接还没有被释放掉,必须经过2MSL(最长报文寿命)时间后,A才进入CLOSED状态。


设置TIME_WAIT的原因:

为了确保客户端发送的最后一个确认报文被B收到了,这个确认报文有可能会丢失,服务器端就收不到这个报文就会超时重传这个FIN+ACK报文,客户端此时会重启2MSL计时器,客户端重新向服务器端发送确认报文,当服务器端收到了这个最后的确认报文,客户端和服务器端都会正常进入CLOSED状态,否则,没有TIME_WAIT的话,服务器端就有可能无法正常进入CLOSED状态


TCP还设有一个保活计时器如果客户端在连接上服务器后主机出故障了,那么服务器就无法在收到客户端发送的数据了,但服务器不能一直等待下去吧?这样会浪费很多资源

服务器端每次收到客户端的数据后,都会更新一次保活计时器,时间的设置通常是两个小时,若两个小时后没有收到客户端的数据,就发送一个探测报文段,每隔75分钟发送一次,若连发10次客户端没有响应则认为客户端退出了,服务器端释放该连接



从一方的 TCP 来说,连接的关闭有三种情况:

1. 对方启动关闭

   当 TCP 收到对方发来的 FIN 报文时,发 ACK 确认此 FIN 报文,并通知应用进程连接正在关闭。应用进程将以关闭命令响 应。 TCP 在发送完尚未处理的报文段后,发一个 FIN 报文给对方 TCP ,然后等待对方对 FIN 的确认,收到确认后关闭连接。若对方的确认未及时到达,在等待一段时间后也关闭连接。

2.本方启动关闭

   收到本方应用进程的关闭命令后, TCP 在发送完尚未处理的报文段后,发 FIN = 1 的报文段给对方,且 TCP 不再受理本方应用进程的数据发送。在 FIN 以前发送的数据字节,包括 FIN ,都需要对方确认,否则要重传。注意 FIN 也占一个顺序号。一旦收到对方对 FIN 的确认以及对方的 FIN 报文段,本方 TCP 就对该 FIN 进行确认,在等待一段时间,然后关闭连接。等待是为了防止本方的确认报文丢失,避免对方的重传报文干扰新的连接。

3. 双方同时启动关闭

   连接双方的应用进程同时发关闭命令,则双方 TCP 在发送完尚未处理的报文段后,发送 FIN 报文。各方 TCP 在 FIN 前所发报文都得到确认后,发 ACK 确认它收到的 FIN 。各方在收到对方对 FIN 的确认后,同样等待一段时间再关闭连接。这称之为同时关闭( simultaneous close )。


TCP 状态机:

其实,网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。TCP的两端分别维护一个状态机,TCP连接的主动发起/被动发起和主动关闭/被动关闭的状态机转换是不同的。 

 TCP 协议的操作可以使用一个具有 11 种状态的有限状态机( Finite State Machine )来表示。

                                    以下是TCP状态表

  wKioL1c62sTgqAkCAABUiX9Cz9w047.png

每个连接均开始于 CLOSED 状态。当一方执行了被动的连接原语( LISTEN )或主动的连接原语( CONNECT )时,它便会脱离 CLOSED 状态。任何一方均可以首先请求释放连接,当连接被释放后,状态又回到了 CLOSED 。


                 TCP有限状态机


  • wKiom1c62f_B9p2yAAHvyqXvmd8617.png

  •        

  • 上图可对照本文第二张图进行分析

1. 正常状态转换

  • 服务器端首先执行 LISTEN 原语进入被动打开状态( LISTEN ),等待客户端连接;

  • 当客户端的一个应用程序发出 CONNECT 命令后,本地的 TCP 实体为其创建一个连接记录并标记为 SYN SENT 状态,然后给服务器发送一个 SYN 报文段;

  • 服务器收到一个 SYN 报文段,其 TCP 实体给客户端发送确认 ACK 报文段同时发送一个 SYN 信号,进入 SYN RCVD 状态;

  • 客户端收到 SYN + ACK 报文段,其 TCP 实体给服务器端发送出三次握手的最后一个 ACK 报文段,并转换为 ESTABLISHED 状态;

  • 服务器端收到确认的 ACK 报文段,完成了三次握手,于是也进入 ESTABLISHED 状态。

  在此状态下,双方可以自由传输数据。当一个应用程序完成数据传输任务后,它需要关闭 TCP 连接。假设仍由客户端发起主动关闭连接。

  • 客户端执行 CLOSE 原语,本地的 TCP 实体发送一个 FIN 报文段并等待响应的确认(进入状态 FIN WAIT 1 );

  • 服务器收到一个 FIN 报文段,它确认客户端的请求发回一个 ACK 报文段,进入 CLOSE WAIT 状态;

  • 客户端收到确认 ACK 报文段,就转移到 FIN WAIT 2 状态,此时连接在一个方向上就断开了;

  • 服务器端应用得到通告后,也执行 CLOSE 原语关闭另一个方向的连接,其本地 TCP 实体向客户端发送一个 FIN 报文段,并进入 LAST ACK 状态,等待最后一个 ACK 确认报文段;

  • 客 户端收到 FIN 报文段并确认,进入 TIMED WAIT 状态,此时双方连接均已经断开,但 TCP 要等待一个 2 倍报文段最大生存时间 MSL ( Maximum Segment Lifetime ),确保该连接的所有分组全部消失,以防止出现确认丢失的情况。当定时器超时后, TCP 删除该连接记录,返回到初始状态( CLOSED )。

  • 服务器收到最后一个确认 ACK 报文段,其 TCP 实体便释放该连接,并删除连接记录,返回到初始状态( CLOSED )。

    wKioL1c62w3QCxHfAACHjfw8t6Y328.png

2. 同时打开:

   尽管发生的可能性极小,两个应用程序同时彼此执行主动打开的情况还是可能的。每一方必 须发送一个 SYN ,且这些 SYN 必须传递给对方。这需要每一方使用一个对方周知的端口作为本地端口。例如,主机 A 中的一个应用程序使用本地端口 7777 ,并与主机 B 的端口 8888 执行主动打开。主机 B 中的应用程序则使用本地端口 8888 ,并与主机 A 的端口 7777 执行主动打开。 TCP 是特意设计为了可以处理同时打开,对于同时打开它仅建立一条连接而不是两条连接(其他的协议族,最突出的是 OSI 传输层,在这种情况下将建立两条连接而不是一条连接)。

  当出现同时打开的情况时,两端几乎在同时发送 SYN ,并进入 SYN_SENT 状态。当每一端收到 SYN 时,状态变为 SYN_RCVD ,同时它们都再发 SYN 并对收到的 SYN 进行确认。当双方都收到 SYN 及相应的 ACK 时,状态都变迁为 ESTABLISHED 。下图显示了这些状态变迁过程。

wKioL1c62yaRKbNhAABNNJv808M981.png

一个同时打开的连接需要交换 4 个报文段,比正常的三次握手多一个。此外,要注意的是我们没有将任何一端称为客户或服务器,因为每一端既是客户又是服务器。

3. 同时关闭:

  正常情况下都是由一方(通常但不总是客户方)发送第一个 FIN 执行主动关闭,但双方都执行主动关闭也是可能的, TCP 协议也允许这样的同时关闭。

   当两端应用层同时发出关闭命令时,两端均从 ESTABLISHED 变为 FIN_WAIT_1 。这将导致双方各发送一个 FIN ,两个 FIN 经过网络传送后分别到达另一端。收到 FIN 后,状态由 FIN_WAIT_1 变迁到 CLOSING ,并发送最后的 ACK 。当收到最后的 ACK 时,状态变化为 TIME_WAIT 。下图总结了这些状态的变化,从图中可以看出同时关闭与正常关闭使用的报文段交换数目相同。

wKiom1c62lTTwwnAAABUo39z4Ag141.png


  • 关于建连接时SYN超时

  • 如果server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。

  • 关于ISN的初始化

  • ISN是不能hard code的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。

  • 关于 MSL 和 TIME_WAIT

  • 我们注意到,在TCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)

  • 关于TIME_WAIT数量太多

  • TIME_WAIT是个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。

部分来自:http://www.cnblogs.com/huntaiji/p/4043967.html



《完》