网络数据包环形缓存捕获技术:原理、设计与实现(C/C++代码实现)

本文用通俗易懂的方式,讲解如何把网络原始数据包实时捕获到环形缓冲区中 ------ 这是网络安全、流量监控、抓包分析领域的核心技术,不依赖复杂框架,纯 C 语言实现轻量级、高性能的数据包缓存方案,解决高流量下抓包丢包、存储卡顿的问题。

一、这东西到底解决了什么问题?

想象你是一名网络管理员,某天服务器突然出问题,你想查原因,但故障只持续了几秒钟。等你反应过来启动抓包工具,黄花菜都凉了。或者你24小时不间断抓包,硬盘很快就被塞爆。

这时候你就需要一种"循环录音带"式的抓包方案:一直在录,但只保留最近的一段时间/一定量的数据。当故障发生时,你只要按下"保存"键,就能把刚才那段数据存下来。这就是环形缓冲区(Ring Buffer)的核心思想。

二、核心概念:什么是环形缓冲区?

2.1 生活中的类比

想象一个只能容纳10个球的管道:

  • 普通队列:球从左边进,从右边出。管道满了就塞不进去,必须等前面的球出去
  • 环形缓冲区:管道是环状的,装满后继续放新球,新球会把最老的球顶掉

这就是"先进先出,满了覆盖"的机制。

2.2 数据结构长什么样?

c 复制代码
struct ringbuf {
    size_t size_max;     // 缓冲区最大容量(比如50MB)
    size_t size_curr;    // 当前已用空间
    size_t num_elems;    // 当前存储的元素个数
    
    struct r_list *first; // 最老的元素(下一个要被覆盖的)
    struct r_list *last;  // 最新的元素
};

// 每个元素是个链表节点
struct r_list {
    void *elem;          // 实际数据(这里是网络包)
    size_t size;         // 数据大小
    struct r_list *prev;
    struct r_list *next;
};

关键点 :用双向链表实现,插入删除都是 O(1) 复杂度。

三、系统架构全景图

c 复制代码
...
struct ringbuf {
	size_t size_max;	
	size_t size_curr;	
	size_t num_elems;

	struct r_list {
		void *elem;
		size_t size;
		struct r_list *prev;
		struct r_list *next;
	} *first;

	struct r_list *last;
};



extern void *ringbuf_last(struct ringbuf *, size_t *);
extern int ringbuf_resize(struct ringbuf *, size_t);
extern void *ringbuf_first(struct ringbuf *, size_t *);
extern int ringbuf_add(struct ringbuf *, void *, size_t);
extern struct ringbuf *ringbuf_init(size_t);
extern const void *ringbuf_peek_last(struct ringbuf *);
extern const void *ringbuf_peek_first(struct ringbuf *);
...
static void usage(const char *pname)
{
	printf("\n-=[ Capture packets into a fixed size ring buffer ]=-\n");
	printf("Usage: %s <dumpdir> [Option(s)] [expression]\n", pname);
	printf("Buffer will be written to <dumpdir> when SIGUSR1 is received\n");
	printf("Options:\n");
	printf("  -d         - Debug, do not become daemon\n");
	printf("  -f logfile - Logfile, default is %s\n", LOGFILE);
	printf("  -i iface   - Listen for packets on interface iface\n");
	printf("  -m max     - Maximum size of packet buffer, default is %s bytes\n", 
		str_hsize(DEFAULT_MAX_SIZE_BYTES));
	printf("  -p pidfile - PID file, default is %s\n", PIDFILE);
	printf("  -P         - Do not listen in promiscuous mode\n");
	printf("  -v         - Be verbose, repeat to increase\n");
	printf("\n");
	exit(EXIT_FAILURE);
}



