TCP 连接队列解析:从 listen () 到 connect ()

前言

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()                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

0voice · GitHub

相关推荐
久绊A1 小时前
Copy Fail Linux内核提权漏洞(CVE-2026-31431)
linux·运维·服务器
剑神一笑1 小时前
Linux grep 命令深度解析:从正则表达式到性能优化
linux·运维·正则表达式
Xpower 171 小时前
从PHM到AI Agent-如何用OpenClaw构建设备健康诊断智能体
网络·人工智能·学习·算法
苏宸啊1 小时前
linux缓冲区的理解
linux
Bert.Cai1 小时前
Linux bc命令详解
linux·运维·服务器
rjszcb1 小时前
Linux.之系统性能监控脚本, CPU、内存、DDR、CMA、ISP、MPP、ION、DRM、进程
linux·服务器
2301_780789661 小时前
2025年服务器漏洞生存指南:从应急响应到长效免疫的实战框架
网络·安全·web安全·架构·ddos
桌面运维家1 小时前
Linux磁盘IO调度器配置技巧 提升系统读写性能
linux·运维·服务器
xcjbqd01 小时前
SAP硬件选择详解:服务器、存储与网络的全面解析
运维·服务器·网络