文章目录
- 
- [一、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-dev2️⃣ 编译程序
            
            
              bash
              
              
            
          
          gcc pcap_demo.c -o pcap_demo -lpcap3️⃣ 运行示例
            
            
              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 | 释放句柄 |