int main(int argc, char *argv[])
{
...

	if ((argv[1] == NULL) || (argv[1][0] == '-'))
		usage(opt.argv0);

	opt.dumpdir = argv[1];
	argc--;
	argv++;

	if (!isdir(opt.dumpdir))
		exit(EXIT_FAILURE);

	while ( (i = getopt(argc, argv, "dvp:m:i:Pf:")) != -1) {
		switch(i) {
			case 'v': opt.verbose++; break;
			case 'P': opt.promisc = 0; break;
			case 'f': opt.logfile = optarg; break;
			case 'm':
				if ( (opt.ringbuf_max = str_to_size(optarg)) == 0)
					errx("Failed to convert max buffer size\n");
				break;
			case 'p': opt.pidfile = optarg; break;
			case 'd': opt.debug = 1; break;
			case 'i': opt.iface = optarg; break;
			default: usage(opt.argv0);
		}
	}

	if (opt.debug == 0) {
        int fd;

		if (daemonize() < 0)
			exit(EXIT_FAILURE);
	
		/* printf("[%d]\n", getpid()); */
        if ( (fd = open(opt.logfile, O_RDWR|O_CREAT|O_APPEND, 0600)) < 0)
            err_errnox("open(%s)\n", opt.logfile);

        fflush(stdout);
        fflush(stderr);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);

		logpid(opt.pidfile);
		atexit(unlink_pidfile);
    }


    close(STDIN_FILENO);
    for (i=STDERR_FILENO+1; i<1024; i++)
        close(i);
		
	verbose(0, "+-+-+-+-+-+ Capture Started +-+-+-+-+-+\n");

	if ( (cap = cap_open(opt.iface, opt.promisc)) == NULL) 
		errx("Failed to open device.\n");

	if (argv[optind] != NULL) {
		opt.filter = str_join(" ", &argv[optind]);
		if (cap_setfilter(cap, opt.filter) < 0)
			exit(EXIT_FAILURE);
	}
	
	verbose(0, "Dump directory: %s\n", opt.dumpdir);
	if (!opt.debug) {
		verbose(0, "Log file: %s\n", opt.logfile);
		verbose(0, "PID file: %s\n", opt.pidfile);
	}
	verbose(0, "Buffer size: %s bytes\n", str_hsize(opt.ringbuf_max));
	if (opt.filter)
		verbose(0, "Filter: %s\n", opt.filter);
	else
		verbose(0, "No capture filter\n");

	if (opt.verbose)
		verbose(1, "Status log interval %s [%u seconds]\n", 
			str_hms(STAT_SEC_INTERVAL), STAT_SEC_INTERVAL);

	if ( (rbuf = ringbuf_init(opt.ringbuf_max)) == NULL)
		exit(EXIT_FAILURE);
	
	signal(SIGUSR1, dumppackets);

	signal(SIGUSR2, write_status);
	
	if (!opt.debug) {
		signal(SIGTERM, exit_handler);
		signal(SIGSEGV, exit_handler);
		signal(SIGBUS, exit_handler);
		signal(SIGILL, exit_handler);
		signal(SIGPIPE, exit_handler);

		signal(SIGQUIT, SIG_IGN);
		signal(SIGABRT, SIG_IGN);
		signal(SIGHUP, SIG_IGN);
		signal(SIGINT, SIG_IGN);
	}
	
	if (opt.verbose) 
		signal(SIGALRM, sigalrm_handler);
		
	for (;;) {
		size_t retry_time;

		if (opt.verbose) {
			char buf[256];
			
			snprintf(buf, sizeof(buf), "%s", str_time(time(NULL) + 
				(STAT_SEC_INTERVAL - (time(NULL) % STAT_SEC_INTERVAL)), NULL));
			
			verbose(1, "First status output aligned to %s\n", buf);
			alarm(STAT_SEC_INTERVAL - (time(NULL) % STAT_SEC_INTERVAL));
		}
		pcap_loop(cap->c_pcapd, -1, capture_pkts, (u_char *)cap);
		retry_time = 10;

		for (;;) {

			if (opt.verbose)
				alarm(0);

			warn("pcap_loop: '%s', retrying in %u seconds\n", 
				pcap_geterr(cap->c_pcapd), retry_time);
			
			if (cap != NULL) {
				cap_close(cap);
				cap = NULL;
			}
			sleep(retry_time);
	
			if ( (cap = cap_open(opt.iface, opt.promisc)) != NULL) 
				break;
			retry_time += 10;

			if (retry_time > 300)
				retry_time = 300;
		}
	}
	exit(EXIT_FAILURE);
}

