文章目录
-
- [一、libpcap 是什么?为什么它能"抓到包"](#一、libpcap 是什么?为什么它能“抓到包”)
- [二、核心工作流程:打开网卡 → 设置过滤 → 捕获数据](#二、核心工作流程:打开网卡 → 设置过滤 → 捕获数据)
- 三、第一步:打开网卡(`pcap_open_live`)
-
- 示例代码
- [💬 参数说明](#💬 参数说明)
- 四、第二步:设置过滤规则(只抓想要的包)
-
- [🔧 编译 + 应用过滤规则](#🔧 编译 + 应用过滤规则)
- [🧠 理解关键点](#🧠 理解关键点)
- [五、第三步:捕获并处理数据包(`pcap_loop`/ `pcap_dispatch`)](#五、第三步:捕获并处理数据包(
pcap_loop/pcap_dispatch)) -
- [🔹 pcap_loop:自动循环式抓包(常用)](#🔹 pcap_loop:自动循环式抓包(常用))
-
- [🔹 回调函数参数详解](#🔹 回调函数参数详解)
- [🔹 回调函数示例:](#🔹 回调函数示例:)
-
- [1️⃣ 数据包结构](#1️⃣ 数据包结构)
- [2️⃣ IP 头](#2️⃣ IP 头)
- [3️⃣ TCP 头](#3️⃣ TCP 头)
- [✅ 总结](#✅ 总结)
- [🔹 `pcap_dispatch`:轮询式抓包](#🔹
pcap_dispatch:轮询式抓包) -
-
- [🔹 示例:基于 `pcap_dispatch` 的轮询式抓包](#🔹 示例:基于
pcap_dispatch的轮询式抓包)
- [🔹 示例:基于 `pcap_dispatch` 的轮询式抓包](#🔹 示例:基于
- [✅ 特点对比](#✅ 特点对比)
-
- 六、释放资源:`pcap_close`
- 七、完整实战:写一个可运行的抓包程序
- 八、编译与运行
-
- [1️⃣ 安装依赖](#1️⃣ 安装依赖)
- [2️⃣ 编译程序](#2️⃣ 编译程序)
- [3️⃣ 运行示例](#3️⃣ 运行示例)
- 九、总结:抓包的完整逻辑图
- [🧩 最后小结](#🧩 最后小结)
对于刚接触网络编程的同学来说,"抓包"听起来很高深。
其实, 只要学会用 libpcap ,你也能像 Wireshark 一样实现自己的抓包工具。
一、libpcap 是什么?为什么它能"抓到包"
libpcap 全称 Packet Capture Library ,是一个跨平台的网络数据包捕获库 。
它就像是一个"监听器",能直接从网卡驱动层获取经过的每一个数据包。
💡 你知道吗?
像 Wireshark、tcpdump 这些工具的底层,都是 libpcap。
它能做的事情:
| 能力 | 说明 |
|---|---|
| 捕获数据包 | 从指定网卡中抓取原始网络数据 |
| 过滤数据包 | 只保留特定协议、端口或 IP 的包 |
| 分析包内容 | 读取协议头、源/目标 IP、端口等信息 |
简单来说:
👉 Wireshark 是图形化的,libpcap 是底层的 。
我们可以用它来实现各种自定义网络分析工具。
二、核心工作流程:打开网卡 → 设置过滤 → 捕获数据
所有基于 libpcap 的程序,本质上都包含 3 步:
- 打开网卡(告诉系统我要抓哪个接口的数据)
- 设置过滤规则(只抓我关心的包,比如 TCP 或端口 80)
- 捕获处理数据(逐个分析抓到的包)
就像听音乐:
插耳机 🎧 → 选歌单 🎵 → 开始听 👂。
下面我们一步步拆开看。
三、第一步:打开网卡(pcap_open_live)
pcap_open_live 是抓包的第一步,它会打开指定的网络接口(比如 eth0 或 wlan0),并返回一个 pcap_t* 类型的句柄,后续所有操作都通过它完成。
示例代码
c
// 初始化 libpcap:打开网卡并返回句柄
char errbuf[PCAP_ERRBUF_SIZE];
pcap_t* handle = pcap_open_live(
device, // 要打开的网卡名,例如 "eth0"
65535, // 最大包长度(以太网最大MTU)
1, // 开启混杂模式(1=是)
1000, // 超时时间:1000ms
errbuf // 存储错误信息
);
if (!handle) {
fprintf(stderr, "打开网卡失败:%s\n", errbuf);
return NULL;
}
printf("✅ 成功打开网卡:%s\n", device);
💬 参数说明
| 参数名 | 含义与类型说明 |
|---|---|
device |
要打开的网卡名(字符串),例如 wlan0、eth0。 |
snaplen |
抓包时每个数据包的最大抓取长度(整数),用于限制单个包读取的字节数。 |
promisc |
是否开启混杂模式(整数,1/0 或 true/false),开启后可接收非本机目标的包。 |
to_ms |
读取超时时间(毫秒,整数),控制捕获函数的阻塞返回行为。 |
errbuf |
错误信息缓冲区(字符数组),用于接收 libpcap 返回的错误描述。 |
📘 混杂模式(promiscuous mode)
通常网卡只接收发给本机的数据包。
开启混杂模式后,可以监听同一局域网中所有设备的包(前提:网卡支持)。
四、第二步:设置过滤规则(只抓想要的包)
抓取所有包的信息量太大,因此我们通常会用过滤器来"筛选"感兴趣的数据。
libpcap 使用一种叫 BPF(Berkeley Packet Filter) 的语法。
像这样写规则:
| 示例规则 | 意义 |
|---|---|
tcp |
抓取所有 TCP 包 |
udp port 53 |
抓取 DNS 包 |
dst port 80 or 443 |
抓取访问网站的包 |
src host 192.168.1.5 |
抓取来自特定主机的包 |
🔧 编译 + 应用过滤规则
我们不能直接把字符串规则交给驱动,要先"编译"成机器能执行的过滤程序。
c
struct bpf_program fp; // 存储编译后的规则
// 1️⃣ 编译过滤规则
if (pcap_compile(handle, &fp, filter_rule, 0, PCAP_NETMASK_UNKNOWN) == -1) {
fprintf(stderr, "编译过滤规则失败:%s\n", pcap_geterr(handle));
return -1;
}
// 2️⃣ 应用过滤规则
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "应用过滤规则失败:%s\n", pcap_geterr(handle));
pcap_freecode(&fp);
return -1;
}
// 3️⃣ 释放编译资源
pcap_freecode(&fp);
printf("✅ 成功设置过滤规则:%s\n", filter_rule);
🧠 理解关键点
pcap_compile:把人类可读的字符串(如"tcp and port 80") 翻译成机器码。pcap_setfilter:把机器码绑定到网卡,这样系统在内核层就能过滤数据,效率更高。pcap_freecode:释放临时编译结果。
五、第三步:捕获并处理数据包(pcap_loop/ pcap_dispatch)
完成过滤器后,就可以开始抓包了!
抓包的核心是注册一个回调函数 ,每次有符合规则的数据包时,libpcap 会自动调用它。
🔹 pcap_loop:自动循环式抓包(常用)
c
pcap_loop(handle, count, packet_handler, user_data);
各参数含义:
| 参数 | 类型 | 作用 | 示例 |
|---|---|---|---|
handle |
pcap_t* |
打开的网卡句柄,由 pcap_open_live 返回 |
handle |
count |
int |
要抓取的数据包数量;0 表示无限抓包 | 5 → 抓 5 个包;0 → 一直抓到用户按 Ctrl+C 停止 |
packet_handler |
回调函数 | 每抓到一个符合规则的数据包,libpcap 会自动调用该函数 |
必须是 void (*)(u_char*, const struct pcap_pkthdr*, const u_char*) 类型 |
user_data |
u_char* |
用户自定义数据,会传给回调函数,可用于传递上下文或统计信息 | 可以传 NULL 或传入结构体指针 |
💡 小提示
user_data可以用来传递额外信息,比如包计数器、日志文件指针或程序状态。在简单示例中可以传NULL。
🔹 回调函数参数详解
libpcap 规定了回调函数的固定格式,参数不能随便改,否则无法正常调用:
c
void packet_handler(
u_char* user_data, // 用户自定义数据(传给 pcap_loop 的最后一个参数)
const struct pcap_pkthdr* header, // 包的基本信息(长度、时间戳等)
const u_char* packet // 包的原始数据(二进制字节流)
);
各参数详解:
-
user_data:由你在调用pcap_loop时传入(最后一个参数),用于给回调函数传递额外信息(比如上下文结构体、配置参数等)。示例中没用到,所以可以忽略。 -
header:指向struct pcap_pkthdr结构体,包含包的元数据:header->len:包的实际长度(字节);header->ts:抓包的时间戳(tv_sec是秒,tv_usec是微秒)。
-
packet:指向包的原始二进制数据(从链路层开始的字节流),需要你手动解析(比如跳过以太网头、解析 IP 头、TCP 头等)。
🔹 回调函数示例:
c
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
// 每抓到一个包,就执行这个函数
void packet_handler(u_char* user_data, const struct pcap_pkthdr* header, const u_char* packet) {
static int count = 0;
count++;
printf("\n===== 第 %d 个包 =====\n", count);
printf("包长度:%d 字节\n", header->len);
// 解析 IP 头(跳过以太网头 14 字节)
const struct ip* ip_header = (struct ip*)(packet + 14);
printf("源 IP:%s\n", inet_ntoa(ip_header->ip_src));
printf("目标 IP:%s\n", inet_ntoa(ip_header->ip_dst));
// 解析 TCP 头
int ip_len = ip_header->ip_hl * 4;
const struct tcphdr* tcp_header = (struct tcphdr*)(packet + 14 + ip_len);
printf("源端口:%d -> 目标端口:%d\n",
ntohs(tcp_header->th_sport), ntohs(tcp_header->th_dport));
}
1️⃣ 数据包结构
一个以太网帧经过 libpcap 捕获后,packet 指向的就是原始字节流,从链路层(以太网层)开始。典型的网络协议层次如下:
[以太网头 Ethernet] 14 字节
[IP 头 IP] 可变长度,最小 20 字节
[TCP 头 TCP] 可变长度,最小 20 字节
[应用数据 Data] 剩下的 payload

-
以太网头通常固定 14 字节:
- 6 字节目标 MAC
- 6 字节源 MAC
- 2 字节类型字段(比如 IPv4 = 0x0800)
因此,如果你想解析 IP 头,需要跳过以太网头:
c
const struct ip* ip_header = (struct ip*)(packet + 14);
这里 packet + 14 的意思就是指针偏移 14 字节,直接指向 IP 头的起始位置。
2️⃣ IP 头
IP 头长度不是固定 20 字节,它的实际长度由 ip_hl 字段决定:
c
int ip_len = ip_header->ip_hl * 4;
ip_hl单位是 32-bit 字(4 字节),所以乘 4 得到实际字节数。- IP 头可能包含可选字段,所以不能直接固定 20 字节。
3️⃣ TCP 头
TCP 头紧跟在 IP 头后面,因此它的起始位置是:
packet + 以太网头长度 + IP 头长度
用代码表示就是:
c
const struct tcphdr* tcp_header = (struct tcphdr*)(packet + 14 + ip_len);
- 这样就确保无论 IP 头有没有可选字段,都能正确指向 TCP 头。
- TCP 头长度也是可变的(
th_off字段),后续如果解析应用层数据,还要用 TCP 头长度偏移。
✅ 总结
- 跳过以太网头(固定 14 字节) → 到达 IP 头。
- IP 头长度可变 → 用
ip_hl * 4获取实际长度。 - TCP 头紧随 IP 头 → TCP 头指针 =
packet + 14 + IP 头长度。
📌 总结
pcap_loop是抓包主循环,它只负责抓包和调用回调函数。回调函数packet_handler承载了你自定义的"处理逻辑",可以打印信息、分析数据、统计流量或写入文件等。
🔹 pcap_dispatch:轮询式抓包
除了 pcap_loop,libpcap 还提供了一个非常常用的函数 ------ pcap_dispatch。
它的作用类似,但在运行机制上更灵活、可控 ,尤其适合在需要定期检查退出条件 或与其他事件循环结合的场景。
c
int pcap_dispatch(pcap_t* handle, int cnt,
pcap_handler callback, u_char* user);
返回值是一个 整数,表示它实际 "处理的数据包数量"
| 参数名 | 含义与说明 |
|---|---|
handle |
打开的捕获会话句柄。 |
cnt |
指定要处理的最大包数(≤0 表示处理所有已到达的包)。 |
callback |
回调函数(和 pcap_loop 相同类型)。 |
user |
用户自定义数据(传递给回调)。 |
🔹 示例:基于 pcap_dispatch 的轮询式抓包
c
#include <signal.h>
volatile int stop_capture = 0;
void signal_handler(int sig) {
(void)sig;
stop_capture = 1;
printf("\n🛑 收到中断信号,准备停止抓包...\n");
}
void pcap_start_capture(pcap_t* handle) {
printf("开始抓包(按 Ctrl+C 停止)...\n");
signal(SIGINT, signal_handler);
while (!stop_capture) {
// 每次只处理 1 个包,超时则返回 0
int ret = pcap_dispatch(handle, 1, packet_handler, NULL);
if (ret == -1) {
fprintf(stderr, "❌ 抓包出错:%s\n", pcap_geterr(handle));
break;
}
// ret == 0 表示这次没有抓到包,可继续轮询
}
printf("✅ 抓包结束。\n");
}
🧠 理解要点:
pcap_dispatch每调用一次,仅处理一次捕获批次(或指定个数的包)。- 返回后可立即检查退出标志、更新 UI 或执行其他任务。
- 这种模式更适合做成"非阻塞"的实时程序(如入侵检测、流量监控)。
✅ 特点对比
| 对比项 | pcap_loop |
pcap_dispatch |
|---|---|---|
| 调用方式 | 阻塞式循环 ,直到抓完 count 个包或收到中断 |
单次处理调用,抓到指定数量包后立即返回 |
| 返回值 | 自动循环内部,不会主动返回 | 返回实际处理的包数,可在外层循环中判断 |
| 适用场景 | 一次性抓包,直到用户手动中断(如 Wireshark 模式) | 在程序主循环中"轮询"式抓包(如防火墙、实时监控) |
| 可中断性 | 需发送信号(如 Ctrl+C)才能中断 | 每次调用都可判断退出条件,更灵活 |
开始抓包
c
void pcap_start_capture(pcap_t* handle, int count) {
printf("开始抓包(按 Ctrl+C 停止)...\n");
int ret = pcap_loop(
handle, // 打开的网卡句柄
count, // 要抓的包数量(0 表示无限抓)
packet_handler, // 回调函数
NULL // 可选参数,这里不用
);
if (ret == PCAP_ERROR_BREAK)
printf("✅ 抓包结束。\n");
else if (ret == PCAP_ERROR)
fprintf(stderr, "❌ 抓包出错:%s\n", pcap_geterr(handle));
}
六、释放资源:pcap_close
程序退出前一定要释放资源,否则可能造成网卡占用或内存泄漏。
c
void pcap_cleanup(pcap_t* handle) {
if (handle) {
pcap_close(handle);
printf("🧹 已关闭网卡并释放资源\n");
}
}
七、完整实战:写一个可运行的抓包程序
下面是完整示例(可以直接复制运行👇)
c
/*
* pcap_demo.c - 简易抓包示例(基于 libpcap)
*
* 用法:
* gcc pcap_demo.c -o pcap_demo -lpcap
* sudo ./pcap_demo <网卡名> "<过滤规则>"
*
* 示例:
* sudo ./pcap_demo wlan0 "tcp and port 80"
*/
#include <pcap.h>
#include <stdio.h>
#include <signal.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <string.h>
int stop_capture = 0; // 标记 Ctrl+C 停止
void signal_handler(int sig) {
(void)sig;
stop_capture = 1;
printf("\n🛑 收到中断信号,准备停止抓包...\n");
}
/* ---------- 核心步骤:打开网卡(pcap_open_live) ---------- */
pcap_t* pcap_init(const char* device) {
char errbuf[PCAP_ERRBUF_SIZE];
pcap_t* handle = pcap_open_live(
device, // 网卡名,例如 "eth0" 或 "wlan0"
65535, // 最大抓包长度
1, // 混杂模式(1=开启)
1000, // 超时时间(ms)
errbuf
);
if (!handle) {
fprintf(stderr, "打开网卡失败:%s\n", errbuf);
}
return handle;
}
/* ---------- 编译并应用过滤器(pcap_compile + pcap_setfilter) ---------- */
int pcap_set_filter(pcap_t* handle, const char* filter_rule) {
struct bpf_program fp;
if (pcap_compile(handle, &fp, filter_rule, 0, PCAP_NETMASK_UNKNOWN) == -1) {
fprintf(stderr, "编译过滤规则失败:%s\n", pcap_geterr(handle));
return -1;
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "应用过滤规则失败:%s\n", pcap_geterr(handle));
pcap_freecode(&fp);
return -1;
}
pcap_freecode(&fp);
printf("✅ 成功设置过滤规则:%s\n", filter_rule);
return 0;
}
/* ---------- 回调函数:处理每个抓到的包(packet_handler) ---------- */
void packet_handler(u_char* user_data, const struct pcap_pkthdr* header, const u_char* packet) {
(void)user_data;
static int count = 0;
count++;
printf("\n===== 第 %d 个包 =====\n", count);
printf("包长度:%u 字节 | 抓包时间:%ld.%06ld\n",
header->len,
(long)header->ts.tv_sec,
(long)header->ts.tv_usec);
/* 基本长度检查(以太网头 14 字节 + 最小 IP 头) */
if (header->caplen < 14 + sizeof(struct ip)) {
printf("包太短,无法解析 IP 头\n");
return;
}
/* 解析 IPv4(假设以太网链路层) */
const u_char* ip_ptr = packet + 14;
const struct ip* ip_header = (const struct ip*)(ip_ptr);
if (ip_header->ip_v != 4) {
printf("非 IPv4 包(版本 %d),跳过解析\n", ip_header->ip_v);
return;
}
//定义两个缓冲区,用于存储转换后的IP地址字符串
char src_buf[INET_ADDRSTRLEN], dst_buf[INET_ADDRSTRLEN];
// 将二进制的目标/源IP地址转换为字符串
inet_ntop(AF_INET, &ip_header->ip_src, src_buf, sizeof(src_buf));
inet_ntop(AF_INET, &ip_header->ip_dst, dst_buf, sizeof(dst_buf));
printf("IP:%s -> %s\n", src_buf, dst_buf);
int ip_header_len = ip_header->ip_hl * 4;
if (ip_header_len < 20) {
printf("IP 头长度异常:%d,跳过\n", ip_header_len);
return;
}
/* 确保包含 TCP 头时长度足够 */
if (ip_header->ip_p == IPPROTO_TCP) {
const u_char* tcp_ptr = ip_ptr + ip_header_len;
if (header->caplen < 14 + ip_header_len + sizeof(struct tcphdr)) {
printf("包太短,无法解析 TCP 头\n");
return;
}
const struct tcphdr* tcp_header = (const struct tcphdr*)(tcp_ptr);
printf("TCP 端口:%u -> %u\n", ntohs(tcp_header->th_sport), ntohs(tcp_header->th_dport));
} else {
printf("非 TCP 协议(协议号 %d),跳过 TCP 解析\n", ip_header->ip_p);
}
}
/* ---------- 抓包主循环:用 pcap_dispatch 短轮询以便响应中断 ---------- */
void pcap_start_capture(pcap_t* handle) {
printf("开始抓包(按 Ctrl+C 停止)...\n");
while (!stop_capture) {
int ret = pcap_dispatch(handle, 1, packet_handler, NULL);
if (ret == -1) {
fprintf(stderr, "抓包出错:%s\n", pcap_geterr(handle));
break;
}
/* ret == 0 表示这次超时未捕获到包,继续循环 */
}
}
/* ---------- 释放资源 ---------- */
void pcap_cleanup(pcap_t* handle) {
if (handle) {
pcap_close(handle);
printf("🧹 已关闭网卡并释放资源\n");
}
}
/* ---------- 程序入口 ---------- */
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "用法:%s <网卡名> <过滤规则>\n", argv[0]);
fprintf(stderr, "示例:%s wlan0 \"tcp and port 80\"\n", argv[0]);
return 1;
}
signal(SIGINT, signal_handler);
const char* device = argv[1];
const char* filter = argv[2];
pcap_t* handle = pcap_init(device);
if (!handle) return 1;
if (pcap_set_filter(handle, filter) != 0) {
pcap_cleanup(handle);
return 1;
}
pcap_start_capture(handle);
pcap_cleanup(handle);
return 0;
}
八、编译与运行
1️⃣ 安装依赖
bash
sudo apt install libpcap-dev
2️⃣ 编译程序
bash
gcc pcap_demo.c -o pcap_demo -lpcap
3️⃣ 运行示例
bash
sudo ./pcap_demo wlan0 "tcp and port 80"
你会看到:
===== 第 1 个包 =====
包长度:74 字节 | 抓包时间:1698xxxxxxx.123456
IP:192.168.1.2 -> 172.217.160.110
TCP 端口:54321 -> 80
九、总结:抓包的完整逻辑图
┌────────────┐
│ 打开网卡 │ ← pcap_open_live()
└─────┬──────┘
│
▼
┌────────────┐
│ 编译过滤器 │ ← pcap_compile()
│ 应用过滤器 │ ← pcap_setfilter()
└─────┬──────┘
│
▼
┌────────────┐
│ 捕获数据包 │ ← pcap_loop()
│ 解析内容 │ ← 自定义回调函数
└─────┬──────┘
│
▼
┌────────────┐
│ 释放资源 │ ← pcap_close()
└────────────┘
🧩 最后小结
| 步骤 | 核心函数 | 作用 |
|---|---|---|
| 打开网卡 | pcap_open_live |
连接网络接口 |
| 编译过滤 | pcap_compile |
把规则转成机器码 |
| 应用过滤 | pcap_setfilter |
绑定过滤器 |
| 抓取数据 | pcap_loop / pcap_dispatch |
抓包主逻辑 |
| 清理资源 | pcap_close |
释放句柄 |