找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5321

积分

0

好友

729

主题
发表于 昨天 20:33 | 查看: 6| 回复: 0

"说说 TCP 三次握手。"

这句话每年出现在无数次面试中。大多数人背过答案:SYN → SYN-ACK → ACK。

但真正能回答下面这些追问的人,很少:

  • 为什么是三次?两次不行吗?
  • 四次挥手为什么要等 2MSL
  • TIME_WAIT 状态为什么让服务器"卡住"?
  • 客户端和服务端各自经历了哪些状态

这篇文章不背答案——我们把每一步都画出来,搞清楚背后的逻辑。如果你想系统性补全网络知识体系,一份条理清晰的知识图谱能帮你把零散的概念串联起来,在面试和工作里游刃有余。

一、先搞清楚:TCP 连接是什么?

TCP 连接的本质,是通信双方在内核里各自维护了一组状态缓冲区

连接的建立,就是双方就"我能收到你、你能收到我"这件事,达成共识的过程。

每一个 TCP 数据包,头部都带着几个关键标志位:

SYN  —— 我想建立连接
ACK  —— 我确认收到了你的包
FIN  —— 我要关闭连接了
RST  —— 直接重置,出问题了

带着这个背景,我们来看握手。

二、三次握手:不是仪式,是必要条件

完整流程图解

TCP三次握手过程时序图:客户端与服务端SYN、SYN-ACK、ACK交互及状态变迁

第一次握手:客户端发 SYN("我想建连接,我的初始序号是 x"),自己进入 SYN_SENT 状态。

第二次握手:服务端收到 SYN,回一个 SYN-ACK("收到了,我的序号是 y,我确认你的序号 x"),进入 SYN_RCVD 状态。

第三次握手:客户端收到 SYN-ACK,发 ACK("收到,咱俩都确认了"),双方进入 ESTABLISHED,连接建立!

三、灵魂拷问:为什么不能两次握手?

这是面试最常被追问的问题,很多人背了答案但说不出逻辑。

我们用一个场景来理解:

设想网络很差,客户端发了一个连接请求,超时没收到回复,于是重发了一次。第一次的请求包"失踪"了——其实没丢,只是在网络里转悠,很久之后才到达服务端。

如果只有两次握手,服务端收到这个"迟到的"SYN,直接认为连接建立了,开始分配资源等待数据。但客户端早就不认这条连接了。结果是:服务端白白消耗资源,浪费连接

三次握手的关键:第三次 ACK 让服务端知道客户端确实还在、确实想建连接。没有第三次,服务端无法区分"真实的新连接"和"迟来的旧请求"。

四、序列号 seq 是什么?

TCP协议序列号与确认号机制示意图:数据字节流与ACK确认关系

为什么不从 0 开始?序列号(seq)有两个关键作用:保证数据有序检测重复包

初始序列号(ISN)之所以随机,是为了防止上一条连接的"迷路包"被新连接误认。如果每次都从 0 开始,旧连接残留的包序号和新连接重叠,就会出现数据错乱。

五、四次挥手:断开连接为什么比建立还麻烦?

TCP四次挥手关闭连接时序图:客户端与服务器FIN、ACK交互及状态变迁

四次挥手比三次握手多一步,原因很简单:TCP 是全双工的

建立连接时,双方同时开始通信,所以 SYN 和 SYN-ACK 可以合并。

关闭连接时,"我不发了"(FIN)和"我也不发了"(服务端的 FIN)是两个独立的事件——服务端收到客户端的 FIN 后,可能还有数据没发完,必须先 ACK 确认,等数据发完,再发自己的 FIN。所以拆成四步。

六、TIME_WAIT 是什么?为什么等 2MSL?

这是面试加分项,也是生产服务器最常见的"坑"之一。

主动关闭连接的一方(通常是客户端)在发完最后一个 ACK 后,不会立刻 CLOSED,而是等 2MSL 时间(MSL = Maximum Segment Lifetime,报文最大存活时间,Linux 默认 60s,2MSL = 120s)。