If you need the complete source code, please add the WeChat number (c17865354792)

复制代码
┌─────────────────────────────────────────────────────────────┐
│                        用户空间                              │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐ │
│  │  信号处理   │    │  状态输出   │    │   数据包转储    │ │
│  │ SIGUSR1/2   │    │  SIGALRM    │    │   dumppackets() │ │
│  └──────┬──────┘    └──────┬──────┘    └────────┬────────┘ │
│         │                  │                    │          │
│         └──────────────────┼────────────────────┘          │
│                            ▼                               │
│                   ┌─────────────────┐                      │
│                   │   环形缓冲区     │                      │
│                   │   (ringbuf)     │                      │
│                   │  ┌───┐┌───┐┌───┐│                      │
│                   │  │包1│→│包2│→│包3││ ← 满了就覆盖最老的   │
│                   │  └───┘└───┘└───┘│                      │
│                   └────────┬────────┘                      │
│                            │                               │
└────────────────────────────┼───────────────────────────────┘
                             │
┌────────────────────────────┼───────────────────────────────┐
│                        内核空间                             │
│                            ▼                               │
│                   ┌─────────────────┐                      │
│                   │    libpcap      │                      │
│                   │  (数据包捕获库)  │                      │
│                   └────────┬────────┘                      │
│                            │                               │
│                   ┌────────┴────────┐                      │
│                   │   网卡驱动/NIC   │                      │
│                   └─────────────────┘                      │
└─────────────────────────────────────────────────────────────┘

四、代码核心逻辑拆解

4.1 数据包捕获流程

c 复制代码
// 每当网卡收到一个包,pcap_loop 就会调用这个回调函数
static void capture_pkts(u_char *arg, 
    const struct pcap_pkthdr *pkthdr, const u_char *packet)
{
    char buf[8192];
    
    // 1. 把包头和包体拼在一起
    memcpy(buf, pkthdr, sizeof(struct pcap_pkthdr));  // 元信息(时间戳、长度等)
    memcpy(&buf[sizeof(struct pcap_pkthdr)], packet, pkthdr->len);  // 实际数据
    
    // 2. 塞进环形缓冲区
    ringbuf_add(rbuf, buf, pkthdr->len + sizeof(struct pcap_pkthdr));
}

设计要点

  • pcap_pkthdr 结构保存包头信息(时间戳、捕获长度、原始长度)
  • 包头+包体一起存储,方便后续直接写入 pcap 文件

4.2 环形缓冲区的"添加"逻辑

虽然你没提供 ringbuf.c 的实现,但从使用方式可以推断出核心逻辑:

c 复制代码
int ringbuf_add(struct ringbuf *r, void *data, size_t size) {
    // 情况1:空间足够,直接追加到链表尾部
    if (r->size_curr + size <= r->size_max) {
        创建新节点 → 插入到 last 后面 → 更新 last
        r->size_curr += size;
        r->num_elems++;
        return 成功;
    }
    
    // 情况2:空间不够,删除最老的元素直到够空间
    while (r->size_curr + size > r->size_max) {
        删除 first 节点 → first 指向 next → 释放内存
        r->size_curr -= first->size;
        r->num_elems--;
    }
    
    // 现在空间够了,执行情况1的操作
    ...
}

精髓:始终保持缓冲区不超过设定上限,新数据来了老数据自动退场。

4.3 信号驱动的转储机制

这是整个系统最巧妙的设计------用 Unix 信号实现"拍照"功能

c 复制代码
// 注册信号处理器
signal(SIGUSR1, dumppackets);   // 用户自定义信号1:转储数据
signal(SIGUSR2, write_status);  // 用户自定义信号2:输出状态

// 管理员在 shell 中执行:
// kill -USR1 <pid>  →  触发数据转储
// kill -USR2 <pid>  →  查看当前缓冲区状态

转储流程

复制代码
收到 SIGUSR1
    │
    ▼
