牢姐与蒯头像
Linux网络篇——TCP(四).从三次握手到四次挥手,再到TIME_WAIT状态与CLOSE_WAIT状态的深入理解封面图

Linux网络篇——TCP(四).从三次握手到四次挥手,再到TIME_WAIT状态与CLOSE_WAIT状态的深入理解

 牢蒯的板书原图在下面的仓库里,各位大佬可以自行取用,古法编程,难免有bug,大佬们可在评论区指出错误。

https://gitee.com/laotian666/linux_storage.git


一、前言

在之前的 TCP 学习中,我们已经了解了 TCP 协议的基本特点,也知道 TCP 是一种面向连接、可靠的、基于字节流的传输层协议。

既然 TCP 是面向连接的,那么一个问题就来了:

TCP 是如何建立连接的?又是如何断开连接的?

相信大家都听说过 TCP 的三次握手和四次挥手,但是如果仅仅记住三次握手发送 SYN、SYN+ACK、ACK,四次挥手发送 FIN、ACK、FIN、ACK,那么对于 TCP 连接的理解还远远不够。

我们真正需要理解的是:

  1. 为什么 TCP 建立连接需要三次握手,而不是两次?

  2. 为什么 TCP 断开连接需要四次挥手?

  3. 为什么主动关闭连接的一方会进入 TIME_WAIT 状态?

  4. TIME_WAIT 状态会不会影响服务器重新启动和端口绑定?

  5. 为什么 CLOSE_WAIT 状态长期存在,可能意味着文件描述符泄漏?

这些问题实际上都是围绕 TCP 连接的建立、维护和释放展开的。

二、TCP连接的建立——三次握手

2.1 什么是三次握手?

TCP 是面向连接的协议,在正式进行数据通信之前,通信双方必须先建立连接。

TCP 建立连接的过程通常被称为三次握手。

所谓三次握手,就是客户端和服务端之间需要进行三次报文交互,最终让双方都确认对方具备正常收发数据的能力。

首先,我们先明确通信双方的角色:

  • 客户端:主动发起连接的一方。

  • 服务端:被动等待连接的一方。

服务端通常会提前创建监听套接字,通过 socket、bind、listen 等操作进入监听状态,等待客户端发起连接。

客户端则通过 connect 函数主动向服务端发起连接请求。

2.2 三次握手的具体过程

第一次握手:

客户端向服务端发送一个 SYN 请求报文,表示客户端希望与服务端建立 TCP 连接。

此时客户端进入 SYN_SENT 状态。

第二次握手:

服务端收到 SYN 请求之后,向客户端发送 SYN+ACK 报文。

其中,SYN 表示服务端也希望建立连接,ACK 表示服务端已经确认收到了客户端之前发送的 SYN 请求。

此时服务端进入 SYN_RCVD 状态。

第三次握手:

客户端收到服务端发送的 SYN+ACK 报文之后,再向服务端发送 ACK 确认报文。

服务端收到 ACK 之后,双方完成连接建立。

此时客户端和服务端都进入 ESTABLISHED 状态,可以正式进行数据通信。

2.3 为什么需要三次握手?

这里有一个非常重要的问题:

为什么 TCP 建立连接需要三次握手,而不是两次握手?

本质上,三次握手是为了让通信双方建立连接的共识。

我们可以从客户端和服务端的角度进行理解。

第一次握手,客户端发送 SYN:

客户端告诉服务端:

我希望和你建立连接,你能收到我的请求吗?

第二次握手,服务端发送 SYN+ACK:

服务端告诉客户端:

我收到了你的请求,而且我也能够向你发送数据。

第三次握手,客户端发送 ACK:

客户端告诉服务端:

我收到了你的响应,现在我们双方都确认了通信能力,可以正式建立连接。

因此,三次握手完成之后,双方都能够确认:

  • 自己发送的数据能够被对方接收。

  • 自己能够接收到对方发送的数据。

  • 双方已经对连接建立达成共识。

这也是为什么 TCP 建立连接需要三次握手,而不是两次握手。

三、TCP连接的断开——四次挥手

3.1 为什么断开连接需要四次挥手?

连接建立之后,客户端和服务端就可以进行数据通信了。

但是,数据通信结束之后,我们还需要考虑一个问题:

TCP 连接应该如何断开?

TCP 是一种全双工通信协议。

所谓全双工,就是通信双方都可以独立地发送和接收数据。

也就是说,客户端可以向服务端发送数据,服务端也可以向客户端发送数据,而且这两个方向的数据传输是相互独立的。

因此,TCP 连接断开时,不能简单地认为一方不想发送数据了,另一方就必须立即停止发送。

我们需要让两个通信方向分别完成关闭,这就是四次挥手存在的原因。

