摘要:从一次"服务器没调accept()但连接成功了"的意外发现出发,深度剖析TCP三次握手与accept()的关系。通过全连接队列和半连接队列的工作原理、以及SYN flood攻击的防御机制,揭秘为什么三次握手在内核完成、accept()只是从队列取连接、以及为什么高并发场景下backlog参数很重要。配合时序图展示连接建立流程,给出服务器参数调优的最佳实践。
💥 翻车现场
周五下午,哈吉米在调试网络程序。
测试代码(服务器):
public class SimpleServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动,监听8080端口");
System.out.println("等待10秒再accept...");
// 故意延迟10秒再accept
Thread.sleep(10000);
System.out.println("开始accept...");
Socket socket = serverSocket.accept(); // 10秒后才调用accept
System.out.println("连接建立:" + socket.getRemoteSocketAddress());
}
}
客户端:
public class SimpleClient {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动,连接服务器...");
Socket socket = new Socket("localhost", 8080);
System.out.println("连接成功!"); // 1秒内就打印了
System.out.println("远程地址:" + socket.getRemoteSocketAddress());
}
}
运行结果:
# 服务器输出
服务器启动,监听8080端口
等待10秒再accept...
(等待中...)
开始accept...
连接建立:/127.0.0.1:12345
# 客户端输出(1秒内)
客户端启动,连接服务器...
连接成功! ← 服务器还没调accept(),客户端就成功了
远程地址:/127.0.0.1:8080
哈吉米:"卧槽,服务器还没调accept(),客户端就显示连接成功了?这不科学啊!"
哈吉米(疑问):"TCP连接是在三次握手完成时建立的?还是在accept()返回时建立的?"
南北绿豆和阿西噶阿西来了。
南北绿豆:"TCP连接在三次握手完成时就建立了,accept()只是从队列里取出连接!"
哈吉米:"队列?"
阿西噶阿西:"来,我给你讲讲内核的全连接队列和半连接队列。"
🤔 TCP连接建立的真相
accept()到底做了什么?
阿西噶阿西在白板上画了一个图。
误解:
客户端connect() → 三次握手 → 服务器accept() → 连接建立
真相:
客户端connect() → 三次握手(内核完成) → 连接建立 ✅
↓
进入全连接队列
↓
服务器accept() → 从队列取出连接
关键:
1. 三次握手在内核完成(不需要应用层参与)
2. 连接建立后,放入全连接队列
3. accept()只是从队列取连接(不参与握手)
4. 即使不调accept(),三次握手也能完成
内核的两个队列
南北绿豆:"内核维护了2个队列来管理连接。"
半连接队列(SYN队列):
存储正在握手的连接(收到SYN,还没完成三次握手)
全连接队列(Accept队列):
存储已完成三次握手的连接(等待accept()取走)
队列结构:
内核空间
┌──────────────────────────────────┐
│ 半连接队列(SYN Queue) │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │SYN1│→│SYN2│→│SYN3│→... │
│ └────┘ └────┘ └────┘ │
│ ↓ │
│ 三次握手完成 │
│ ↓ │
│ 全连接队列(Accept Queue) │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │连接1│→│连接2│→│连接3│→... │
│ └────┘ └────┘ └────┘ │
│ ↑ │
└──────│───────────────────────────┘
│
accept()取出
│
┌──────↓───────────────────────────┐
│ 应用空间 │
│ Socket socket = accept(); │
└──────────────────────────────────┘
🎯 完整的连接建立流程
详细时序图
sequenceDiagram
participant Client as 客户端
participant Kernel as 内核(服务器)
participant SynQueue as 半连接队列
participant AcceptQueue as 全连接队列
participant App as 应用(服务器)
Note over App: serverSocket.listen(8080, backlog=128)
Note over AcceptQueue: 全连接队列创建,容量=128
Client->>Kernel: 1. SYN(第一次握手)
Kernel->>SynQueue: 2. 连接加入半连接队列
Note over SynQueue: 状态:SYN_RECEIVED
Kernel->>Client: 3. SYN + ACK(第二次握手)
Client->>Kernel: 4. ACK(第三次握手)
Kernel->>Kernel: 5. 三次握手完成 ✅
Kernel->>SynQueue: 6. 从半连接队列移除
Kernel->>AcceptQueue: 7. 加入全连接队列
Note over AcceptQueue: 连接已建立,等待accept()
Client->>Client: 8. connect()返回成功
Note over Client: 客户端认为连接成功
Note over App: 应用还没调用accept()
Note over AcceptQueue: 连接在队列中等待
App->>AcceptQueue: 9. accept()(10秒后)
AcceptQueue->>App: 10. 返回连接
Note over App: 应用拿到连接,可以通信了
关键时间点:
T1: 客户端发送SYN
T2: 服务器内核回复SYN+ACK
T3: 客户端发送ACK,三次握手完成 ✅
→ 客户端connect()返回
→ 连接进入全连接队列
T10: 服务器应用调用accept()
→ 从队列取出连接
→ accept()返回
结论:
连接在T3就建立了(内核完成)
accept()在T10才返回(应用层)
哈吉米:"所以accept()不参与三次握手,只是从队列里取连接?"
南北绿豆:"对!三次握手是内核完成的,应用层只负责取连接。"
🎯 全连接队列满了会怎样?
队列满的场景
全连接队列容量:128
场景:
1. 128个连接完成三次握手,进入队列
2. 应用还没调用accept()(处理慢)
3. 第129个连接完成三次握手
问题:队列满了,怎么办?
内核的处理策略
// Linux内核参数
sysctl -a | grep tcp_abort_on_overflow
net.ipv4.tcp_abort_on_overflow = 0 // 默认值
// 两种策略:
tcp_abort_on_overflow = 0:
→ 丢弃ACK(第三次握手的ACK)
→ 客户端认为连接成功
→ 但服务器没有把连接加入队列
→ 客户端发送数据 → 服务器回复RST
→ 连接断开
tcp_abort_on_overflow = 1:
→ 发送RST给客户端
→ 客户端connect()失败
→ 客户端可以重试
时序图(队列满):
sequenceDiagram
participant Client as 客户端
participant Kernel as 内核
participant Queue as 全连接队列(已满)
Client->>Kernel: 1. SYN
Kernel->>Client: 2. SYN + ACK
Client->>Kernel: 3. ACK
Kernel->>Queue: 4. 尝试加入队列
Queue->>Kernel: 5. 队列已满 ❌
alt tcp_abort_on_overflow = 0
Kernel->>Kernel: 6. 丢弃ACK
Note over Client: 客户端认为连接成功<br/>但服务器没有接受连接
Client->>Kernel: 7. 发送数据
Kernel->>Client: 8. RST(连接不存在)
Note over Client: 连接异常断开
else tcp_abort_on_overflow = 1
Kernel->>Client: 6. RST
Note over Client: 连接失败,可以重试
end
南北绿豆:"所以全连接队列满了,会导致连接失败或异常,必须调大backlog参数!"
🎯 backlog参数的设置
什么是backlog?
ServerSocket serverSocket = new ServerSocket(8080, 128);
↑
backlog参数
作用:
backlog:全连接队列的容量
示例:
backlog = 128
→ 全连接队列最多存128个已完成握手的连接
→ 第129个连接会被拒绝(或丢弃)
如何设置backlog?
推荐值:
backlog = 峰值QPS × 平均处理时间 × 2
示例:
峰值QPS:1000
平均处理时间:0.1秒
backlog = 1000 × 0.1 × 2 = 200
// 设置
ServerSocket serverSocket = new ServerSocket(8080, 200);
内核限制:
# Linux查看全连接队列最大值
sysctl -a | grep somaxconn
net.core.somaxconn = 128 # 默认128
# 调大
sysctl -w net.core.somaxconn=1024
# 永久生效(/etc/sysctl.conf)
net.core.somaxconn = 1024
实际队列大小:
实际大小 = min(backlog, somaxconn)
示例:
ServerSocket(8080, 500) // backlog=500
somaxconn = 128 // 内核限制
实际队列大小 = min(500, 128) = 128
🎯 SYN flood攻击
攻击原理
攻击者:
每秒发送10万个SYN包(伪造源IP)
服务器:
1. 收到SYN,放入半连接队列
2. 回复SYN+ACK
3. 等待ACK(但攻击者不发ACK)
4. 半连接队列满了
5. 无法处理正常请求 ❌
防御方案
方案1:SYN Cookie
# 开启SYN Cookie
sysctl -w net.ipv4.tcp_syncookies=1
# 原理:
1. 不把连接放入半连接队列
2. 用算法生成cookie(基于IP、端口、时间戳)
3. 把cookie编码到SYN+ACK的序列号中
4. 客户端回复ACK时,验证cookie
5. cookie正确 → 直接建立连接(绕过半连接队列)
方案2:调大半连接队列
# 查看半连接队列大小
sysctl -a | grep tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 1024 # 默认1024
# 调大
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
方案3:减少SYN+ACK重试次数
# SYN+ACK重试次数
sysctl -a | grep tcp_synack_retries
net.ipv4.tcp_synack_retries = 5 # 默认5次
# 减少重试(快速释放半连接队列)
sysctl -w net.ipv4.tcp_synack_retries=2
🎓 面试标准答案
题目:没有accept()方法,能建立TCP连接吗?
答案:
能!
原因:
-
三次握手在内核完成
- 客户端发SYN
- 服务器内核回SYN+ACK
- 客户端发ACK
- 三次握手完成,连接建立 ✅
-
连接进入全连接队列
- 建立的连接放入队列
- 等待accept()取走
-
accept()只是从队列取连接
- 不参与三次握手
- 队列空则阻塞等待
- 队列有连接则立即返回
结论:
- 三次握手:内核完成
- accept():从队列取连接
- 即使不调accept(),三次握手也能完成
问题:
- 不调accept(),连接堆积在队列
- 队列满了,新连接被拒绝
题目:全连接队列和半连接队列的区别?
答案:
| 队列 | 作用 | 大小 | 存储内容 |
|---|---|---|---|
| 半连接队列 | 存储正在握手的连接 | tcp_max_syn_backlog(1024) | SYN_RECEIVED状态的连接 |
| 全连接队列 | 存储已完成握手的连接 | min(backlog, somaxconn) | ESTABLISHED状态的连接 |
流程:
客户端SYN → 半连接队列(SYN_RECEIVED)
↓
三次握手完成
↓
全连接队列(ESTABLISHED)
↓
accept()取出
队列满的影响:
- 半连接队列满 → 丢弃SYN(SYN flood攻击)
- 全连接队列满 → 丢弃ACK或发RST
调优:
- backlog设置合理值(200-1024)
- somaxconn调大(1024-4096)
- 开启SYN Cookie(防攻击)
题目:为什么高并发场景下要调大backlog?
答案:
场景:
高并发(QPS=10000):
- 每秒10000个连接
- 应用处理速度:每秒5000个(accept() + 处理)
- 每秒堆积:10000 - 5000 = 5000个连接
如果backlog=128:
- 0.026秒后队列满(128 / 5000)
- 后续连接被拒绝 ❌
如果backlog=1024:
- 0.2秒后队列满
- 能缓冲更多连接 ✅
推荐值:
短连接(HTTP):
backlog = 峰值QPS × 0.1 = 1000 × 0.1 = 100
长连接(WebSocket):
backlog = 峰值并发连接数 × 0.1 = 10000 × 0.1 = 1000
🎉 结束语
晚上8点,哈吉米终于理解了TCP连接建立的真相。
哈吉米:"原来三次握手是内核完成的,accept()只是从队列取连接!"
南北绿豆:"对,内核维护了半连接队列和全连接队列,应用层只负责取连接。"
阿西噶阿西:"记住:即使不调accept(),三次握手也能完成,连接会堆积在队列中。"
哈吉米:"还有backlog参数要根据并发量设置,队列满了会丢弃连接。"
南北绿豆:"对,高并发场景一定要调大backlog和somaxconn!"
记忆口诀:
三次握手内核完成,accept只是取连接
半连接队列存握手中,全连接队列存已建立
backlog设置全连接队列大小,高并发要调大
队列满了连接被拒,somaxconn是上限
SYN Cookie防攻击,调优参数保高可用