TCP连接的状态转换

2020年10月23日 / 11次阅读 / Last Modified 2020年10月23日
socket

要彻底搞明白tcp网络编程及相关配置,就需要把tcp状态转换的每个细节搞清楚。

本文详细介绍TCP连接的三次握手,四次挥手,2MSL,TIME_WAIT状态等概念。下图去掉CLOSED,刚好是TCP的11个状态:

TCP的状态转换图
TCP的状态转换图

这个图来自RFC793,第23页。

TCP传输的可靠性,是通过对每个传输字节的seq进行确认和checksum来保证的。

三次握手

  1. 客户端发送一个带SYN标志的TCP报文到服务器,这是三次握手过程中的报文1。
  2. 服务器端回应,这是三次握手中的第2个报文,这个报文同时带ACK标志(确认客户端发的SYN)和SYN标志(给客户端发自己的SYN)。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。
  3. 客户必须再次回应服务段一个ACK报文(确认服务器端的SYN),这是报文段3。

客户端状态: SYN_SENT ----> ESTAB。

客户端报文: 发送SYN ----> 接收ACK和SYN ----> 发送ACK。

服务器端状态: LISTEN ----> SYN_RCVD ----> ESTAB。

服务器端报文: 接收SYN ----> 发送ACK和SYN ----> 接收ACK。

客户端会先于服务器端一点点,进入ESTAB状态。

SYN报文中含有一个序列号SEQ,ACK报文中含有SEQ+1。

四次挥手

服务器端的TCP,其实就是不断地生产与客户端一样的TCP,因此在ESTAB状态下,任何一方都可以首先离开,发FIN。下面是一个TCP关闭流程说明:

  1. 主动关闭连接的一方(A),调用close(),协议层发送FIN包 ,进入FINWAIT-1状态;
  2. 被动关闭的一方(B)收到FIN包后,协议层回复ACK;然后B进入CLOSE_WAIT状态;
  3. A收到ACK后进入FINWAIT-2状态;此时,A等待B方的应用程序,调用close操作 ;
  4. B在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给A,等待A的ACK,B进入LAST_ACK状态;
  5. A收到FIN包,协议层回复ACK;此时,A进入TIME_WAIT状态;而B进入CLOSED状态 ;
  6. 等待2MSL时间,A结束TIME_WAIT,进入CLOSED状态 ;

因此,首先发起关闭TCP的一方,最终会进入著名的TIME_WAIT状态!

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

我们说的四次挥手,具体是那四次呢?

其实就是双方格式放出的FIN报文和双方给对方的ACK报文,加起来一共4个报文。

主动关闭连接的一方A的状态变化: ESTAB ----> FINWAIT-1 ----> FINWAIT-2 ----> TIME_WAIT ---->CLOSED

A的报文:发送FIN ----> 收到ACK ----> 收到FIN ----> 发送ACK

被动关闭连接的一方B的状态变化:ESTAB ----> CLOSE_WAIT ----> LAST-ACK ----> CLOSED

B的报文:收到FIN ----> 发送ACK ----> 发送FIN ----> 收到ACK

由于网络传输,A有可能先收到FIN,在收到ACK,所以有了进入 CLOSING 状态,然后跳过 FINWAIT-2 状态,直接到 TIME_WAIT状态的转移路径!

TIME_WAIT状态的作用

要先记住,主动发起FIN的那一方(A),才会进入这个TIME_WAIT状态。主动发FIN,就意味着自己的数据传输已经完成了,后面不会再发送数据了!

此时的B,虽然收到了FIN,也发出了ACK,B的数据可能还没有发完,它还在继续发送数据。B要发完数据后,才会发出自己的FIN,进入LAST-ACK状态。

(?我理解为socket的send函数还没有返回时,协议层已经收到并处理了对方的FIN报文,而send将返回0,需要主动调用close函数发FIN;或者是异步的情况,send还没结束,别的线程发起了FIN,send返回0)

由于网络传输的各种不确定性,在TIME_WAIT状态下,虽然占用了一组(ip:port)暂时不能被系统回收再利用,但是确保了新的连接(继续使用相同的ip和port)不会收到旧连接在网络上重传的报文。

seq=3的报文,在上图场景下,会被新的连接错误的接收(此时正好seq=3能够跟其它报文的seq对应上,如果seq对不上,会被RST)。

上图是收到错误的数据包的情况,下图是收到错误类型的报文:

B发送FIN后,一直收不到ACK(丢了),此时如果正好A端复用了(ip:port)来简新的连接,A会发送SYN,B收到后认为这是个错误的报文,直接RST!

因此,TIME_WAIT状态其实就是等待网络出清,规避各种可能因为网络传输的不确定性造成的问题。

  • 确保对方收到自己发送的最后一个ACK(因为对方发送了FIN),如果对方没有收到自己发送的ACK必定会重新发送FIN,这样保证4次断开的完整性。因为MSL是最大报文生存时间,如果在1个MSL时间内自己发送的ACK对方没有收到那就注定收不到了,而且对方肯定还会发送FIN,那么一个FIN发送过来的最长时间也是1个MSL,所以这里要等待2MSL。
  • 另外一个原因就是避免延迟的IP报文,在频繁短连接的场景下客户端通常会对同一个IP和端口在短时间内发起多次连接,而客户端使用的端口是自己系统随机分配的高位端口,有一定概率发生上一个socket四元组和下一个socket四元组一样,如果这时候一个原本属于上一个socket四元组的被延迟的IP报文送达,那么这将发送数据混乱的状态,所以为了避免这种情况就利用MSL这个报文最大生存时长机制让残余的IP报文在网络中消失。这时候同样的四元组又可以被使用了。

2MSL

MSL:Max Segment Lifetime,是网络报文的最大生存时间!2MSL,就是2倍这个时间。

这个2MSL时间有多长呢?(1)30秒,内核写死的;(2)2分钟,协议规定2分钟;(3)不一定,1分钟、2分钟或者4分钟,还有的30秒。不同的发行版可能会不同。在Centos 7.6.1810 的3.10内核版本上是60秒。

TIME_WAIT优化

TIME_WAIT会占用端口:

如果服务器访问量巨大,而系统端口数是有限的,端口全被TIME_WAIT占用肯定是不好的。

TIME_WAIT会占用内存和CPU:

个人认为大量的TIME_WAIT占用的内存不会很多,而CPU的计算时间也其实非常的快!因为端口号一共就65535个,这个量级的计算不会特别让人烦恼。

这个细节我没有仔细研究,待续......

同时连接

两个应用程序同时执行主动打开的情况是可能的,虽然发生的可能性较低。每一端都发送一个SYN,并传递给对方,且每一端都使用对端所知的端口作为本地端口。但多数伯克利版的tcp/ip实现并不支持同时打开。

同时FIN

这种情况比较糟糕,通信两段都会进入TIME_WAIT状态。

如果应用程序同时发送FIN,或者由于网络传输的原因,导致双方都在发出自己的FIN后,收到对方的FIN。则双方在发送后会首先进入FIN_WAIT_1状态。在收到对端的FIN后,回复一个ACK,会进入CLOSING状态。在收到对端的ACK后,进入TIME_WAIT状态。这种情况称为同时关闭。同时关闭也需要有4次报文交换,与典型的关闭相同。

TCP真心复杂!

-- EOF --

本文链接:https://www.pynote.net/archives/2612

留言区

电子邮件地址不会被公开。 必填项已用*标注


前一篇:
后一篇:

More


©Copyright 麦新杰 Since 2019 Python笔记

go to top