3.2 四次挥手的具体过程

假设客户端主动提出断开连接,服务端被动响应。

第一次挥手:

客户端认为自己已经没有数据需要发送了,于是向服务端发送 FIN 报文,表示客户端希望关闭自己的发送方向。

客户端进入 FIN_WAIT_1 状态。

第二次挥手:

服务端收到客户端发送的 FIN 报文之后,向客户端发送 ACK 确认报文。

表示服务端已经收到了客户端的关闭请求。

此时服务端进入 CLOSE_WAIT 状态。

客户端收到 ACK 之后,进入 FIN_WAIT_2 状态。

注意:客户端发送 FIN,只代表客户端不再向服务端发送数据,并不代表服务端也必须立即停止发送数据。

因为 TCP 是全双工通信,所以服务端仍然可以继续向客户端发送数据。

第三次挥手:

当服务端也确认自己没有数据需要发送之后,就会向客户端发送 FIN 报文,表示服务端也希望关闭连接。

此时服务端进入 LAST_ACK 状态。

第四次挥手:

客户端收到服务端发送的 FIN 报文之后,再向服务端发送 ACK 确认报文。

此时客户端进入 TIME_WAIT 状态。

服务端收到 ACK 之后,进入 CLOSED 状态,连接正式关闭。

客户端等待一段时间之后,也会进入 CLOSED 状态。

至此,四次挥手完成。

3.3 为什么不能把四次挥手变成三次?

可能有同学会问:

既然三次握手可以把 SYN 和 ACK 放在同一个报文中,那么四次挥手是不是也可以?

答案是:有可能合并,但通常情况下是四次。

原因在于 TCP 是全双工通信。

客户端发送 FIN,表示客户端已经没有数据需要发送了。

但是服务端此时可能还有数据没有发送完成。

因此,服务端需要先回复 ACK,确认收到了客户端的关闭请求。

等服务端真正处理完剩余数据之后,才能发送 FIN,表示自己也准备关闭连接。

所以,第二次挥手和第三次挥手之间可能存在一段时间间隔。

如果服务端恰好没有剩余数据需要发送,那么 ACK 和 FIN 在特定情况下可以合并发送。

这里一定要记住:三次握手的核心是建立连接的共识,四次挥手的核心是分别关闭 TCP 连接的两个通信方向。

四、TIME_WAIT状态的深入理解

4.1 什么是TIME_WAIT状态?

在四次挥手的最后,客户端发送 ACK 确认报文之后,并不会立即进入 CLOSED 状态,而是进入 TIME_WAIT 状态。

那么问题来了:

明明服务端已经收到最后一个 ACK,连接似乎已经结束了,为什么客户端还需要等待一段时间?

实际上,TIME_WAIT 状态主要有两个作用:

  1. 保证 TCP 连接能够可靠地完成关闭。

  2. 防止旧连接中的延迟报文影响后续建立的新连接。

4.2 为什么需要等待2MSL?

首先,我们需要理解 MSL 的含义。

MSL 全称为 Maximum Segment Lifetime,表示报文段在网络中能够存活的最大时间。

TCP 进入 TIME_WAIT 状态之后,需要等待 2MSL。

为什么是 2MSL,而不是 1MSL?

我们可以从最后一次挥手来分析。

客户端向服务端发送最后一个 ACK 之后,存在两种情况。

第一种情况:

服务端成功收到了客户端发送的 ACK。

此时服务端正常进入 CLOSED 状态。

客户端等待一段时间,确保网络中可能存在的旧报文能够消失即可。

第二种情况:

客户端发送的最后一个 ACK 在网络传输过程中丢失了。

服务端没有收到 ACK,就会认为自己的 FIN 报文可能没有被客户端正常确认。

于是,服务端可能会重新发送 FIN 报文。

客户端在 TIME_WAIT 状态下仍然能够接收这个重复的 FIN,并重新发送 ACK。

这样就避免了服务端一直等待确认的问题。

为什么要等待 2MSL?

因为一个报文从客户端到服务端最多需要一个 MSL,而服务端重新发送的报文再从服务端返回客户端,理论上又需要一个 MSL。

因此,等待 2MSL 可以保证旧连接相关的报文有足够的时间从网络中消失。

4.3 TIME_WAIT状态会带来什么问题?

TIME_WAIT 虽然有重要作用,但是在实际开发中也可能带来一些问题。

例如,我们编写了一个 TCP 服务端程序,使用固定端口 8888 提供服务。

服务端启动之后,客户端连接成功,双方进行数据通信。

当连接断开之后,如果主动关闭连接的一方进入 TIME_WAIT 状态,那么系统中仍然可能保留该连接对应的 TCP 状态信息。

