TCP的状态变迁图囊括了TCP的所有状态,这里我们详细学习一下。在此之前,我们先简单回顾一下TCP的三步握手和“四步分手”。

1. TCP的三步握手

因为TCP是全双工的(即数据可以在两个方向上同时传递),所以建立一个TCP连接需要三步握手。如下图,是TCP三步握手的状态图:

establish.png

  1. 服务器端通过调用一系列接口(通常是socket、bind、listen)准备好接受外来的连接,我们称之为被动打开(passive open)。
  2. 客户端通过调用connect发起主动打开(active open)。这将导致客户TCP发送一个SYN分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只包含一个头部、一个TCP首部及可能有的TCP选项。
  3. 服务器必须确认(ACK)客户的SYN,同时自己也发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
  4. 客户必须确认服务器的SYN。

下面是我用wireshark抓的一个TCP三步握手的图:

三步握手.png

2. TCP的四步分手

同理,因为TCP是全双工的,并且又有半关闭(half-close)状态的存在,所以终止一个TCP连接需要4次握手,即每一个方向都必须单独的进行关闭。这个原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。当一端收到一个FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。下图是TCP四步分手的状态图

close.png

  1. 某个应用进程(既可以是客户端,也可以是服务端)首先调用close,我们称该端执行主动关闭(active close)。该端的TCP便发送一个FIN分节,表示数据发送完毕。
  2. 接收到FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队列等候应用进程接收的任何数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可以接收。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  4. 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

需要注意的是,通常情况下断开一个TCP连接需要4个分节,但是某些情况下第一步的FIN随着数据一起发送;另外第二步和第三步可能被合成一个分节发送。比如下面我用wireshark抓的一个TCP四步分手的图里面就是这种场景:

四步分手.png

OK,TCP的连接和建立过程简单介绍完以后,我们来看一下TCP的状态变迁图。

3. TCP的状态变迁图

TCP的状态变迁图更细致的描述了TCP连接建立和连接终止中的各个状态。下图是一个完整的TCP状态变迁图(图片截自《UNIX网络编程》电子版):

tcp_status.png

TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。举例来说,当某个应用进程在CLOSED状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT。如果这个TCP接着接收到一个带ACK的SYN,它将发送发送一个ACK,且新的状态是ESTABLISHED。这个状态是绝大多数数据传送发生的状态。

自ESTABLISHED状态引出的两个箭头处理连接的终止。如果某个应用进程在接收到一个FIN之前调用close(即主动关闭),那就转换到FIN_WAIT_1状态。但如果某个应用进程在ESTABLISHED状态期接收到一个FIN(即被动关闭),那就转到CLOSE_WAIT状态。

TCP的11种状态:

  • ESTABLISHED: The socket has an established connection.
  • SYN_SENT: The socket is actively attempting to establish a connection.
  • SYN_RECV: A connection request has been received from the network.
  • FIN_WAIT1: The socket is closed, and the connection is shutting down.
  • FIN_WAIT2: Connection is closed, and the socket is waiting for a shutdown from the remote end.
  • TIME_WAIT: The socket is waiting after close to handle packets still in the network.
  • CLOSE: The socket is not being used.
  • CLOSE_WAIT: The remote end has shut down, waiting for the socket to close.
  • LAST_ACK: The remote end has shut down, and the socket is closed. Waiting for acknowledgement.
  • LISTEN: The socket is listening for incoming connections.

当然netstat命令可能还能看到两种状态:

CLOSING: Both sockets are shut down but we still don't have all our data sent.
UNKNOWN: The state of the socket is unknown.

下面是TCP正常连接建立和终止所对应的状态:

state.png

结合上面各个状态的解释以及上图,TCP状态变迁图中的各个状态(除TIME_WAIT状态)出现的时机都比较容易理解。这里我们主要说一下FIN_WAIT_2状态和不太容易理解的TIME_WAIT状态。

FIN_WAIT_2状态

在FIN_WAIT_2状态时我们已经发出了FIN,并且另一端也已对它进行了确认。除非我们在实行半关闭,否则将等待另一端应用层意识到它已收到一个文件结束符说明,并向我们发一个FIN来关闭另一个方向上的连接。只有当另一端的进程完成这个关闭,我们这端才会从FIN_WAIT_2状态进入到TIME_WAIT状态。

这意味着我们这端可能永远保持这个状态。另一端也将处于CLOSE_WAIT状态,并一直保持这个状态直到应用层决定进行关闭。不过许多TCP实现时,为了防止这种无限等待状态,如果执行主动关闭的应用层将进行全关闭,而不是半关闭来说明它还想接收数据,就设置一个定时器。如果这个连接空闲一段时间(常见的伯克利实现是10min75秒),TCP将进入CLOSED状态。

TCP的半关闭(Half-Close):连接的一端在结束它的发送后还能接收来自另一端数据的能力。socket API的close一般是终止两个方向的连接,即全关闭。而shutdown提供了半关闭的功能。

TCP半打开(Half-Open):一方关闭或者异常终止连接而另一方却不知道。这种状态在实际场景中挺常见的,比如一端突然掉电,网络突然中断等。一般可以通过TCP的Keepalive机制或者应用层心跳等机制去检测这种状态。

TIME_WAIT状态

TIME_WAIT状态又称为2MSL等待状态。所谓MSL(Maximum Segment Lifetime)指的是报文段在网络内存活的最大时间。RFC 793建议的MSL为2分钟,具体的实现中一般为30秒或1分钟。不过IP数据报在网络中传输时一般是以跳数限制的,即TTL。TIME_WAIT状态的存在主要是基于两个考虑:

1. 可靠的实现TCP全双工连接的终止。
2. 允许老的重复分节在网络中消逝。

对于第1个考虑:当TCP执行一个主动关闭,并发回最后一个ACK(即上图中的ACK N+1),该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(如果丢失的话,另一端超时并重发最后的FIN)。

对于第2个考虑:我们假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP和端口之间建立另外一个连接。后一个连接称为前一个连接的化身(incarnation),因为他们的IP地址和端口号相同。TCP必须防止某个连接的老的分组在该连接终止后再现,从而被误解成属于同一个连接的某个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒被丢弃。通过实施这一个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已经在网络中消逝了。

对于TIME_WAIT的第2个考虑点,还有两个注意事项:

  1. 只有执行主动关闭的一方才会进入TIME_WAIT状态,被动关闭方不会。一般来说,都是客户端执行主动关闭,服务端都是被动关闭的。因为客户端重新连接时,一般会使用一个新的端口号(通常都是系统分配的),所以不会受TIME_WAIT的限制。
  2. 为了防止TIME_WAIT对服务端的影响——服务端一般都使用的是固定的知名端口,如果重启的话(主动关闭),很可能因为TIME_WAIT的限制无法创建建立连接。所以socket API提供了SO_REUSEADDR选项,它让调用者可以对处于2MSL状态(即TIME_WAIT状态)的本地端口进行赋值。

当然,关于TIME_WAIT状态还有一些细节性的东西,有兴趣的可以阅读《TCP/IP详解 卷1:协议》第18.6.1章节。

至此,TCP的状态变迁就介绍完了。当然,TCP是一个比较复杂的协议,上面的状态变迁图中还包含了同时打开和同时关闭等一些比较少见但却存在的场景,这里就不再细述了。