TCP 三次握手与四次挥手是网络编程面试中的高频考点。大多数候选人能背出状态转换图,但一旦追问“内核具体怎么实现的”“半连接队列和全连接队列有什么区别”“为什么挥手需要四次”,区分度就出来了。这篇文章从内核实现的角度,把这两个过程拆开讲清楚。
一、三次握手的内核实现
1.1 从 socket 到 listen:准备工作
服务端调用 socket() 创建套接字,bind() 绑定地址端口,listen() 将套接字转为被动监听状态。listen(fd, backlog) 中的 backlog 参数决定了全连接队列的最大长度(内核 4.6 之后语义有所调整,但面试中按经典理解即可)。
内核为监听套接字维护两个关键队列:
- 半连接队列(SYN Queue) :收到 SYN、尚未完成三次握手的连接
- 全连接队列(Accept Queue) :已完成三次握手、等待
accept()取走的连接
1.2 第一步:客户端发送 SYN
客户端调用 connect(),内核构造 SYN 报文,携带客户端初始序列号(ISN)。客户端进入 SYN_SENT 状态。
ISN 不是从 0 或固定值开始,而是基于时钟和四元组哈希生成的,防止序列号预测攻击。
1.3 第二步:服务端回复 SYN+ACK
服务端收到 SYN 后,内核做几件事:
- 检查半连接队列是否已满。如果满了且未开启
syncookies,直接丢弃该 SYN(客户端会超时重传)。 - 创建一个 request_sock 结构(轻量级,不是完整的 socket),放入半连接队列。
- 回复 SYN+ACK,携带服务端自己的 ISN 和对客户端 ISN 的确认。
此时服务端处于 SYN_RECV 状态。
1.4 第三步:客户端回复 ACK
客户端收到 SYN+ACK 后,发送 ACK,进入 ESTABLISHED 状态。
服务端收到这个 ACK 后,内核从半连接队列取出对应的 request_sock,创建完整的 sock 结构,将其放入全连接队列,连接状态变为 ESTABLISHED。此时连接已经就绪,等待应用层调用 accept() 取走。
1.5 面试常问的两个队列问题
半连接队列满了怎么办?
- 默认行为:丢弃新来的 SYN,客户端会重传
- 如果
net.ipv4.tcp_syncookies=1,内核会启用 SYN Cookie:不分配 request_sock,而是将连接信息编码进 SYN+ACK 的序列号中,收到 ACK 后再解码还原。这可以抵御 SYN Flood 攻击。
全连接队列满了怎么办?
- 默认行为:丢弃客户端的 ACK(或根据
tcp_abort_on_overflow参数决定是否发 RST) - 客户端认为自己已建立连接,发数据时才发现服务端不认,触发重传
- 可以通过
ss -lnt查看Send-Q列(全连接队列大小)和Recv-Q列(当前排队数量)
# 查看全连接队列溢出情况
netstat -s | grep -i "listen"
# 或
nstat -az | grep -i listen
二、四次挥手的内核实现
2.1 为什么需要四次
TCP 是全双工的,每个方向需要独立关闭。主动关闭方发送 FIN 表示“我没有数据要发了”,但对方可能还有数据要发,所以 ACK 和 FIN 不能合并(除非对方恰好也没有数据要发了)。
2.2 第一步:主动关闭方发送 FIN
应用层调用 close() 或 shutdown(SHUT_WR),内核发送 FIN 报文,主动关闭方进入 FIN_WAIT_1 状态。
注意:close() 会同时关闭读写两个方向,而 shutdown(SHUT_WR) 只关闭写方向,仍可读取对方数据。这个区别在面试中经常被问到。
2.3 第二步:被动关闭方回复 ACK
被动关闭方内核收到 FIN,回复 ACK,进入 CLOSE_WAIT 状态。
此时连接处于半关闭状态:主动关闭方进入 FIN_WAIT_2,被动关闭方还可以继续发送数据。
CLOSE_WAIT 状态如果大量堆积,通常意味着应用程序没有正确调用 close(),这是常见的线上问题。
2.4 第三步:被动关闭方发送 FIN
被动关闭方应用层处理完数据后调用 close(),内核发送 FIN,进入 LAST_ACK 状态。
2.5 第四步:主动关闭方回复 ACK
主动关闭方收到 FIN,回复 ACK。此时主动关闭方进入 TIME_WAIT 状态,等待 2MSL(报文最大生存时间的两倍)后才真正关闭。
被动关闭方收到 ACK 后,连接彻底关闭。
2.6 TIME_WAIT 的内核意义
TIME_WAIT 不是设计缺陷,它有两个核心作用:
- 确保最后一个 ACK 能到达对方:如果 ACK 丢失,对方会重传 FIN,处于 TIME_WAIT 的一方还能重新回复 ACK。
- 让旧连接的延迟报文在网络中消散:防止新旧连接四元组相同导致数据混淆。
内核相关参数:
# TIME_WAIT 状态持续时间(默认 60 秒)
cat /proc/sys/net/ipv4/tcp_fin_timeout
# 允许 TIME_WAIT 状态的 socket 被新连接复用
cat /proc/sys/net/ipv4/tcp_tw_reuse
# 快速回收 TIME_WAIT(已在新内核中移除,不建议使用)
# net.ipv4.tcp_tw_recycle 在 4.12 内核后被删除
三、内核中的关键数据结构
面试中如果被问到“内核用什么结构管理这些连接”,可以这样回答:
- struct sock:完整的套接字结构,代表一个 established 连接
- struct request_sock:半连接阶段的轻量结构,只保存握手必需的信息
- struct tcp_sock:TCP 专用扩展,包含序列号、窗口、拥塞控制状态等
- ehash / lhash:内核用哈希表管理 established 连接(ehash)和监听套接字(lhash)
三次握手的本质,就是内核从 request_sock 升级到完整 sock 的过程。
四、面试答题思路总结
遇到这类问题,建议按以下层次作答:
- 先画状态转换图,说清楚每个状态的进入和退出条件
- 再讲内核行为:队列管理、数据结构、异常处理
- 最后联系实际问题:SYN Flood、CLOSE_WAIT 堆积、TIME_WAIT 过多怎么排查和调优
能把这三层都讲清楚,基本可以证明你不仅背过八股文,而且真正理解 TCP 在内核中的运作方式。
未经允许不得转载:任鹏个人博客 » Linux 面试题:TCP 三次握手、四次挥手在内核中的实现要点

