扩展— TCP 全连接队列与 tcpdump 抓包

前言

在 Linux 网络编程开发中,listen() 函数 的第二个参数 backlog 是容易被初学者忽略的知识点。大多数开发者仅知道该参数连接排队相关,但不了解内核存在半连接队列、全连接队列两套独立管理结构,一旦并发连接量超过队列上限,会出现客户端连接超时、建链失败等问题。

本文结合完整 C++ TCP 服务端 / 客户端实验、数据结构图解、netstat状态观测、tcpdump命令行抓包,完整拆解 TCP 双队列底层逻辑,配套可复现的实操案例,所有内容基于本科计算机网络、Linux 系统编程体系,无复杂度解析,适合网络编程入门和知识学习。


一、TCP 两套连接队列

1.1 三次握手与双队列分工

TCP 建立连接需要三次握手,Linux 内核协议栈为监听端口维护两套独立队列,分别管理握手不同阶段的连接请求,两套队列各司其职,互不干扰:

  1. 半连接队列 存储服务器收到客户端SYN报文、回复SYN+ACK,但尚未收到客户端第三次握手ACK的连接。

  2. 全连接队列 客户端完成三次握手、连接进入ESTABLISHED状态,但应用程序未调用accept()从内核取出的连接。

1.2 backlog 参数

backlog仅约束全连接队列,结论:全连接队列最大有效长度 = backlog + 1

同时系统全局参数net.core.somaxconn会对队列上限做兜底限制,最终生效长度取min(backlog+1, somaxconn),线上高并发服务需同步调高该内核参数,避免队列提前溢出。

队列溢出后果:当全连接队列填满,新完成三次握手的连接无法存入队列,内核不会回复 RST 重置报文,客户端会持续重传第三次握手 ACK,最终出现连接超时失败;服务端侧新连接长期停留在SYN_RECV状态,无法转为ESTABLISHED。


二、TCP 双队列复现实验

2.1 设计思路

  1. 服务端逻辑:调用Bind绑定端口,Listen(2)设置 backlog=2,永久不调用 accept (),所有完成握手的连接全部积压在全连接队列。

  2. 客户端逻辑:循环发起 TCP 连接,不主动断开;

  3. 预期现象:backlog=2,全连接队列最大容量为 3,启动 3 个客户端可正常建立ESTABLISHED连接;启动第 4 个客户端时,第四个连接停留在SYN_RECV,无法进入全连接队列。

2.2 源代码

2.2.1 服务端 test_server.cc
cpp 复制代码
#include "tcp_socket.hpp"
#include <cstdio>
#include <unistd.h>

int main(int argc, char* argv[]) {
    // 参数校验:./test_server [ip] [port]
    if (argc != 3) {
        printf("Usage ./test_server [ip] [port]\n");
        return 1;
    }
    TcpSocket sock;
    bool ret = sock.Bind(argv[1], atoi(argv[2]));
    if (!ret) {
        printf("Bind failed\n");
        return 1;
    }
    // listen第二个参数backlog=2
    ret = sock.Listen(2);
    if (!ret) {
        printf("Listen failed\n");
        return 1;
    }
    // 核心:不调用accept,所有连接积压在内核全连接队列
    while (1) {
        sleep(1);
    }
    return 0;
}
2.2.2 客户端 test_client.cc
cpp 复制代码
#include "tcp_socket.hpp"
#include <cstdio>
#include <unistd.h>

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("Usage ./test_client [ip] [port]\n");
        return 1;
    }
    TcpSocket sock;
    bool ret = sock.Connect(argv[1], atoi(argv[2]));
    if (ret) {
        printf("connect ok\n");
    } else {
        printf("connect failed\n");
    }
    // 连接成功后持续持有,不断开
    while (1) {
        sleep(1);
    }
    return 0;
}
2.3 步骤与状态观测
  1. 编译运行服务端:./test_server 127.0.0.1 9090;

  2. 依次打开 3 个终端启动客户端:./test_client 127.0.0.1 9090;

  3. 执行netstat -an | grep 9090查看连接状态:

  • 服务端监听端口:tcp 0 0 0.0.0.0:9090 0.0.0.0:* LISTEN

  • 3 组双向ESTABLISHED连接,内核全连接队列未满;

  1. (4)启动第 4 个客户端,再次执行netstat 观测:新增一条服务端侧状态为SYN_RECV的记录,客户端侧显示ESTABLISHED。

原因:客户端已完成三次握手并收到服务端 SYN+ACK,本地连接置为 ESTABLISHED;但服务端全连接队列已满,无法将该连接从半连接队列迁移至全连接队列,服务端持续停留在SYN_RECV。


三、TCP 数据结构图解

3.1 层级关系