TCP TIME_WAIT状态存在原因详解:确保最后ACK送达与旧数据包消散

TIME_WAIT 在生产中的影响:服务器主动关闭大量短连接时,会堆积大量 TIME_WAIT 状态,导致端口耗尽,新连接 bind 失败。解决办法:

// 服务端启动时设置 SO_REUSEADDR,允许复用 TIME_WAIT 状态的端口
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

或者在系统层面开启:

# 允许 TIME_WAIT 状态的 socket 复用
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

七、完整状态机:TCP 连接一生经历的所有状态

TCP连接状态转换完整流程图:客户端与服务端各阶段状态变迁与触发事件

这张图是面试中最值得背下来的——蓝色是客户端走的路,绿色是服务端走的路,两条路在 ESTABLISHED 汇合,又在不同的步骤分开。

八、用代码验证:亲眼看到状态变化

# 终端1:启动一个服务端
nc -l 8080

# 终端2:查看连接状态
ss -tn state established
# 或者
netstat -an | grep 8080

# 终端3:模拟客户端
nc 127.0.0.1 8080

# 终端2 再看,能看到 ESTABLISHED 状态
# 按 Ctrl+C 断开后再看,能短暂看到 TIME_WAIT
// 用 setsockopt 查看/设置 TCP 相关选项
struct tcp_info info;
socklen_t len = sizeof(info);
getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &len);
// info.tcpi_state 就是当前连接状态(数字对应 TCP_ESTABLISHED=1 等)
printf("TCP state: %d\n", info.tcpi_state);

九、高频面试题精析

Q:三次握手中,第三次 ACK 丢失会怎样?

服务端处于 SYN_RCVD 状态,会重传 SYN-ACK(默认最多 5 次,间隔指数退避)。如果客户端已经进入 ESTABLISHED 并发了数据,服务端收到数据后也会进入 ESTABLISHED。如果一直没收到,服务端超时后关闭连接。

Q:为什么四次挥手不能合并成三次?

因为服务端收到 FIN 时,可能还有数据没发完,必须先 ACK 确认 FIN,等数据发完后,再单独发 FIN。如果服务端没有"剩余数据要发"这种情况,理论上可以把 ACK 和 FIN 合并(变成三次)——这在某些场景确实会发生(延迟 ACK 机制下)。

Q:SYN Flood 攻击是怎么利用握手过程的?

攻击者发大量 SYN 包,但不完成握手,服务端的 SYN_RCVD 半连接队列被打满,正常连接进不来。对策:开启 SYN Cookieecho 1 > /proc/sys/net/ipv4/tcp_syncookies),服务端不分配资源,而是在 SYN-ACK 里带一个特殊 cookie,收到 ACK 后验证 cookie 再建连接。想深入了解这类防御机制的底层原理,查阅技术文档中关于内核参数调优的部分是极好的途径。

Q:close()shutdown() 触发挥手有什么区别?

close() 引用计数为 0 才真正关闭,如果 fd 被 dup() 过,另一个副本还在,不会发 FIN。shutdown(fd, SHUT_WR) 立即发 FIN,不管引用计数。要可靠地触发四次挥手,用 shutdown()

十、一图总结:三次握手 vs 四次挥手

TCP三次握手与四次挥手对比总结图:连接建立与断开的核心步骤与最少次数原因

结语

把三次握手和四次挥手搞清楚,你会发现 TCP 的每一步都有严密的逻辑:

  • 三次,是为了最少次数内确认双向通信正常
  • 四次,是因为关闭是两个独立的半关闭
  • TIME_WAIT,是为了最后一个 ACK 能送达 + 旧包彻底消散

下次面试被问到,不用再背答案,把这几张图在脑子里过一遍,逻辑自然就出来了。当你在网络编程或后端架构的实践中需要处理更复杂的连接问题时,这些基础状态机知识会自动为你导航。




上一篇:TIME_WAIT 详解:bind 报错的根本原因与三大解决方案
下一篇:服务器报警排查实战:CPU/内存/磁盘IO/网络四维定位法
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-8 00:43 , Processed in 0.780898 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表