┌─────────────┐
│  忽略新信号  │ ← 防止转储过程中被打断
│ signal(IGN) │
└──────┬──────┘
       ▼
┌─────────────┐
│ 创建临时文件 │ ← 避免转储失败留下半成品
│  xxx.pid    │
└──────┬──────┘
       ▼
┌─────────────┐
│ 循环读取缓冲区│ ← 从 first 到 last,逐个写入 pcap
│  ringbuf_first()
└──────┬──────┘
       ▼
┌─────────────┐
│ 关闭并重命名 │ ← xxx.pid → eth0_20240115_10:00:00-10:05:30.pcap
│  文件名包含  │     设备名_开始时间-结束时间.pcap
│  时间范围   │
└──────┬──────┘
       ▼
┌─────────────┐
│ 恢复信号处理 │ ← 重新监听 SIGUSR1
└─────────────┘

4.4 优雅的故障恢复

网络接口可能会断开(比如网线被拔),程序不能就此罢工:

c 复制代码
for (;;) {  // 外层死循环:永久捕获
    pcap_loop(cap->c_pcapd, -1, capture_pkts, ...);  // 开始捕获
    
    // 如果执行到这里,说明 pcap_loop 异常返回了
    retry_time = 10;
    for (;;) {  // 内层循环:重连逻辑
        关闭旧连接 → 等待 retry_time 秒 → 尝试重新打开网卡
        成功则跳出,失败则 retry_time += 10(最长5分钟)
    }
}

设计智慧:指数退避(Exponential Backoff)避免疯狂重试耗尽 CPU。

五、涉及的技术领域知识

5.1 网络数据包捕获(Packet Capture)

概念 说明
libpcap Unix/Linux 平台标准的数据包捕获库,Win 平台对应 WinPcap/Npcap
BPF Berkeley Packet Filter,内核级过滤,只把感兴趣的包传给用户态
混杂模式 Promiscuous Mode,网卡接收所有经过的帧,不只是发给自己的
pcap 文件格式 行业标准格式,Wireshark、tcpdump 都能打开

5.2 进程管理相关

  • 守护进程(Daemon)daemonize() 函数让程序脱离终端后台运行
  • 信号处理:Unix 经典的进程间通信方式,USR1/USR2 留给用户自定义
  • PID 文件 :把进程号写入 /var/run/xxx.pid,方便外部管理(停止、发信号)

5.3 内存管理

  • 零拷贝思想:虽然这里做了拷贝(从 libpcap 缓冲区到环形缓冲区),但避免了频繁写磁盘
  • 动态内存分配 :每个包大小不同(64B ~ 1518B),需要 malloc/free 灵活管理

5.4 时间处理

代码里大量用到时间戳处理:

c 复制代码
// 计算缓冲区"时间跨度"
last->ts.tv_sec - first->ts.tv_sec

// 生成文件名中的时间
str_time(pkthdr->ts.tv_sec, "%Y%m%d_%H:%M:%S")

注意tv_sec 是 Unix 时间戳(1970年以来的秒数),tv_usec 是微秒。

六、设计亮点总结

  1. 空间换时间:内存比磁盘快几个数量级,先把数据存内存,需要时再落盘
  2. 信号即接口 :不需要 RPC、HTTP API,简单的 kill 命令就能控制程序
  3. 故障自愈:网卡断了自动重连,无需人工干预
  4. 原子性转储:先写临时文件,成功后再重命名,防止产生损坏的 pcap 文件
  5. 资源受限:严格限制内存使用,绝不会因为流量突增导致 OOM(Out of Memory)

七、适用场景与局限性

适合用

  • 7×24 小时监控,但只关心"刚才发生了什么"
  • 嵌入式设备或硬盘空间有限的环境
  • 需要低延迟抓包,不能频繁写磁盘

不适合用

  • 需要保存所有历史数据(这是全量抓包工具如 tcpdump 的场景)
  • 数据包太大的场景(如千兆网满流量,50MB 缓冲区可能只存几毫秒数据)
  • 需要复杂查询分析(应该直接存数据库或 Elasticsearch)