用户态进程到 TCP 传输控制块的内核层级调用链路:

  • task_struct:Linux 进程核心结构体,包含struct files_struct文件描述符管理结构;

  • fd_array :文件描述符数组,每个 fd 指向独立struct file;

  • struct file私有数据private_data绑定struct socket套接字结构;

  • struct socket 持有通用传输控制块struct sock;

  • struct sock为多态基类,衍生inet_sock、tcp_sock;所有 TCP 连接的收发缓冲区、半 / 全连接队列、状态标识,全部存储在tcp_sock结构体中,是内核管理 TCP 连接的底层载体。

3.2 sk_buff

sk_buff是 Linux 网络协议栈承载单个数据包的标准容器,网卡收发、TCP 队列排队的数据包全部封装为该结构,四个指针划分内存区域:

  • head:整块缓冲区内存起始地址;

  • data:当前有效协议数据起始,可前后移动,用于动态增删二层 / IP/TCP 头部;

  • tail:当前有效数据末尾;

  • end:缓冲区内存边界;

内存分区从前往后依次为:头空间、二层帧头、IP 头、TCP 头、应用层数据、尾空间。头部 / 尾部预留空闲空间,协议栈处理报文时无需拷贝数据,仅移动指针即可完成头部增减,提升网络性能。

图中nr_frags = 2代表数据包存在分片,分片数据通过frag_list链表挂载,适合大报文分段存储。


四、tcpdump 命令行抓包

tcpdump是 Linux 原生命令行抓包工具,无需图形界面,适合服务器线上故障排查,支持精准过滤 IP、端口、TCP 标志位,抓包文件可导出.pcap格式在 Wireshark 中可视化分析。

4.1 安装

  1. Ubuntu/Debian 系列

    bash 复制代码
    sudo apt-get update
    sudo apt-get install tcpdump
  2. CentOS/RHEL 系列

    bash 复制代码
    sudo yum install tcpdump

    注意:tcpdump 必须 root 权限运行,普通用户执行需加sudo前缀。

4.2 抓包命令分类

4.2.1 按网络接口
bash 复制代码
# 捕获所有网卡全部TCP报文
sudo tcpdump -i any tcp
# 仅捕获eth0网卡流量
sudo tcpdump -i eth0 tcp

参数说明:-i指定网络接口,any匹配本机全部网卡;末尾tcp过滤仅保留 TCP 协议报文。

4.2.2 按源/目的 IP 报文
bash 复制代码
# 捕获源IP 127.0.0.1的TCP包
sudo tcpdump src host 127.0.0.1 and tcp
# 捕获发往127.0.0.1的TCP包
sudo tcpdump dst host 127.0.0.1 and tcp
# 双向过滤:仅抓取127.0.0.1与192.168.1.10交互流量
sudo tcpdump src host 127.0.0.1 and dst host 192.168.1.10 and tcp
4.2.3 按端口过滤
bash 复制代码
# 抓取9090端口所有TCP流量(服务端监听端口)
sudo tcpdump port 9090 and tcp
4.2.4 抓包存储
bash 复制代码
# 将9090端口抓包数据保存至test.pcap
sudo tcpdump -i any port 9090 -w test.pcap
# 离线读取pcap文件分析数据包
tcpdump -r test.pcap

4.3 注意事项

  1. -n:不反向解析主机名、端口名称,避免云服务器 DNS 解析延迟、乱码主机名;线上抓包必加:

  2. -v/-vv/-vvv:逐级提升报文打印详细程度,查看 TCP 序列号、窗口大小、校验和;

  3. 第三次握手纯 ACK 报文数据长度为 0,无应用层负载。

结语

TCP 连接队列是网络编程底层核心基础,看似简单的listen函数背后涉及完整内核队列管理机制。本文通过可复现 C++ 实验代码、内核结构体图解、命令行抓包实验,覆盖理论、代码、观测完整链路。读者们可本地搭建实验环境,修改 backlog 参数重复观测队列溢出现象,加深对 TCP 内核双队列模型的理解。

相关推荐
byte_conn1 小时前
高压级联储能通信架构:CAN隔离与光纤中继的选型实战
网络·架构·制造·信息与通信
小小代码狗2 小时前
PHP 常用函数与变量参考文档
开发语言·安全·php
RisunJan2 小时前
Linux命令-skill(发送信号给进程)
linux·运维·服务器
潘正翔2 小时前
k8s基础_kubeadm搭建k8s集群
linux·运维·docker·云原生·容器·kubernetes
changjiahong2 小时前
lvs总结
服务器·数据库·lvs
看昭奚恤哭2 小时前
LWIP TCP滑动窗口为TCP ZeroWindow的解决方法
网络·tcp/ip·php
编程阿峰2 小时前
Python 环境配置(二)安装jupyter、matplotlib、numpy库
开发语言·python·jupyter·numpy·matplotlib
AC赳赳老秦2 小时前
CSDN 技术社区数据采集:OpenClaw 抓取公开技术热帖,生成领域技术热点周报
java·大数据·前端·数据库·python·php·openclaw
rcms152702692182 小时前
OWER-ONE SPF4G4D1P6V6 直流电源模块
网络