首页 / 资讯中心 / 文章详情

基于QT5与WinPcap的轻量级网络抓包工具实现

基于QT5与WinPcap的轻量级网络抓包工具实现 ★ FEATURED ARTICLE
简介一份基于QT5与WinPcap实现、功能对标WireShark的网络抓包程序源码包面向网络协议分析学习者、Windows平台C开发者及需要快速搭建抓包工具原型的工程师。程序整体仿照WireShark的界面与交互覆盖数据包捕获、过滤、解析和统计等常见需求可帮助理解网络数据在网卡层面的流动以及WinPcap API与QT界面如何协同工作。资源共392个文件压缩包约8.15MB以C/C源码、头文件、工程配置vcproj/dsp/sln及HTML说明为主另含UI界面、图标资源与静态库文件目录结构清晰便于按模块研读。目前已有197人学习浏览。解压后可按数据包捕获、过滤、解析、展示的链路梳理代码学习WinPcap底层API的调用方式与Qt事件驱动的界面刷新机制同时抓包/发包测试程序可帮助快速验证网络数据收发效果适合作为网络原理实验、毕业设计或协议分析工具二次开发的实战参考。1. 仿 WireShark 的 QT5 WinPcap 抓包程序到底解决什么问题把“网络抓包程序、仿 WireShark、基于 QT5 和 WinPcap 实现”这个标题拆开看它要回答的其实是一个很实际的问题不装完整版 Wireshark自己用 QT5 画一套界面、再用 WinPcap 拿到底层报文能不能做出一个能用的轻量抓包工具答案是能而且这套组合至今仍是 Windows 平台上做抓包工具最稳的路线之一。它适合两类人一类是课程设计或毕业设计需要“从零写一个抓包器”的学生另一类是在内网环境里想要定制抓包逻辑、又不想被 Wireshark 的庞大插件体系拖住的工程师。看完这篇文章你会明白 WinPcap 这一层到底提供了什么、QT5 界面和抓包线程之间怎么协作以及真正把包抓上来之后从原始字节到界面表格之间那几步解析是怎么串起来的。2. 先搭 QT5 WinPcap 环境驱动选型、SDK 目录与 .pro 配置2.1 WinPcap 还是 NPcap这个时代怎么选驱动标题写的是 WinPcap但你真正去下载的时候会发现在 Windows 10 和 Windows 11 上官方 WinPcap 安装包经常装不进去或者装完驱动签名报错。这里要先说明一个背景WinPcap 的底层基于 libpcap它定义了一套 BPF 抓包接口而 NPcap 是它的直接继任者接口完全兼容但驱动支持了新版 Windows 的 NDIS 6 和 802.11 无线网卡监控模式。也就是说如果你的代码按 WinPcap 的 API 写那么在 NPcap 环境下编译运行基本不用改代码。从工程角度我的建议是代码按 WinPcap API 写编译和运行环境直接用 NPcap SDK。这样标题里的“基于 WinPcap”没有变味因为 API 层面就是 pcap_findalldevs、pcap_open_live 那一套同时你也能绕开 WinPcap 在 Win10 以后驱动签名导致的安装失败问题。最典型的翻车现场就是程序编译通过一运行 pcap_findalldevs 返回 0 个设备或者 pcap_open_live 直接返回 NULL最后查半天发现是驱动没装上。遇到这种情况不用急着怀疑代码先打开命令行用管理员身份运行 npcap 安装包装完后去设备管理器里确认“NPF”相关的网络服务是否在运行。2.2 配置 Qt 工程.pro 文件的 INCLUDE 与 LIBS环境准备好了之后第一件事是让 Qt 工程能链接到 pcap 的库。你不需要把整个 Wireshark 的源码拖下来只需要 SDK 里的 include 和 lib。SDK 解压后通常是一个文件夹里面有 include 和 lib 两个目录include 下放着 pcap.h、pcap-stats.h 这些头文件lib 下是 wpcap.lib 和 Packet.lib。下面这份 .pro 配置是常见做法路径按你自己的 SDK 位置改QT core gui network greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET mySniffer TEMPLATE app CONFIG c11 CONFIG - app_bundle # SDK 路径根据自己的实际目录修改 INCLUDEPATH C:/npcap-sdk/include LIBS -LC:/npcap-sdk/lib -lwpcap -lws2_32这段配置里QT network 是给后面可能用到的套接字功能预留的抓包本身不依赖它LIBS 里的 -lwpcap 是抓包核心库-lws2_32 容易被忽略但非常重要。pcap_findalldevs 在 Windows 平台上要先调用 WSAStartup 初始化 Winsock否则返回错误码 -1而 ws2_32 就是支撑这个初始化的库。另外如果你的 SDK 里只有 x64 版本的 lib记得 Qt 编译器套件也要选成 64 位的 MSVC 或 MinGW混用 32 位库和 64 位编译器会直接链接失败。链接报错的时候优先检查这里而不是去怀疑代码逻辑。2.3 先写一个最小的设备枚举测试配置好 .pro 之后不要急着搭界面。先用一个控制台式的最小验证把链路打通确认驱动和库都没有问题。下面这段代码可以放在 main.cpp 里临时测试#include QCoreApplication #include pcap.h #include winsock2.h #include iostream int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), wsaData); // WinPcap 在 Windows 下必须初始化 Winsock pcap_if_t *alldevs nullptr; char errbuf[PCAP_ERRBUF_SIZE] {0}; int result pcap_findalldevs(alldevs, errbuf); if (result -1) { std::cerr pcap_findalldevs error: errbuf std::endl; return -1; } for (pcap_if_t *dev alldevs; dev; dev dev-next) { std::cout Device: dev-name std::endl; if (dev-description) { std::cout Description: dev-description std::endl; } } pcap_freealldevs(alldevs); return 0; }这段代码的作用是枚举本机所有网卡输出设备名和描述。pcap_if_t 结构体里的 name 后面要传给 pcap_open_live 使用注意它可能看起来不像一个正常的设备路径而是一长串以 rpcap 或 \Device\NPF_ 开头的字符串这属于正常现象。如果这里输出的设备列表是空的或者结果等于 -1说明驱动层有问题先解决环境再往下一步走。不要一上来就写界面等这一步能打印出网卡列表了你对环境才有底。3. 主窗口骨架网卡列表、启停按钮和表格区的线程分工3.1 为什么界面和抓包必须分线程很多新手写抓包程序时第一版会把 pcap_loop 直接放在主窗口的一个按钮响应函数里结果点“开始抓包”之后界面立刻卡死按钮按不动、窗口拖不动最后只能强杀进程。原因很简单pcap_loop 是一个阻塞调用它会一直停留在回调函数里等待数据包如果不把它放到独立线程主线程的事件循环就完全被占住了。所以工程结构上我一般会把抓包线程单独抽出来用 Qt 的信号槽机制把“抓到包了”这个事件发回主线程由主线程负责刷新表格。这样抓包线程只做最核心的工作从网卡上拿原始报文、做初步解析主线程只做界面更新。这里有一个隐含的好处即使抓包速率很高界面刷新也不至于把抓包线程拖慢两者之间的耦合被降到了最低。下面这个线程类的骨架是后面所有功能的基座。3.2 用代码搭出主窗口下拉框、按钮和表格区主窗口布局不复杂我用 QMainWindow 作为框架中央部件放一个 QSplitter左侧是网卡选择下拉框和启停按钮右侧是 QTableWidget。网格线这个设置很难看会让表格显得很碎一般我会用 setShowGrid(false) 关掉再配合 setAlternatingRowColors(true) 让行有底色间隔看起来更像 Wireshark 的列表风格。#include QMainWindow #include QComboBox #include QPushButton #include QTableWidget #include QSplitter #include QVBoxLayout class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); private slots: void onStartClicked(); void onPacketArrived(quint64 timestamp, QString src, QString dst, QString proto, int len); private: QComboBox *deviceCombo; QPushButton *startBtn; QTableWidget *packetTable; };MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto *central new QWidget(this); auto *layout new QVBoxLayout(central); // 左侧控制区网卡选择 启停按钮 auto *controlLayout new QHBoxLayout; deviceCombo new QComboBox(central); startBtn new QPushButton(tr(开始抓包), central); controlLayout-addWidget(deviceCombo, 1); controlLayout-addWidget(startBtn); // 右侧表格序号、时间、源地址、目的地址、协议、长度 packetTable new QTableWidget(central); packetTable-setColumnCount(6); packetTable-setHorizontalHeaderLabels( {序号, 时间, 源地址, 目的地址, 协议, 长度}); packetTable-setShowGrid(false); packetTable-setAlternatingRowColors(true); packetTable-horizontalHeader()-setStretchLastSection(true); layout-addLayout(controlLayout); layout-addWidget(packetTable, 1); setCentralWidget(central); connect(startBtn, QPushButton::clicked, this, MainWindow::onStartClicked); }这段代码把界面搭起来了但有个关键动作还没做网卡下拉框的内容必须从 pcap_findalldevs 的结果里填充。在构造函数里调用一个枚举函数把每个设备的 description 作为下拉框显示文本、name 存到 itemData 里后面抓包的时候直接取出来用。另外注意 connect 的写法使用了新式信号槽语法避免在槽函数里写字符串形式的 SIGNAL 宏。表格的列我选了 6 列这是最精简的抓包表结构Wireshark 默认列表也不外乎这几列加上 Info 一列早期实现先不加 Info 可以少踩不少解析的坑。3.3 抓包线程类的声明与启停控制线程类的核心是要能从外部通知它停止不能粗暴用 terminate()。pcap_loop 每抓到一个包会回调一次在回调里检查一个标志位为 false 时就调用 pcap_breakloop 来终止循环。#include QThread #include pcap.h class SnifferThread : public QThread { Q_OBJECT public: SnifferThread(QString deviceName, QObject *parent nullptr); ~SnifferThread() override; void stopSniffing(); signals: void packetCaptured(quint64 timestamp, QString src, QString dst, QString proto, int len); protected: void run() override; private: QString deviceName; pcap_t *handle; volatile bool running; };run 函数里要做的流程是 pcap_open_live 打开设备、pcap_loop 开始循环、结束时 pcap_close。stopSniffing 里把 running 置 false并在回调里触发 pcap_breakloop。注意 running 用 volatile 修饰是为了让回调线程能看到主线程的修改防止编译器优化把它缓存到寄存器里。这种做法比在回调里调用 QThread::requestInterruption 更直接因为 pcap_loop 的阻塞发生在内核空间只有 pcap_breakloop 能打断它。4. 抓包主流程从 pcap_loop 回调到报文解析与表格刷新4.1 枚举网卡到打开设备pcap_open_live 的参数细节线程 run 里的第一步是打开设备。pcap_open_live 的签名经历了多次演变WinPcap 时代最常见的版本是传五个参数。下面这段是我常用的打开方式void SnifferThread::run() { char errbuf[PCAP_ERRBUF_SIZE] {0}; // 第一个参数是设备名第二个是抓包长度第三个是否混杂模式 // 第四个是超时时间第五个是错误缓冲 handle pcap_open_live(deviceName.toLocal8Bit().constData(), 65536, 1, 1000, errbuf); if (handle nullptr) { emit errorOccurred(QString(pcap_open_live failed: %1).arg(errbuf)); return; } // 检测数据链路类型 int linkType pcap_datalink(handle); if (linkType ! DLT_EN10MB) { emit errorOccurred(QString(Unsupported datalink type: %1).arg(linkType)); pcap_close(handle); return; } running true; pcap_loop(handle, 0, SnifferThread::packetHandler, reinterpret_castu_char *(this)); pcap_close(handle); }这里 65536 表示抓取每个数据包的最大字节数以太网 MTU 是 1500加上各种头部不会超过这个值设置大一点是为了保证 Jumbo Frame 不被截断。混杂模式传 1意思是网卡会接收所有经过它的报文而不仅仅是发给本机的这是抓包工具的基本行为。超时 1000 毫秒的含义是如果长时间没有包到达pcap_loop 也会定期返回一次让程序有机会处理停止请求如果你设成 0在部分驱动版本上会导致 pcap_loop 忙等CPU 占用率直接拉满。设备名用 toLocal8Bit 转换是因为 Windows 设备名里可能包含非 ASCII 字符直接用 toStdString 在某些编码下会乱掉。4.2 回调里解析以太网帧与 IP 头把结果发回主线程pcap_loop 的回调函数是静态成员所以没法直接访问实例成员。常见做法是把 this 指针作为 user 参数传进去在回调里强转回来。下面这段实现了解析以太网帧和 IPv4 头部并把结果包装成信号发出去void SnifferThread::packetHandler(u_char *user, const struct pcap_pkthdr *header, const u_char *packet) { SnifferThread *self reinterpret_castSnifferThread *(user); if (!self-running) { pcap_breakloop(self-handle); return; } // 以太网帧头固定 14 字节 const struct ether_header *ethHdr reinterpret_castconst struct ether_header *(packet); // 判断帧类型0x0800 是 IPv40x0806 是 ARP unsigned short etherType ntohs(ethHdr-ether_type); if (etherType 0x0800 header-len 34) { // 跳过 14 字节以太网头指向 IP 头 const struct ip_header *ipHdr reinterpret_castconst struct ip_header *(packet 14); // 从 IP 头里取源、目的地址和协议号 QString src inet_ntoa(*(struct in_addr *)ipHdr-ip_src); QString dst inet_ntoa(*(struct in_addr *)ipHdr-ip_dst); QString proto self-protocolName(ipHdr-ip_p); int ipTotalLen ntohs(ipHdr-ip_len); emit self-packetCaptured(header-ts.tv_sec, src, dst, proto, ipTotalLen); } }解析过程中最容易出错的点是字节序。以太网帧头里的 ether_type 是一个多字节值在内存里是网络字节序直接比较会得到错误结果所以要先 ntohs 转成主机字节序。IP 地址则不需要手工转换字节序直接用 inet_ntoa 就能得到点分十进制的字符串。header-len 这个字段表示实际抓到的包长度判断它是否大于 3414 字节以太网头 20 字节最小 IP 头是为了防止畸形报文导致越界读取。这段逻辑是 Wireshark 那套解析体系的极简版Wireshark 抓包及分析的能力远不止如此但作为自研工具先把这条链路跑通比追求完整协议栈更重要。4.3 协议映射与列显示让报文从十六进制变成可读信息协议号只是一个数字界面表格里直接显示 6 或 17 可读性太差。我习惯在 SnifferThread 里维护一个静态映射表QString SnifferThread::protocolName(unsigned char proto) { static const QHashunsigned char, QString protoMap { {1, ICMP}, {6, TCP}, {17, UDP}, {47, GRE}, {50, ESP}, {89, OSPF} }; return protoMap.value(proto, QString(IP-%1).arg(proto)); }用 QHash 而不是 switch-case 的好处是协议增多的时候只需要在初始化列表里添加一行不需要改动函数结构。这里只列举了几个常见协议实际使用中可以根据需要补充。光有协议名还不够TCP 和 UDP 的端口也是排查问题时的关键信息可以在回调里继续解析四层头部TCP 头偏移 1420 字节之后就是源端口和目的端口各 2 字节取出后一并塞进信号里。表格刷新时把收到的参数按列填充到 QTableWidget 的新行里注意插入行时要让表格滚动到最新一行否则抓包速度一快你看到的永远是最早的几十条记录。5. 抓包必踩的坑驱动类型、回环包、过滤器与界面冻结排查5.1 网络抓包没有任何输出errbuf 却一直是空的现象程序正常启动网卡也能列出点开始抓包后界面无任何报文更新回调函数一次都没进。你打印 errbuf 发现是空字符串。原因多半出在设备名上。pcap_findalldevs 返回的设备名字段在 Windows 下是一串类似\Device\NPF_{GUID}的长字符串如果你把它直接传给 pcap_open_live本身没问题但有些实现会误把 description 字段当成设备名传进去而这个字段像“Realtek PCIe GbE Family Controller”这样自然打开失败。解决从下拉框的 itemData 里取 name 而不是 displayText。还有一个小概率是你把 isatap 或虚拟网卡的设备排在了前面选中的默认设备没有实际流量这类虚拟网卡设备描述里通常带着 Microsoft 或 Virtual 字样枚举时可以直接过滤掉。5.2 数据链路类型判断错了解析出来全是乱码现象抓到的包长度正常计数在涨但是源地址、目的地址全是乱码协议类型显示成 IP-255 这种奇怪的值。原因不是所有网卡的数据链路层都是以太网。比如某些无线网卡驱动在 WinPcap/NPcap 下返回的数据链路类型是 DLT_IEEE802_11值为 105帧头不是 14 字节而是 802.11 的管理帧头直接按 Ethernet II 解析从偏移 14 读出来的所谓 IP 头完全是错位的。解决在 run 函数里用 pcap_datalink 检查链路类型如果不是 DLT_EN10MB 就主动报错提示不要硬着头皮解析。之前遇到用 Vivado 装 WinPcap 失败的情况电脑上残留的驱动版本混杂导致无线网卡的数据链路类型被识别错重装 NPcap 并重启后恢复正常。5.3 过滤器一条都抓不到或在错误网卡上抓到空包现象设置了只抓 TCP 的过滤器结果启动后一包都进不来或者过滤器看起来生效了但抓到的包跟你访问的网站完全对不上。原因有两类一类是你把过滤表达式写错了WinPcap 用的是 BPF 语法字符串里写tcp and port 80没问题但你要是写成protocol tcp这种类 C 风格表达式编译就不通过另一类是选了错误的网卡比如访问外部网站的数据包是从物理网卡出去的你却挂在虚拟网卡上监听。解决先用不加过滤器的模式抓一把确认流量确实经过当前网卡再加过滤struct bpf_program filter; // 表达式按 BPF 语法写编译失败时 pcap_compile 会返回 -1 if (pcap_compile(handle, filter, tcp port 80, 1, PCAP_NETMASK_UNKNOWN) -1) { emit errorOccurred(QString(Filter compile failed: %1) .arg(pcap_geterr(handle))); return; } pcap_setfilter(handle, filter);pcap_compile 的第四个参数是 netmask填 PCAP_NETMASK_UNKNOWN 即可它只有在过滤表达式里包含 broadcast 或 multicast 这类依赖掩码的关键字时才有意义。值得留意的是过滤器生效后同样会影响回调频率抓包速度高时不要靠过滤去“节流”它会无差别丢弃不匹配的包而这在后续分析时可能让你误判“对方没发包”。5.4 界面卡死与刷新错乱在回调里碰 GUI 的下场现象抓包一开始界面立刻进入“未响应”状态几秒后恢复抓包停止时表格又刷出一大堆行或者表格行一直在跳但顺序是乱的看起来新旧数据在互相覆盖。原因就一句话你在 pcap 回调线程里直接操作了 QTableWidget。Qt 的 GUI 类不是线程安全的所有控件操作必须在主线程执行。新手常犯的错误是在 packetHandler 里调用 self-table-insertRow()以为这样没关系实际上会破坏 Qt 内部的事件队列状态轻则界面卡顿重则直接崩溃。解决回调里只发信号主线程的槽函数里才做表格操作如果抓包速率极高导致主线程刷新不过来可以在槽函数里做一个简单的限速比如距离上次刷新不足 50 毫秒就丢弃这次的界面更新保留数据但别刷屏。5.5 WinPcap 服务没启动老生常谈却最常翻车现象代码在别的机器上跑得好好的换一台电脑就提示 pcap_findalldevs 找不到设备。原因目标机器的 NPcap 服务没有自动启动或者安装新版驱动后旧版本残留的 npf.sys 冲突。解决用管理员身份在命令提示符执行net start npf然后重新打开程序。如果服务启动失败去设备管理器里卸载网络适配器驱动再重新安装 NPcap。这里要提醒一句如果你是在虚拟机里做实验宿主机的无线网卡不会被直接透传成可抓包的设备需要在虚拟机设置里把网卡模式调成“桥接”并安装对应驱动否则列表里只能看到虚拟网卡流量环境完全是隔离的。6. 离线保存与进阶验证pcap_dump 后置校验的实用技巧抓包程序做到能实时列表显示只完成了一半。真正让自己放心也方便向别人证明“这个程序没有瞎解析”的办法是把抓到的原始包保存成 pcap 文件再用 Wireshark 打开做对比。WinPcap 提供了现成的 dump 接口pcap_dump_open 创建文件pcap_dump 在回调里写入原始数据pcap_dump_close 收尾。下面这段是保存逻辑的常见接法pcap_dumper_t *dumper pcap_dump_open(handle, capture.pcap); // 在 pcap_loop 的回调里直接调用 pcap_dump 写入原始报文 pcap_dump((u_char *)dumper, header, packet); // 停止抓包后关闭文件 pcap_dump_close(dumper);pcap_dump 写入的格式和 Wireshark 的 pcap 文件完全兼容你可以把保存下来的文件直接拖进 Wireshark逐一对照源地址、目的地址、协议号和长度列。如果两边显示完全一致说明解析层没有黑匣子如果列对不上优先排查字节序问题再看是不是结构体字段的偏移写错了。这里也顺带解决一个 QT5 无法拖拽文件的问题想在程序里支持拖入 pcap 文件回放只需要在主窗口构造函数里调用 setAcceptDrops(true)再重写 dragEnterEvent 和 dropEvent在 dropEvent 里取到文件路径后调用一个新的解析线程即可。事件过滤器和拖拽之间没有隐式关联漏掉两个重写函数中的任何一个拖拽都会没有任何反应。如果还想进一步压榨性能可以研究一下 pcap_setmintocopy。这个函数用来设置内核缓冲区到用户态的最小拷贝字节数默认值在部分驱动上会导致小包高频触发回调拖慢整个线程。把最小拷贝值调大到 4096 左右系统会先在缓冲区里攒一批包再交给回调界面刷新频率会明显下降CPU 占用率也能低一截。我自己的习惯是保留这个参数配置入口默认不改但在抓高频小包时手动调大效果很直观。整个工具做到这一步已经不是一个玩具了——能实时抓、能过滤、能保存成标准格式、能被 Wireshark 交叉验证这套东西的基本盘就已经立住了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站