本文迁移并整理自个人之前在博客园的博客。
参考资料:
- 《UNIX网络编程 卷1》
- TCP的三次握手/四次挥手以及状态变迁图
TCP 状态转换图
在《UNIX网络编程 卷1》一书中,作者给出了 TCP 状态转换图(如下)。本文也将围绕此图进行阐释。
注:上图红框表示比较特殊的地方。
TCP 状态转换两条主线
图 2-4 中的两条主线当然就是客户端和服务器端的状态迁移:
对客户端(也可以是服务器端,这里的例子是客户端主动打开连接,服务器端被动打开):
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
注:若客户端到达 FIN_WAIT_1 时,它同时接收到了服务器的 SYN、ACK,则它会直接跳过 FIN_WAIT_2 而到达 TIME_WAIT 状态。
对服务器端(也可以是客户端):
CLOSED->LISTEN->SYN_RECEIVED->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED
另外还需要注意的是,客户端和服务器端可能会同时打开连接或同时关闭连接(很少出现),这时处理过程会稍微跟原来的不一样,具体参考图 2-4。
各状态解释
CLOSED - 初始状态,表示 TCP 连接是“关闭着的”或“未打开的”。
LISTEN - 表示服务器端的某个 SOCKET 处于监听状态,可以接受客户端的连接。
SYN-SENT - 这个状态与 SYN_RCVD 状态相呼应,当客户端 SOCKET 执行 connect() 进行连接时,它首先发送 SYN 报文,然后随即进入到 SYN_SENT 状态,并等待服务端的发送三次握手中的第 2 个报文。SYN_SENT 状态表示客户端已发送 SYN 报文。
SYN-RCVD - 表示接收到了 SYN 报文。在正常情况下,这个状态是服务器端的 SOCKET 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用 netstat 很难看到这种状态(除非故意写一个监测程序,将三次 TCP 握手过程中最后一个 ACK 报文不予发送)。当 TCP 连接处于此状态时,再收到客户端的 ACK 报文,它就会进入到 ESTABLISHED 状态。
ESTABLISHED - 表示 TCP 连接已经成功建立,数据可以传送给用户;
FIN-WAIT-1 - 这个状态得好好解释一下,其实 FIN_WAIT_1 和 FIN_WAIT_2 两种状态的真正含义都是表示等待对方的 FIN 报文。而这两种状态的区别是:FIN_WAIT_1 状态实际上是当 SOCKET 在 ESTABLISHED 状态时,它想主动关闭连接,向对方发送了 FIN 报文,此时该 SOCKET 进入到 FIN_WAIT_1 状态。而当对方回应 ACK 报文后,则进入到 FIN_WAIT_2 状态。当然在实际的正常情况下,无论对方处于任何种情况下,都应该马上回应 ACK 报文,所以 FIN_WAIT_1 状态一般是比较难见到的,而 FIN_WAIT_2 状态有时仍可以用 netstat 看到。
FIN-WAIT-2 - 上面已经解释了这种状态的由来,实际上 FIN_WAIT_2 状态下的 SOCKET 表示半连接,即有一方调用 close() 主动要求关闭连接。注意:FIN_WAIT_2 是没有超时的(不像 TIME_WAIT 状态),这种状态下如果对方不关闭(不配合完成 4 次挥手过程),那这个 FIN_WAIT_2 状态将一直保持到系统重启,越来越多的 FIN_WAIT_2 状态会导致内核崩溃。
CLOSE-WAIT - 表示正在等待关闭。怎么理解呢?当对方 close() 一个 SOCKET 后发送 FIN 报文给自己,你的系统毫无疑问地将会回应一个 ACK 报文给对方,此时 TCP 连接则进入到 CLOSE_WAIT 状态。接下来呢,你需要检查自己是否还有数据要发送给对方,如果没有的话,那你也就可以 close() 这个 SOCKET 并发送 FIN 报文给对方,即关闭自己到对方这个方向的连接。有数据的话则看程序的策略,继续发送或丢弃。简单地说,当你处于 CLOSE_WAIT 状态下,需要完成的事情是等待你去关闭连接。
CLOSING - 这种状态在实际情况中应该很少见,属于一种比较罕见的例外状态。正常情况下,当一方发送 FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示一方发送 FIN 报文后,并没有收到对方的 ACK 报文,反而却也收到了对方的 FIN 报文。什么情况下会出现此种情况呢?那就是当双方几乎在同时 close() 一个 SOCKET 的话,就出现了双方同时发送 FIN 报文的情况,这时就会出现 CLOSING 状态,表示双方都正在关闭 SOCKET 连接。
LAST-ACK - 当被动关闭的一方在发送 FIN 报文后,等待对方的 ACK 报文的时候,就处于 LAST_ACK 状态。当收到对方的 ACK 报文后,也就可以进入到 CLOSED 可用状态了。
TIME-WAIT - 等待足够的时间以确保远程 TCP 接收到连接中断请求的确认; 表示收到了对方的 FIN 报文,并发送出了 ACK 报文。TIME_WAIT 状态下的 TCP 连接会等待 2*MSL(Max Segment Lifetime,最大分段生存期,指一个 TCP 报文在 Internet 上的最长生存时间。每个具体的 TCP 协议实现都必须选择一个确定的 MSL 值,RFC 1122 建议是 2 分钟,但 BSD 传统实现采用了 30 秒,Linux 可以 cat /proc/sys/net/ipv4/tcp_fin_timeout 看到本机的这个值),然后即可回到 CLOSED 可用状态了。如果 FIN_WAIT_1 状态下,收到了对方同时带 FIN 标志和 ACK 标志的报文时,可以直接进入到 TIME_WAIT 状态,而无须经过 FIN_WAIT_2 状态。
几个问题
1)为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为,服务端的 LISTEN 状态下的 SOCKET 当收到 SYN 报文的连接请求后,它可以把 ACK 和 SYN(ACK 起应答作用,而 SYN 起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的 FIN 报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭 SOCKET,也即你可能还需要发送一些数据给对方之后,再发送 FIN 报文给对方来表示你同意现在可以关闭连接了,所以它这里的 ACK 报文和 FIN 报文多数情况下都是分开发送的。
2)为什么 TIME_WAIT 状态还需要等 2MSL 后才能返回到 CLOSED 状态?
A. 可靠地实现 TCP 全双工连接的终止
TCP 协议在关闭连接的四次握手过程中,最终的 ACK 是由主动关闭连接的一端(后面统称 A 端)发出的,如果这个 ACK 丢失,对方(后面统称 B 端)将重发出最终的 FIN,因此 A 端必须维护状态信息(TIME_WAIT)允许它重发最终的 ACK。如果 A 端不维持 TIME_WAIT 状态,而是处于 CLOSED 状态,那么 A 端将响应 RST 分节,B 端收到后将此分节解释成一个错误(在 java 中会抛出 connection reset 的 SocketException)。
因而,要实现 TCP 全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的 A 端必须维持 TIME_WAIT 状态 。
B. 允许老的重复分节在网络中消逝(实际也就是避免同一端口对应多个套接字
)
TCP 分节可能由于路由器异常而“迷途”,在迷途期间,TCP 发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的 IP 和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP 协议不允许处于 TIME_WAIT 状态的连接启动一个新的可用连接,因为 TIME_WAIT 状态持续 2MSL,就可以保证当成功建立一个新 TCP 连接的时候,来自旧连接重复分组已经在网络中消逝。
3)关闭 TCP 连接一定需要四次挥手吗?
不一定,四次挥手关闭 TCP 连接是最安全的做法。但在有些时候,我们不喜欢 TIME_WAIT 状态(如当 MSL 数值设置过大导致服务器端有太多 TIME_WAIT 状态的 TCP 连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置 SOCKET 变量的 SO_LINGER 标志来避免 SOCKET 在 close() 之后进入 TIME_WAIT 状态,这时将通过发送 RST 强制终止 TCP 连接(取代正常的 TCP 四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的。