八、程序运行测试

抓包必须用 root,否则打不开网卡!

最基础运行命令(直接复制)

bash 复制代码
sudo mkdir -p /tmp/pcapdump
sudo ./ringcapd /tmp/pcapdump -d

命令解释

  • /tmp/pcapdump:抓包导出的存放目录
  • -d调试模式,不后台运行,直接在前台打印日志(最适合测试)

运行成功你会看到:

复制代码
+-+-+-+-+-+ Capture Started +-+-+-+-+-+
Dump directory: /tmp/pcapdump
Buffer size: 50.0M bytes
No capture filter

说明正在抓包了!


测试导出抓包文件

程序收到 SIGUSR1 信号就会把缓冲区的包导出成 pcap 文件。

新开一个终端,输入:

bash 复制代码
sudo pkill -SIGUSR1 ringcapd

回到运行 ringcapd 的终端,你会看到类似日志:

复制代码
Dumped 128KB bytes with 30 packets from 2026-03-30 12:00:00 to 2026-03-30 12:01:00

去目录看文件:

bash 复制代码
ls /tmp/pcapdump

你会看到:

复制代码
any_20260330_12:00:00-20260330_12:01:00.pcap

直接用 Wireshark 打开就能看包!


常用测试命令大全

1. 指定网卡抓包(比如 eth0)

bash 复制代码
sudo ./ringcapd /tmp/pcapdump -i eth0 -d

2. 修改缓冲区大小(比如 10MB)

bash 复制代码
sudo ./ringcapd /tmp/pcapdump -m 10M -d

3. 不开启混杂模式(不监听别人的包)

bash 复制代码
sudo ./ringcapd /tmp/pcapdump -P -d

4. 开启详细日志(更清楚看到缓存状态)

bash 复制代码
sudo ./ringcapd /tmp/pcapdump -v -d

5. 后台守护运行(不占终端)

bash 复制代码
sudo ./ringcapd /tmp/pcapdump

6. 查看状态(每秒打印缓存信息)

bash 复制代码
sudo pkill -SIGUSR2 ringcapd

7. 停止程序

bash 复制代码
sudo pkill ringcapd

最完整的一条测试命令

bash 复制代码
sudo ./ringcapd /tmp/pcapdump -i any -m 20M -v -d

含义:

  • 监听所有网卡
  • 缓冲区 20MB
  • 打印详细日志
  • 前台调试运行

总结

这个工具的核心哲学是:"我宁愿丢掉 99% 的数据,也要确保关键时刻的数据在手边"。环形缓冲区就是这个哲学的完美载体------它像一位不知疲倦的守门员,永远准备着把最近发生的事情告诉你,而不管那些陈年旧事。

在网络安全、故障排查、性能分析等领域,这种"滑动窗口"式的数据保留策略比盲目全量存储要实用得多。理解了这个设计,你也能在自己项目中灵活运用环形缓冲区解决类似问题。

Welcome to follow WeChat official account【程序猿编码

相关推荐
云栖梦泽2 小时前
Linux内核与驱动:3.驱动模块传参,内核模块符号导出
linux·服务器·c++
上海云盾-小余2 小时前
黑产入侵链路拆解:从打点踩点到内网横移的完整防御思路
网络·安全·web安全
默|笙2 小时前
【Linux】进程信号(4)_信号捕捉_内核态与用户态
linux·运维·服务器
supersolon2 小时前
PVE9安装32位爱快路由(ikuai)
linux·运维·网络
123过去2 小时前
mfterm使用教程
linux·网络·测试工具·安全
123过去2 小时前
nfc-mfclassic使用教程
linux·网络·测试工具·安全
虎头金猫2 小时前
自建 GitLab 没公网?用内网穿透技术,远程开发协作超丝滑
运维·服务器·网络·开源·gitlab·开源软件·开源协议
一个人旅程~5 小时前
Linux系统如何分区更合适?
linux·经验分享·电脑
zfxwasaboy10 小时前
Linux宏clamp(val, lo, hi)的作用
linux·运维·服务器