牢蒯的板书原图在下面的仓库里,各位大佬可以自行取用,古法编程,难免有bug,大佬们可在评论区指出错误。
https://gitee.com/laotian666/linux_storage.git
一、前言
在之前的 TCP 学习中,我们已经了解了 TCP 协议的基本特点,也知道 TCP 是一种面向连接、可靠的、基于字节流的传输层协议。
既然 TCP 是面向连接的,那么一个问题就来了:
TCP 是如何建立连接的?又是如何断开连接的?
相信大家都听说过 TCP 的三次握手和四次挥手,但是如果仅仅记住三次握手发送 SYN、SYN+ACK、ACK,四次挥手发送 FIN、ACK、FIN、ACK,那么对于 TCP 连接的理解还远远不够。
我们真正需要理解的是:
为什么 TCP 建立连接需要三次握手,而不是两次?
为什么 TCP 断开连接需要四次挥手?
为什么主动关闭连接的一方会进入 TIME_WAIT 状态?
TIME_WAIT 状态会不会影响服务器重新启动和端口绑定?
为什么 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 状态主要有两个作用:
-
保证 TCP 连接能够可靠地完成关闭。
-
防止旧连接中的延迟报文影响后续建立的新连接。
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 是有限资源。
一个进程能够打开的文件描述符数量受到系统和进程资源限制。
如果服务端程序不断接收客户端连接,却没有正确关闭已经结束的连接,那么就可能出现以下问题:
已经不再使用的连接仍然被应用程序持有。
文件描述符数量不断增加。
可用文件描述符逐渐减少。
当文件描述符资源耗尽时,后续 socket、accept 等操作可能失败。
这种由于没有正确关闭文件描述符,导致文件描述符资源无法正常回收的现象,就是我们通常所说的 文件描述符泄漏。
因此,服务端程序在处理完连接之后,一定要注意调用 close(fd)。
5.4 TIME_WAIT和CLOSE_WAIT有什么区别?
这两个状态经常被放在一起讨论,但是它们的含义完全不同。
TIME_WAIT 通常出现在主动关闭连接的一方。
CLOSE_WAIT 通常出现在被动关闭连接的一方。
TIME_WAIT 的主要目的是保证 TCP 连接关闭的可靠性,以及避免旧连接的延迟报文影响新连接。
CLOSE_WAIT 则表示本端已经收到对端的 FIN,但是本端应用程序还没有完成关闭操作。
我们可以通过下面的表格进行对比:
| 对比内容 | TIME_WAIT | CLOSE_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 三次握手、四次挥手以及连接状态转换展开。
我们需要掌握以下几个核心知识点:
-
TCP 建立连接需要三次握手,目的是让通信双方确认彼此的收发能力,建立连接共识。
-
TCP 断开连接通常需要四次挥手,因为 TCP 是全双工通信,通信双方需要分别关闭自己的发送方向。
-
TIME_WAIT 通常出现在主动关闭连接的一方,其主要作用是保证最后一次 ACK 能够可靠完成,并让旧连接中的延迟报文有足够的时间消失。
-
SO_REUSEADDR 可以解决服务端重启时常见的端口绑定问题,但不会让 TIME_WAIT 状态立即消失。
-
CLOSE_WAIT 表示本端已经收到对端的 FIN,但是本端应用程序还没有完成关闭操作。
-
服务端处理完连接之后,需要正确调用 close(fd),否则可能出现文件描述符泄漏。
-
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




