前言
TCP 连接建立是网络编程中最核心的机制之一。当我们在编写高性能服务器时,经常会遇到 "连接超时"、"accept 阻塞"、"队列溢出" 等问题。要解决这些问题,必须深入理解内核在底层是如何管理连接队列的。
本文将系统性地剖析 TCP 服务端的 listen () 函数、连接队列的创建与管理,以及客户端 connect () 的内核执行流程。这些知识点既是高级后端开发的核心技能,也是系统架构师面试中的高频考点。
第一章 listen () 函数的内核之旅
1.1 从用户态到内核态
当应用代码调用 listen(sockfd, backlog) 时,背后经历了复杂的内核交互:
┌─────────────────────────────────────────────────────────────┐
│ listen() 调用的完整路径 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户态 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ listen(sockfd, backlog); │ │
│ │ │ │ │
│ │ │ sockfd = 1024 (整数文件描述符) │ │
│ │ │ backlog = 128 (排队最大连接数) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ 系统调用陷入内核 │
│ 内核态 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 步骤 1:通过 fd 找到内核对象 │ │
│ │ sockfd_lookup_light(fd, &err, &fput_needed) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 找到 struct socket *sock │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 步骤 2:进入协议栈 │ │
│ │ sock->ops->listen(sock, backlog) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ inet_listen() │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 listen () 的核心职责
listen() 函数在内核中做了三件关键事情:
┌─────────────────────────────────────────────────────────────┐
│ listen() 的三大任务 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 任务一:状态检查 │ │
│ │ │ │
│ │ 确保 socket 处于正确状态: │ │
│ │ - 必须是 SOCK_STREAM 类型(面向连接) │ │
│ │ - 状态必须是 SS_UNCONNECTED │ │
│ │ - 还未执行过 listen │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 任务二:设置全连接队列长度 │ │
│ │ │ │
│ │ 内核会取 min(用户backlog, somaxconn) │ │
│ │ 作为最终的全连接队列长度 │ │
│ │ 防止单个应用占用过多资源 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 任务三:初始化连接队列 │ │
│ │ │ │
│ │ 调用 inet_csk_listen_start() │ │
│ │ - 分配半连接队列(哈希表) │ │
│ │ - 初始化全连接队列(链表) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
第二章 两个核心队列:半连接与全连接
2.1 TCP 三次握手与队列的关系
TCP 建立连接的过程可以划分为两个关键阶段,每个阶段对应一个队列:
┌─────────────────────────────────────────────────────────────┐
│ TCP 三次握手与连接队列 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 │
│ │ │
│ │─────── SYN ──────────────→│ │
│ │ │ 半连接队列 │
│ │ │ (SYN Queue) │
│ │ │ 状态: SYN_RECV │
│ │◀────── SYN+ACK ──────────│ │
│ │ │ │
│ │─────── ACK ──────────────→│ │
│ │ │ │
│ │ │ 全连接队列 │
│ │ │ (Accept Queue) │
│ │ │ 状态: ESTABLISHED │
│ │ │ │
│ │◀════════════════════════════│ │
│ │ 连接建立完成 │ │
│ │ │ │
│ 服务端 │
│ │ │ │
│ │ accept() 取出连接 │
│ │ ▼ │
│ │ 应用处理 │
│ │
└─────────────────────────────────────────────────────────────┘
2.2 半连接队列(SYN Queue)
┌─────────────────────────────────────────────────────────────┐
│ 半连接队列详解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 队列状态: │
│ - 收到了客户端的 SYN │
│ - 已发送 SYN+ACK │
│ - 等待客户端的最终 ACK │
│ - 连接状态:SYN_RECV │
│ │
│ 存储结构:哈希表(syn_table) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 哈希表 (syn_table) │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 桶 0: [req1] → [req2] │ │ │
│ │ │ 桶 1: [req3] │ │ │
│ │ │ 桶 2: [req4] → [req5] → [req6] │ │ │
│ │ │ ... │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 为什么用哈希表? │ │
│ │ - 此时连接还未建立,没有分配正式 socket fd │ │
│ │ - 需要通过 {源IP, 源端口} 快速查找请求 │ │
│ │ - 哈希表提供 O(1) 的查找效率 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2.3 全连接队列(Accept Queue)
┌─────────────────────────────────────────────────────────────┐
│ 全连接队列详解 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 队列状态: │
│ - 三次握手已完成 │
│ - 连接状态:ESTABLISHED │
│ - 等待应用程序调用 accept() 取走 │
│ │
│ 存储结构:双向链表(FIFO) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ rskq_accept_head rskq_accept_tail │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ conn 1 │ → │ conn 2 │ → │ conn 3 │ → ... │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ 最早完成 最后完成 │ │
│ │ 握手优先被 握手靠后被 │ │
│ │ accept() 取走 accept() 取走 │ │
│ │ │ │
│ │ 为什么用链表? │ │
│ │ - 此时连接已建立,可以分配正式 socket fd │ │
│ │ - 只需要按顺序存储,先进先出 │ │
│ │ - 链表插入和删除都是 O(1) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2.4 两种队列的核心对比
┌─────────────────────────────────────────────────────────────┐
│ 半连接队列 vs 全连接队列 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 半连接队列 │ │
│ │ 存储内容 request_sock(连接请求) │ │
│ │ 数据结构 哈希表 │ │
│ │ 查找方式 {源IP, 源端口} 哈希查找 │ │
│ │ 连接状态 SYN_RECV │ │
│ │ 生命周期 收到SYN → 收到最终ACK │ │
│ │ 内存分配 动态分配(每个请求单独分配) │ │
│ │ 队列长度 min(backlog, tcp_max_syn_backlog) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 全连接队列 │ │
│ │ 存储内容 established_request(已建立连接) │ │
│ │ 数据结构 双向链表 │ │
│ │ 查找方式 按顺序遍历(先进先出) │ │
│ │ 连接状态 ESTABLISHED │ │
│ │ 生命周期 握手完成 → accept() 被调用 │ │
│ │ 内存分配 动态分配(握手时分配) │ │
│ │ 队列长度 min(backlog, somaxconn) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
第三章 队列长度的计算逻辑
3.1 全连接队列长度计算
┌─────────────────────────────────────────────────────────────┐
│ 全连接队列长度计算 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 计算公式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 最终长度 = min(用户传入的 backlog, somaxconn) │ │
│ │ │ │
│ │ 其中 somaxconn = /proc/sys/net/core/somaxconn │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 示例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 代码:listen(fd, 128); │ │
│ │ 系统:somaxconn = 128 │ │
│ │ │ │
│ │ 计算:min(128, 128) = 128 │ │
│ │ 结果:全连接队列长度为 128 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 代码:listen(fd, 1000); │ │
│ │ 系统:somaxconn = 128 │ │
│ │ │ │
│ │ 计算:min(1000, 128) = 128 │ │
│ │ 结果:全连接队列长度仍为 128(你设的1000被忽略了) │ │
│ │ │ │
│ │ 这就是为什么有时改了 backlog 却没生效! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 半连接队列长度计算
┌─────────────────────────────────────────────────────────────┐
│ 半连接队列长度计算 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 计算公式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 取最小值:min(backlog, tcp_max_syn_backlog) │ │
│ │ │ │
│ │ 2. 向上取整到 2 的幂次方: │ │
│ │ roundup_pow_of_two(min_value) │ │
│ │ │ │
│ │ 其中 tcp_max_syn_backlog = │ │
│ │ /proc/sys/net/ipv4/tcp_max_syn_backlog │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么向上取整? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 哈希表需要 2 的幂次方作为容量,以便使用位运算 │ │
│ │ 而非取模运算(更高效) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 示例: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 代码 backlog = 10 │ │
│ │ 系统 tcp_max_syn_backlog = 128 │ │
│ │ │ │
│ │ 步骤1:min(10, 128) = 10 │ │
│ │ 步骤2:roundup_pow_of_two(10) = 16 │ │
│ │ │ │
│ │ 结果:半连接队列哈希表容量为 16 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
3.3 队列长度调优参数汇总
┌─────────────────────────────────────────────────────────────┐
│ 连接队列相关内核参数 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 参数 路径 │ │
│ │ ─────────────────────────────────────────────────── │ │
│ │ somaxconn /proc/sys/net/core/ │ │
│ │ tcp_max_syn_backlog /proc/sys/net/ipv4/ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 调优建议: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ # 设置全连接队列上限 │ │
│ │ echo 65535 > /proc/sys/net/core/somaxconn │ │
│ │ 或 │ │
│ │ sysctl -w net.core.somaxconn=65535 │ │
│ │ │ │
│ │ # 设置半连接队列上限 │ │
│ │ sysctl -w net.ipv4.tcp_max_syn_backlog=2048 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
第四章 队列初始化与内存分配
4.1 reqsk_queue_alloc 函数
当调用 listen() 时,内核需要为两个队列分配内存:
┌─────────────────────────────────────────────────────────────┐
│ reqsk_queue_alloc 执行流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ inet_csk_listen_start() │
│ │ │
│ ▼ │
│ reqsk_queue_alloc(&icsk->icsk_accept_queue, size) │
│ │ │
│ ├──► 分配 listen_sock 结构体 │
│ ├──► 分配半连接队列哈希表内存 │
│ └──► 初始化全连接队列链表头 │
│ │
│ 内存布局: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ struct listen_sock { │ │
│ │ u32 max_syn_backlog; // 半连接队列最大长度 │ │
│ │ u32 syn_table[...]; // 哈希表数组 │ │
│ │ }; │ │
│ │ │ │
│ │ struct request_sock_queue { │ │
│ │ struct request_sock *rskq_accept_head; │ │
│ │ struct request_sock *rskq_accept_tail; │ │
│ │ ... │ │
│ │ }; │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 连接请求的动态分配
┌─────────────────────────────────────────────────────────────┐
│ 请求对象的生命周期 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 半连接队列中存储的是指针,而非完整的 request_sock: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 哈希表条目 (syn_table) │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ syn_table[0] ──→ [指针1] ──→ req1 │ │ │
│ │ │ syn_table[1] ──→ [指针2] ──→ req2 │ │ │
│ │ │ syn_table[2] ──→ NULL │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 每个 request_sock 对象是什么时候分配的? │ │
│ │ → 收到客户端 SYN 包时动态分配 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 全连接队列中存储的也是指针: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 全连接队列 (链表) │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ [指针] → [指针] → [指针] → NULL │ │ │
│ │ │ ↓ ↓ ↓ │ │ │
│ │ │ req3 req4 req5 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 三次握手完成时: │ │
│ │ → 将半连接中的 req 移动到全连接队列 │ │
│ │ → req 中包含新分配的 socket 信息 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
第五章 客户端 connect () 的内核执行流程
5.1 connect () 系统调用路径
┌─────────────────────────────────────────────────────────────┐
│ connect() 的完整调用链 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户态 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ connect(sockfd, (struct sockaddr *)&addr, len); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ 系统调用 │
│ 内核态 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. sockfd_lookup_light(fd) │ │
│ │ 找到 struct socket *sock │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 2. sock->ops->connect(sock, addr, ...) │ │
│ │ 协议无关层分发 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 3. inet_stream_connect(sock, addr, ...) │ │
│ │ TCP 协议族入口 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 4. tcp_v4_connect(sock, addr, ...) │ │
│ │ TCPv4 专用连接 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 5. tcp_connect(skb) │ │
│ │ 发送 SYN 包 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
5.2 tcp_v4_connect 的核心逻辑
┌─────────────────────────────────────────────────────────────┐
│ tcp_v4_connect 执行流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 1:加锁防止并发 │ │
│ │ lock_sock(sk); │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 2:状态检查 │ │
│ │ │ │
│ │ if (sk->sk_state != TCP_CLOSE) │ │
│ │ return -EAlready...; │ │
│ │ │ │
│ │ 确保 socket 处于初始状态 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 3:路由查找 │ │
│ │ │ │
│ │ ip_route_connect(...) │ │
│ │ │ │ │
│ │ ├──► 确定目标 IP 地址 │ │
│ │ ├──► 查找路由表 │ │
│ │ └──► 选择出口网卡和网关 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 4:端口分配(如未绑定) │ │
│ │ │ │
│ │ inet_hash_connect(...) │ │
│ │ │ │ │
│ │ └──► 自动选择可用本地端口 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 5:初始化 TCP 参数 │ │
│ │ │ │
│ │ tcp_connect_init(sk) │ │
│ │ │ │ │
│ │ ├──► 设置窗口大小 │ │
│ │ ├──► 计算 MSS │ │
│ │ └──► 初始化序号 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 6:发送 SYN 包 │ │
│ │ │ │
│ │ tcp_connect(sk) │ │
│ │ │ │ │
│ │ ├──► 构建 SYN 数据包 │ │
│ │ ├──► 放入发送队列 │ │
│ │ ├──► 启动重传定时器 │ │
│ │ └──► 触发网络发送 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
5.3 SYN 包发送与重传机制
┌─────────────────────────────────────────────────────────────┐
│ SYN 包的发送与重传 │
├─────────────────────────────────────────────────────────────┤
│ │
│ tcp_connect() 内部实现: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 申请 SKB(数据包缓冲区) │ │
│ │ skb = alloc_skb(...); │ │
│ │ │ │
│ │ 2. 构建 TCP 头部 │ │
│ │ th = tcp_hdr(skb); │ │
│ │ th->source = 本地端口; │ │
│ │ th->dest = 目标端口; │ │
│ │ th->syn = 1; // 标记为 SYN │ │
│ │ │ │
│ │ 3. 加入发送队列 │ │
│ │ sk_stream_add_to_write_queue(sk, skb); │ │
│ │ │ │
│ │ 4. 启动重传定时器 │ │
│ │ inet_csk_reset_xmit_timer(sk, │ │
│ │ ICSK_TIME_RETRANS, │ │
│ │ tcp_paws_to = TCP_TIMEOUT_INIT); │ │
│ │ │ │
│ │ 5. 发送 │ │
│ │ tcp_transmit_skb(sk, skb, 1, GFP_KERNEL); │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 重传定时器: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 初始超时时间 (RTO): │ │
│ │ - Linux 3.10+:默认 1 秒 │ │
│ │ - Linux 2.6.x:默认 3 秒 │ │
│ │ │ │
│ │ 超时行为: │ │
│ │ 1. 第一次超时 → 重发 SYN │ │
│ │ 2. 第二次超时 → 再次重发 │ │
│ │ 3. 重复 tcp_syn_retries 次后放弃 │ │
│ │ │ │
│ │ 最终结果:ETIMEDOUT │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
第六章 端口分配与复用机制
6.1 inet_hash_connect 函数
当调用 connect() 但未使用 bind() 绑定端口时,内核会自动分配一个本地端口:
┌─────────────────────────────────────────────────────────────┐
│ 自动端口分配流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 1:检查是否已绑定 │ │
│ │ │ │
│ │ snum = inet_sk(sk)->inet_num; │ │
│ │ │ │
│ │ if (snum != 0) │ │
│ │ return; // 用户已绑定,直接使用 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 2:获取可用端口范围 │ │
│ │ │ │
│ │ inet_get_local_port_range(&low, &high); │ │
│ │ │ │
│ │ 默认范围:32768 ~ 61000(约 2.8 万个端口) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 3:计算随机偏移量 │ │
│ │ │ │
│ │ offset = sin->sin_addr.s_addr + │ │
│ │ sin->sin_port; │ │
│ │ │ │
│ │ offset ^= (offset >> 16); │ │
│ │ offset ^= (offset >> 8); │ │
│ │ offset %= remaining; │ │
│ │ │ │
│ │ 目的:避免每次从同一位置开始遍历 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 步骤 4:遍历寻找可用端口 │ │
│ │ │ │
│ │ for (i = 0; i < remaining; i++) { │ │
│ │ port = low + (i + offset) % remaining; │ │
│ │ // 检查 port 是否可用 │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
6.2 端口可用性检查
┌─────────────────────────────────────────────────────────────┐
│ 端口可用性判断流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 检查 1:是否在保留端口列表中 │ │
│ │ │ │
│ │ /proc/sys/net/ipv4/ip_local_reserved_ports │ │
│ │ │ │
│ │ 如果 port 在保留列表中 → 跳过 (continue) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 检查 2:查找 bind 哈希表 │ │
│ │ │ │
│ │ bhash = inet_bhashfn(port); │ │
│ │ 检查 bhash_table[bhash] 中是否有冲突 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 检查 3:深度检查(即使在 bhash 中存在) │ │
│ │ │ │
│ │ check_established(sk, port, ...) │ │
│ │ │ │ │
│ │ ├──► 遍历已有连接 │ │
│ │ ├──► 比较四元组 │ │
│ │ └──► 四元组不冲突 → 可复用端口 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
6.3 四元组与端口复用
这是理解 "单机 65535 连接限制" 的关键:
┌─────────────────────────────────────────────────────────────┐
│ 四元组与连接唯一性 │
├─────────────────────────────────────────────────────────────┤
│ │
│ TCP 连接由四元组唯一标识: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ {源IP, 源端口, 目的IP, 目的端口} │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 错误理解: │ │
│ │ │ │
│ │ "单机最多 65535 个连接" │ │
│ │ │ │
│ │ 这是把端口数当作连接数的上限 │ │
│ │ 但忽略了 IP 和端口的组合关系 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 正确理解: │ │
│ │ │ │
│ │ 每个 TCP 连接必须唯一: │ │
│ │ {源IP, 源端口, 目的IP, 目的端口} 唯一 │ │
│ │ │ │
│ │ 假设本机 IP = 192.168.1.100 │ │
│ │ │ │
│ │ 连接 1: {192.168.1.100, 10000, 10.0.0.1, 80} │ │
│ │ 连接 2: {192.168.1.100, 10001, 10.0.0.1, 80} │ │
│ │ 连接 3: {192.168.1.100, 10002, 10.0.0.1, 80} │ │
│ │ ... │ │
│ │ │ │
│ │ 端口不同 → 不同连接 ✓ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 复用示例: │ │
│ │ │ │
│ │ 连接 1: {192.168.1.100, 10000, 10.0.0.1, 80} │ │
│ │ 连接 2: {192.168.1.100, 10000, 10.0.0.2, 80} │ │
│ │ │ │
│ │ 源端口相同 (10000),但目标 IP 不同! │ │
│ │ 四元组不同 → 不同连接 ✓ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
6.4 打破连接数限制
┌─────────────────────────────────────────────────────────────┐
│ 如何突破单机连接数限制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 限制因素一:端口范围 │ │
│ │ │ │
│ │ 扩大 ip_local_port_range: │ │
│ │ $ sysctl -w net.ipv4.ip_local_port_range="1024 65535" │ │
│ │ │ │
│ │ 效果:可用端口从 ~2.8 万扩展到 ~6.4 万 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 限制因素二:目标服务器 │ │
│ │ │ │
│ │ 连接到不同目标 → 端口可复用 │ │
│ │ │ │
│ │ 连接 1: {本地IP, 端口, 目标1, 80} │ │
│ │ 连接 2: {本地IP, 端口, 目标2, 80} │ │
│ │ 连接 3: {本地IP, 端口, 目标3, 80} │ │
│ │ ... │ │
│ │ │ │
│ │ 同一端口可以同时连接任意数量的不同服务器 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 限制因素三:绑定多个 IP │ │
│ │ │ │
│ │ 给网卡绑定多个 IP 地址: │ │
│ │ $ ip addr add 192.168.1.101/24 dev eth0 │ │
│ │ $ ip addr add 192.168.1.102/24 dev eth0 │ │
│ │ │ │
│ │ IP 翻倍,连接数翻倍! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 实战案例:Nginx 反向代理 │ │
│ │ │ │
│ │ 单机理论最大连接数: │ │
│ │ = 可用端口数 × 目标服务器数 × IP 数 │ │
│ │ = 64511 × 100 × 10 = 6451 万+ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
第七章 常见问题与排查
7.1 连接超时问题
┌─────────────────────────────────────────────────────────────┐
│ 连接超时排查流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 症状:客户端 connect() 返回 ETIMEDOUT │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 可能原因 1:半连接队列满 │ │
│ │ │ │
│ │ 服务端收到大量 SYN,但处理不过来 │ │
│ │ → SYN 丢失 │ │
│ │ → 客户端等待超时 │ │
│ │ │ │
│ │ 排查命令: │ │
│ │ $ netstat -s | grep -i "SYN" │ │
│ │ $ ss -s | grep "SYNRECV" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 可能原因 2:SYN 重试次数过多 │ │
│ │ │ │
│ │ 默认重试 6 次,每次等待时间翻倍 │ │
│ │ 总等待时间可能超过 60 秒 │ │
│ │ │ │
│ │ 优化建议: │ │
│ │ $ sysctl -w net.ipv4.tcp_syn_retries=3 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 可能原因 3:网络不可达 │ │
│ │ │ │
│ │ $ ping -c 3 <目标IP> │ │
│ │ $ traceroute <目标IP> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
7.2 连接被拒绝问题
┌─────────────────────────────────────────────────────────────┐
│ 连接被拒绝排查流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 症状:客户端 connect() 返回 ECONNREFUSED │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 可能原因 1:全连接队列满 │ │
│ │ │ │
│ │ 三次握手完成了,但 accept() 太慢 │ │
│ │ → 服务端无法完成连接 │ │
│ │ → 发送 RST │ │
│ │ │ │
│ │ 排查命令: │ │
│ │ $ ss -ltn | grep -i "listen" │ │
│ │ $ cat /proc/sys/net/core/somaxconn │ │
│ │ │ │
│ │ 优化建议: │ │
│ │ $ sysctl -w net.core.somaxconn=65535 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 可能原因 2:进程未监听该端口 │ │
│ │ │ │
│ │ 服务端程序未启动或绑定到错误端口 │ │
│ │ │ │
│ │ 排查命令: │ │
│ │ $ netstat -tlnp | grep <端口> │ │
│ │ $ ss -tlnp | grep <端口> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 可能原因 3:防火墙拦截 │ │
│ │ │ │
│ │ $ iptables -L -n │ │
│ │ $ firewall-cmd --list-all │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
7.3 队列状态监控命令
┌─────────────────────────────────────────────────────────────┐
│ 连接队列监控命令速查 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 查看监听队列状态: │ │
│ │ │ │
│ │ $ ss -ltn │ │
│ │ State Recv-Q Send-Q Local Address │ │
│ │ LISTEN 0 128 *:8080 │ │
│ │ │ │
│ │ Recv-Q: 全连接队列当前长度 │ │
│ │ Send-Q: 对端未确认的字节数 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 查看半连接队列(SYN_RECV): │ │
│ │ │ │
│ │ $ ss -tan state syn-recv | head -20 │ │
│ │ $ netstat -ant | grep SYN_RECV | wc -l │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 查看系统统计: │ │
│ │ │ │
│ │ $ ss -s │ │
│ │ Total: 12345 (kernel 12350) │ │
│ │ TCP: 1234 (estab 1000, timewait 200) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 查看丢包统计: │ │
│ │ │ │
│ │ $ netstat -s | grep -i "overflow\|dropped" │ │
│ │ $ ss -s | grep "SYN" │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
第八章 完整流程图总结
8.1 服务端 listen () 到 accept () 完整流程
┌─────────────────────────────────────────────────────────────┐
│ TCP 服务端完整连接处理流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 应用层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ socket() → bind() → listen() → accept() → 处理 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 内核(监听启动) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ listen(backlog=128) │ │
│ │ │ │ │
│ │ ├──► 设置全连接队列长度 = min(128, somaxconn) │ │
│ │ ├──► 分配半连接队列(哈希表) │ │
│ │ └──► 初始化全连接队列(链表) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 内核(三次握手) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 收到 SYN ──→ 进入半连接队列 ──→ 发送 SYN+ACK │ │
│ │ │ │ │
│ │ │ │ │
│ │ 收到 ACK ──→ 移入全连接队列 ──→ 连接建立完成 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 应用层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ accept() ──→ 从全连接队列取出连接 │ │
│ │ │ │ │
│ │ └──► 返回客户端 socket fd │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
8.2 客户端 connect () 到数据交互完整流程
┌─────────────────────────────────────────────────────────────┐
│ TCP 客户端完整连接建立流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 应用层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ socket() → connect() → send()/recv() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 内核 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ connect() │ │
│ │ │ │ │
│ │ ├──► 查找路由 │ │
│ │ ├──► 分配本地端口(自动或 bind) │ │
│ │ ├──► 初始化 TCP 参数 │ │
│ │ ├──► 发送 SYN ────────────────────────────→ │ │
│ │ │ │ │
│ │ └──► 启动重传定时器 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 网络 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ SYN ───────────────────────────────────────────→ │ │
│ │ ◀── SYN+ACK │ │
│ │ ACK ───────────────────────────────────────────→ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 内核 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 收到 SYN+ACK │ │
│ │ │ │ │
│ │ ├──► 取消重传定时器 │ │
│ │ ├──► 进入 ESTABLISHED 状态 │ │
│ │ └──► 通知应用层 connect() 返回 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 应用层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ connect() 返回成功! │ │
│ │ │ │ │
│ │ └──► 可以开始 send()/recv() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