此时,如果我们马上重新启动服务端,并尝试绑定相同的 IP 和端口,就有可能遇到 bind 失败的问题。

例如:

./myhttp 8888

程序第一次启动正常。

但是关闭程序之后,马上再次启动,可能出现:

socket success
bind error

为什么会出现这种情况?

因为 TCP 连接进入 TIME_WAIT 状态之后,相关的 TCP 状态信息并不会立即消失。

旧连接的状态可能影响新连接对本地地址和端口的绑定。

这就是我们经常遇到的 TIME_WAIT 相关问题。

4.4 如何解决TIME_WAIT导致的端口绑定问题?

既然 TIME_WAIT 可能影响服务端重新绑定端口,那么有没有解决办法?

有。

我们可以在服务端调用 bind 之前,使用 setsockopt 设置 SO_REUSEADDR 选项。

具体代码如下:

int opt = 1;

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

这段代码的作用是:

允许套接字在满足系统复用规则的情况下,重新绑定已经使用过的本地地址。

其中:

  • listenfd 表示服务端监听套接字。

  • SOL_SOCKET 表示设置套接字层面的选项。

  • SO_REUSEADDR 表示允许地址复用。

  • opt 表示设置的选项值。

  • sizeof(opt) 表示选项值的长度。

通常,我们会在服务端初始化套接字之后、调用 bind 之前设置该选项。

这样,当服务端重新启动时,就能够在符合系统复用规则的情况下重新绑定之前使用过的端口。

但是这里需要注意:

SO_REUSEADDR 并不是让 TIME_WAIT 状态立即消失,也不是无条件允许多个套接字同时绑定同一个地址和端口。

它只是放宽了地址绑定的限制。

所以,SO_REUSEADDR 是解决服务端重启时端口绑定问题的一种常见方案,但并不是消除 TIME_WAIT 的方法。

4.5 TIME_WAIT还有没有其他解决思路?

除了设置 SO_REUSEADDR,我们还可以从连接管理的角度考虑问题。

例如:

  • 合理设计客户端和服务端的连接生命周期。

  • 避免不必要的频繁建立和关闭连接。

  • 对于需要长期通信的业务,合理使用长连接。

  • 在应用层做好连接管理,避免短时间内产生大量不必要的连接。

TIME_WAIT 本身是 TCP 为了保证可靠关闭而设计的机制,不应该简单地将它视为错误状态。

五、CLOSE_WAIT状态与文件描述符泄漏

5.1 什么是CLOSE_WAIT状态?

前面我们讲过四次挥手。

当客户端主动发送 FIN,服务端收到 FIN 并回复 ACK 之后,服务端就会进入 CLOSE_WAIT 状态。

那么,CLOSE_WAIT 到底意味着什么?

它表示:

对端已经主动关闭了自己的发送方向,而本端还没有完成自己的关闭操作。

我们可以把它理解成:

客户端已经告诉服务端:

我这边的数据已经发送完了,我准备关闭连接。

服务端回复:

我知道了。

但是服务端自己可能还有数据没有发送完,或者应用程序还没有执行关闭连接的操作。

所以,服务端会停留在 CLOSE_WAIT 状态。

等到服务端确认自己也没有数据需要发送了,就可以调用 close(fd),向对端发送 FIN,完成后续的连接关闭过程。

5.2 CLOSE_WAIT为什么可能长期存在?

假设客户端和服务端建立了 TCP 连接。

之后,客户端主动关闭连接,向服务端发送 FIN。

服务端收到 FIN,回复 ACK,于是服务端进入 CLOSE_WAIT 状态。

按照正常流程,服务端应该在处理完业务之后,调用 close(fd),主动关闭自己的连接。

但是,如果服务端程序出现了逻辑问题,没有执行 close(fd),会发生什么?

服务端就可能一直停留在 CLOSE_WAIT 状态。

因为客户端已经提出关闭连接的请求,但是服务端应用程序始终没有完成自己的关闭操作。

于是,这条连接就一直无法按照正常流程完成释放。

5.3 不调用close(fd)会有什么后果?

我们都知道,Linux 中的文件描述符 fd 是有限资源。

一个进程能够打开的文件描述符数量受到系统和进程资源限制。

如果服务端程序不断接收客户端连接,却没有正确关闭已经结束的连接,那么就可能出现以下问题:

  1. 已经不再使用的连接仍然被应用程序持有。

  2. 文件描述符数量不断增加。

  3. 可用文件描述符逐渐减少。

  4. 当文件描述符资源耗尽时,后续 socket、accept 等操作可能失败。

这种由于没有正确关闭文件描述符,导致文件描述符资源无法正常回收的现象,就是我们通常所说的 文件描述符泄漏。

