##本文详细解析了 Linux 服务器端 TCP 监听的完整流程,从 socket()、bind() 到 listen()。重点探讨了 半连接队列 (SYN Queue) 和 全连接队列 (Accept Queue) 的作用,以及 TCP_DEFER_ACCEPT 选项如何修改 listen() 的。{alertInfo}
由人工编写审核,非AI生成内容,请放心观看!
{getToc} $title={文章目录}
典型服务器端代码:
int sock = socket(AF_INET, SOCK_STREAM, 0);
bind(sock, ...);
listen(sock, backlog);
for (;;) {
int conn = accept(sock, ...);
handle(conn);
}socket() → bind() → listen() → accept() → read()/write()
listen() 主要作用:
- 让这个 socket 成为“监听 socket”
- 内核为此 socket 分配两个队列:
| 队列名 | Linux 名称 | 作用 |
|---|---|---|
| 半连接队列 | SYN queue(syn backlog) | 存放握手中的连接或未完成条件的连接 |
| 全连接队列 | accept queue | 存放已准备好可被 accept 的连接 |
- 客户端发送 SYN
- 内核放入 半连接队列 SYN queue
- 服务端回复 SYN/ACK
- 客户端发 ACK
- 内核确认 3 次握手完成,把连接放入: → accept queue
accept()被唤醒,返回新 FD
流程图:
客户端 服务器
SYN ───────► (入 SYN queue)
◄─────── SYN/ACK
ACK ───────► (握手完成 → accept queue)
↑
accept() returns
结论:
默认情况下,只要三次握手完成,就会进入 accept queue,accept 会被立即唤醒。
添加操作
listen 前后均可:
int v = 1;
setsockopt(sock, IPPROTO_TCP, TCP_DEFER_ACCEPT, &v, sizeof(v));行为改变点:
握手完成后,不会加入 accept queue。
流程变为:
- SYN → 入 SYN queue
- SYN/ACK
- ACK → 握手完成
- 连接保持在“半完成状态”,不进入 accept queue
- 客户端发送数据时: → 内核才把连接移入 accept queue
- accept() 才会返回新连接 完整流程:
客户端 服务器
SYN ─────────────► (入 SYN queue)
◄───────────── SYN/ACK
ACK ─────────────► (握手完成,等待数据 ✘不进入 accept queue)
DATA ─────────────► (收到数据 → 入 accept queue)
↑
accept() returns here
listen 的正常行为(不 defer)
listen 的作用是:
- 握手完成 → 放到 accept queue → 唤醒 accept() 对于典型 HTTP server,有 70~95% 的连接:
- “握手结束后立即发送请求”
- 但内核唤醒 accept() 需要一次调度(上下文切换),即使数据还没来
listen 的行为被修改为:
- 握手完成不会立即进入 accept queue
- 必须客户端发送数据才算真正准备好
优势:
- 减少 accept() 被无意义唤醒
- 减少上下文切换(性能提升)
- 减少空连接占用 worker 资源
这是 Nginx、Apache、HAProxy、libevent 等常用的大规模服务器优化手段。
| 阶段 | 默认行为 | 开启 TCP_DEFER_ACCEPT 后 |
|---|---|---|
| listen 后队列 | 创建 SYN queue 和 accept queue | 相同 |
| 握手完成后是否进 accept queue | 立即进入 | 不进入 |
| accept() 是否立即被唤醒 | 会 | 不会 |
| 什么时候进入 accept queue | 握手完成 | 收到数据之后 |
| accept() 返回时机 | 握手完成时 | 数据到来时 |
用一句话总结:
TCP_DEFER_ACCEPT 修改了 listen 的“连接就绪条件”,
从“握手完成”变成“握手完成 + 已有数据到达”。
setsockopt(sock, IPPROTO_TCP, TCP_DEFER_ACCEPT, &timeout, sizeof(timeout));timeout参数(Linux 下是秒数)用于设置 内核等待数据到来的最大时间。- 当开启后,流程变化如下:
- listen + SYN 队列 同上。
- 三次握手完成
- 连接完成,但 内核不会立即将其放入 accept 队列。
- 连接处于“半就绪”状态,等待客户端发送应用层数据。
- 数据到达
- 当客户端在连接上发送数据后,内核才会把连接放入 已完成队列,此时
accept()才会返回。
- 当客户端在连接上发送数据后,内核才会把连接放入 已完成队列,此时
- 超时机制
- 如果客户端在
timeout秒内没有发送数据,内核会丢弃连接(类似SYN_RECV超时)。
- 如果客户端在
TCP 服务端状态时序图(含 TCP_DEFER_ACCEPT)
版权声明:感谢您的阅读,资源整理自网络,如果您发现任何侵权行为,请联系 理科生网 管理人员,管理员将及时删除侵权内容。否则均为 理科生网 原创内容,转载时请务必以超链接(而非纯文本链接)标注来源于理科生网及本文完整链接,感谢!{alertInfo}Ahmedabad