没有accept方法,能建立TCP连接吗?

摘要:从一次"服务器没调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连接吗?

答案

能!

原因

  1. 三次握手在内核完成

    • 客户端发SYN
    • 服务器内核回SYN+ACK
    • 客户端发ACK
    • 三次握手完成,连接建立 ✅
  2. 连接进入全连接队列

    • 建立的连接放入队列
    • 等待accept()取走
  3. 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防攻击,调优参数保高可用