因此,服务端程序在处理完连接之后,一定要注意调用 close(fd)。

5.4 TIME_WAIT和CLOSE_WAIT有什么区别?

这两个状态经常被放在一起讨论,但是它们的含义完全不同。

TIME_WAIT 通常出现在主动关闭连接的一方。

CLOSE_WAIT 通常出现在被动关闭连接的一方。

TIME_WAIT 的主要目的是保证 TCP 连接关闭的可靠性,以及避免旧连接的延迟报文影响新连接。

CLOSE_WAIT 则表示本端已经收到对端的 FIN,但是本端应用程序还没有完成关闭操作。

我们可以通过下面的表格进行对比:

对比内容TIME_WAITCLOSE_WAIT
常见出现位置主动关闭连接的一方被动关闭连接的一方
进入条件主动关闭方完成最后一次 ACK收到对端 FIN 并回复 ACK
主要含义等待连接关闭保护时间结束等待本端应用程序完成关闭
是否需要关注需要关注数量和持续时间长期大量存在通常需要排查
常见问题端口重新绑定可能受影响可能反映应用程序未及时关闭连接

这里还需要特别强调一个容易混淆的知识点:

TIME_WAIT 状态不等于文件描述符一直没有释放。

对于正常执行 close(fd) 的程序,进程中的文件描述符可以被释放,但内核仍然可能保留 TCP 连接的 TIME_WAIT 状态。

而 CLOSE_WAIT 长期存在,则往往说明应用程序还没有正确完成对应连接的关闭操作。

因此,不能简单地把 TIME_WAIT 和文件描述符泄漏画等号。

六、TCP连接关闭的本质

6.1 断开连接实际上是在建立关闭连接的共识

回过头来看,为什么 TCP 断开连接需要四次挥手?

其实,断开连接的本质就是建立双方断开连接的共识。

s->c && c->s

假设客户端和服务端正在进行数据通信。

客户端认为:

我已经没有数据需要发送了,我想关闭连接。

但是服务端可能认为:

我还有数据没有发送完,暂时不能关闭。

这时候双方对于是否关闭连接,还没有达成共识。

因此,客户端首先发送 FIN。

服务端收到之后回复 ACK,表示已经知道客户端希望关闭连接。

但是服务端仍然可以继续发送剩余数据。

等到服务端也认为自己没有数据需要发送了,就会发送 FIN。

客户端收到之后回复 ACK。

至此,双方才真正完成两个通信方向的关闭。

所以,TCP 四次挥手并不是简单地发送四个报文,而是通信双方分别表达关闭意愿,并对对方的关闭请求进行确认。

6.2 为什么双方不能直接一起关闭?

因为 TCP 是全双工通信。

客户端和服务端的发送方向是相互独立的。

客户端不想再发送数据,并不代表服务端也不想发送数据。

同样,服务端不想再发送数据,也不代表客户端已经完成自己的数据发送。

因此,双方需要分别通知对方自己已经完成数据发送。

这也解释了为什么正常情况下需要四次挥手。

七、总结

本篇文章主要围绕 TCP 三次握手、四次挥手以及连接状态转换展开。

我们需要掌握以下几个核心知识点:

  1. TCP 建立连接需要三次握手,目的是让通信双方确认彼此的收发能力,建立连接共识。

  2. TCP 断开连接通常需要四次挥手,因为 TCP 是全双工通信,通信双方需要分别关闭自己的发送方向。

  3. TIME_WAIT 通常出现在主动关闭连接的一方,其主要作用是保证最后一次 ACK 能够可靠完成,并让旧连接中的延迟报文有足够的时间消失。

  4. SO_REUSEADDR 可以解决服务端重启时常见的端口绑定问题,但不会让 TIME_WAIT 状态立即消失。

  5. CLOSE_WAIT 表示本端已经收到对端的 FIN,但是本端应用程序还没有完成关闭操作。

  6. 服务端处理完连接之后,需要正确调用 close(fd),否则可能出现文件描述符泄漏。

  7. TIME_WAIT 和 CLOSE_WAIT 虽然都是 TCP 的连接状态,但是它们产生的原因、存在的意义以及需要排查的问题完全不同。

最后,我们再回到最开始的问题:

TCP 建立连接的本质,是建立双方通信的共识;TCP 断开连接的本质,则是建立双方断开通信的共识。

理解了这一点,再结合 TCP 状态转换图去分析 SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK、TIME_WAIT 等状态,整个 TCP 连接建立和释放的过程就会清晰很多。

TCP 三次握手和四次挥手并不只是需要背诵的几个报文,而是理解 TCP 面向连接、全双工通信以及可靠传输机制的重要基础。


最后附上牢蒯的板书:

​

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_87941709/article/details/170514250

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--