ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

QT5与WinPcap实战:从零构建轻量级网络抓包程序

QT5与WinPcap实战:从零构建轻量级网络抓包程序 简介这是一份面向网络安全初学者、协议分析爱好者及C开发者的网络抓包程序源码基于QT5与WinPcap实现功能与界面仿照WireShark可用于数据包捕获、过滤、统计与网络故障诊断等场景。压缩包共392个文件约8.15MB包含16个cpp与27个c源文件、29个h头文件、3个ui界面文件及qrc资源文件另有vcproj、dsp、sln等工程配置以及lib、a静态库和png、gif界面素材完整呈现了从界面构建到底层抓包的项目结构。已有196人学习下载。通过研读源码读者可理解QT5信号槽与界面交互、WinPcap的API调用方式、数据包解析与过滤规则的实现思路并借助工程文件在Windows下直接编译运行适合作为网络协议分析与抓包工具开发的实践参考。1. 从零手搓一个 SnifferQT5 加 WinPcap 到底能抓到什么很多人第一次听到「网络抓包程序仿 WireShark基于 QT5 和 WinPcap 实现」这个标题脑子里冒出来的第一个问题是WireShark 都这么成熟了为什么还要自己写一个答案其实很朴素——你要的不是另一个 WireShark而是一个能嵌进自己工具链、能定制过滤逻辑、能把抓到的包直接喂给业务代码的轻量级抓包内核。WireShark 是给人看的Sniffer 是给程序用的。这个区别决定了整个技术选型的方向。QT5 负责界面和跨平台事件循环WinPcap 负责在 Windows 上绕过协议栈直接拿网卡原始帧。两者拼在一起就是一个最小可用的抓包工具骨架。适合谁适合需要做协议分析、流量监控、安全测试工具二次开发的人。你不需要从零写驱动也不需要理解 NDIS 的每一个细节但你需要知道 WinPcap 的句柄怎么开、过滤器怎么编译、QT5 的信号槽怎么把抓包线程的数据安全地送到 UI 线程。这篇文章就按这个路径把能跑通的代码和参数配置一步步拆开。2. 环境搭建QT5 与 WinPcap 的版本匹配和安装顺序2.1 为什么 QT5 版本和编译器必须一起选QT5 在 Windows 上的安装方式直接决定了后面能不能顺利链接 WinPcap。常见做法是用 Qt 官方在线安装器选 MSVC 2017 64-bit 或 MinGW 对应版本。这里有一个血泪经验如果你选的是 MSVC 套件WinPcap 的库文件必须用 MSVC 编译的版本如果选 MinGW就要用 MinGW 的.a库。混用会报undefined reference to pcap_open_live这类链接错误而且报错信息不会告诉你根因是编译器不匹配。安装顺序建议是先装 QT5再装 WinPcap最后装 WinPcap 的开发包WpdPack。WinPcap 运行库安装时会往系统目录写wpcap.dll和Packet.dll开发包则提供pcap.h、wpcap.lib和Packet.lib。很多人只装了运行库编译时找不到头文件这就是典型的「运行库和开发包不分」。注意WinPcap 4.1.3 是最后一个官方版本在 Win10/Win11 上安装时如果遇到npf错误通常是因为系统已经装了 Npcap 或者驱动签名策略变化。常见做法是卸载冲突的抓包驱动后重装或者改用 Npcap 的兼容模式但本文以 WinPcap 为主线。2.2 在 QT5 项目文件里正确引入 WinPcapQT5 用.pro文件管理构建配置。你需要把 WpdPack 的Include和Lib目录写进去同时区分 32 位和 64 位。下面是一个可抄的.pro配置片段# Sniffer.pro QT core gui widgets TARGET Sniffer TEMPLATE app # 根据你的 WpdPack 实际路径修改 WINPCAP_DIR C:/WpdPack INCLUDEPATH $$WINPCAP_DIR/Include LIBS -L$$WINPCAP_DIR/Lib/x64 -lwpcap -lPacket # 如果编译报错找不到 Packet.lib检查 x64 目录是否存在 # 32 位套件请把 x64 改成 x86 SOURCES main.cpp \ mainwindow.cpp \ capturethread.cpp HEADERS mainwindow.h \ capturethread.h这段配置的逻辑是INCLUDEPATH让编译器找到pcap.hLIBS让链接器找到wpcap.lib和Packet.lib。参数说明上-L指定库目录-l指定库名QT 的 qmake 会自动补全.lib后缀。如果你用的是 MinGW库文件后缀是.a写法类似但文件名不同。失败时先看编译输出里cannot open file wpcap.lib还是undefined reference前者是路径问题后者是编译器不匹配。2.3 验证 WinPcap 是否真正可用装完不要急着写界面先用一个最小控制台程序确认pcap_findalldevs能列出网卡。这一步能过滤掉 80% 的环境问题。// check_pcap.cpp #include pcap.h #include iostream int main() { pcap_if_t *alldevs; char errbuf[PCAP_ERRBUF_SIZE]; // 获取所有网卡列表 if (pcap_findalldevs(alldevs, errbuf) -1) { std::cerr Error: errbuf std::endl; return 1; } // 遍历并打印网卡名和描述 for (pcap_if_t *d alldevs; d ! nullptr; d d-next) { std::cout Device: d-name std::endl; if (d-description) std::cout Desc: d-description std::endl; } pcap_freealldevs(alldevs); return 0; }逻辑说明pcap_findalldevs返回一个链表每个节点代表一块网卡。errbuf是错误缓冲区大小由PCAP_ERRBUF_SIZE定义。参数上不需要额外配置但如果你在虚拟机里跑可能只能看到虚拟网卡。如果这里返回空列表说明 WinPcap 驱动没加载成功后面 QT5 界面写得再漂亮也没用。3. 抓包核心用 pcap_loop 在独立线程里收包并投递到 QT5 界面3.1 为什么抓包必须放在子线程QT5 的主线程负责界面刷新和事件分发。如果你在按钮点击槽函数里直接调用pcap_loop这个函数会阻塞直到抓到指定数量的包界面会直接卡死Windows 还会给你弹一个「程序未响应」。常见做法是把抓包逻辑封装成一个继承自QThread的类在run()里开循环每抓到一个包就通过信号槽发给主线程。这里有一个关键点WinPcap 的回调函数是 C 风格的函数指针不能直接调用 QT 的信号。你需要用一个静态成员函数或者全局函数作为回调在回调里把数据转成QByteArray再通过emit发出去。注意跨线程传递自定义类型时要先qRegisterMetaType。3.2 抓包线程的完整实现下面是一个可复现的CaptureThread实现包含打开网卡、设置过滤器、回调收包三个环节。// capturethread.h #ifndef CAPTURETHREAD_H #define CAPTURETHREAD_H #include QThread #include QByteArray #include pcap.h class CaptureThread : public QThread { Q_OBJECT public: explicit CaptureThread(QObject *parent nullptr); void setDevice(const QString devName); void setFilter(const QString filter); void stop(); signals: void packetCaptured(QByteArray data, quint32 len, quint32 ts_sec); void captureError(QString msg); protected: void run() override; private: QString m_devName; QString m_filter; pcap_t *m_handle nullptr; volatile bool m_running false; static void packetHandler(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes); }; #endif// capturethread.cpp #include capturethread.h CaptureThread::CaptureThread(QObject *parent) : QThread(parent) {} void CaptureThread::setDevice(const QString devName) { m_devName devName; } void CaptureThread::setFilter(const QString filter) { m_filter filter; } void CaptureThread::stop() { m_running false; if (m_handle) { pcap_breakloop(m_handle); // 让 pcap_loop 立即返回 } } void CaptureThread::packetHandler(u_char *user, const struct pcap_pkthdr *h, const u_char *bytes) { CaptureThread *self reinterpret_castCaptureThread *(user); // 把原始帧拷贝成 QByteArray避免回调返回后内存失效 QByteArray data(reinterpret_castconst char *(bytes), h-caplen); emit self-packetCaptured(data, h-len, h-ts.tv_sec); } void CaptureThread::run() { char errbuf[PCAP_ERRBUF_SIZE]; // 打开网卡snaplen 设为 65535 保证不截断 m_handle pcap_open_live(m_devName.toLocal8Bit().constData(), 65535, 1, 1000, errbuf); if (!m_handle) { emit captureError(QString(pcap_open_live failed: %1).arg(errbuf)); return; } // 编译并设置过滤器比如 tcp port 80 struct bpf_program fp; if (!m_filter.isEmpty()) { if (pcap_compile(m_handle, fp, m_filter.toLocal8Bit().constData(), 0, PCAP_NETMASK_UNKNOWN) -1) { emit captureError(QString(pcap_compile failed: %1).arg(pcap_geterr(m_handle))); pcap_close(m_handle); m_handle nullptr; return; } if (pcap_setfilter(m_handle, fp) -1) { emit captureError(QString(pcap_setfilter failed: %1).arg(pcap_geterr(m_handle))); pcap_freecode(fp); pcap_close(m_handle); m_handle nullptr; return; } pcap_freecode(fp); } m_running true; // 循环抓包-1 表示无限抓直到 stop() 调用 pcap_breakloop pcap_loop(m_handle, -1, packetHandler, reinterpret_castu_char *(this)); pcap_close(m_handle); m_handle nullptr; }逻辑说明pcap_open_live的第三个参数1表示开启混杂模式第四个参数1000是读取超时毫秒数。pcap_compile把过滤表达式编译成 BPF 字节码pcap_setfilter把它绑定到句柄。回调里用reinterpret_cast把user指针还原成线程对象再emit信号。参数上snaplen设成 65535 是为了不截断巨型帧如果你只关心头部可以设成 96 或 128 来降低内存拷贝开销。3.3 在 QT5 主界面里接收并展示数据包主线程只需要连接信号槽把QByteArray转成十六进制字符串显示到QPlainTextEdit或QTableView。下面是一个最小连接示例// mainwindow.cpp 片段 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_thread new CaptureThread(this); // 跨线程传递 QByteArray 需要注册元类型 qRegisterMetaTypeQByteArray(QByteArray); connect(m_thread, CaptureThread::packetCaptured, this, MainWindow::onPacketCaptured); connect(m_thread, CaptureThread::captureError, this, MainWindow::onCaptureError); } void MainWindow::onStartClicked() { m_thread-setDevice(m_deviceName); m_thread-setFilter(m_filterEdit-text()); // 例如 tcp port 443 m_thread-start(); } void MainWindow::onPacketCaptured(QByteArray data, quint32 len, quint32 ts) { // 只显示前 64 字节的十六进制避免界面卡顿 QString hex data.left(64).toHex( ); ui-logEdit-appendPlainText(QString([%1] len%2\n%3) .arg(ts).arg(len).arg(hex)); }参数说明data.left(64)限制显示长度因为一个 1500 字节的帧转成十六进制是 3000 个字符全量刷新会拖慢 UI。toHex( )用空格分隔字节方便肉眼对齐。如果你要显示协议树需要自己解析以太头、IP 头、TCP 头这部分可以用结构体强转但要注意字节序和内存对齐。4. 避坑与排查WinPcap 安装失败、QT5 链接报错、抓不到包怎么办4.1 现象WinPcap 安装时报「npf 错误」或驱动安装失败原因系统里已经存在 Npcap 或其他抓包驱动两者都试图注册npf.sys网络过滤驱动冲突导致安装程序无法覆盖。另外 Win10 之后的驱动签名强制策略也可能拦截旧版 WinPcap 的未签名驱动。解决先在「网络适配器」属性里卸载所有带「Npcap」或「WinPcap」字样的服务重启后再装 WinPcap。如果仍然失败可以尝试以管理员身份运行安装程序并临时关闭驱动签名强制。实在不行就换 Npcap它的 API 和 WinPcap 兼容只需把库文件换成 Npcap 的 SDK。4.2 现象QT5 编译时报cannot open file wpcap.lib原因.pro文件里的LIBS路径写错或者 WpdPack 的Lib目录下没有对应位数的子目录。很多人下载的 WpdPack 只有x86没有x64但 QT5 套件选的是 64 位 MSVC。解决打开 WpdPack 的Lib目录确认存在x64文件夹。如果没有去下载完整版 WpdPack 或者改用 32 位 QT5 套件。然后在.pro里用绝对路径不要用相对路径因为 qmake 的工作目录可能和.pro不在同一层。4.3 现象程序能运行但网卡列表为空或抓不到任何包原因pcap_findalldevs返回空通常是因为 WinPcap 服务没启动。抓不到包则可能是过滤器写错、网卡选错、或者目标流量根本不经过你选的那块网卡。解决先检查 Windows 服务里NPF是否处于运行状态。然后在代码里打印每块网卡的name和description确认你选的是正在使用的物理网卡而不是虚拟网卡。过滤器先用空字符串抓所有包确认有流量后再加tcp、udp、icmp等条件。如果抓本机回环流量WinPcap 在 Windows 上默认抓不到localhost需要用 Npcap 的 loopback 适配器。4.4 现象抓包线程停止后程序崩溃或界面卡死原因pcap_breakloop调用后pcap_loop会返回但如果你在stop()里直接delete线程对象而run()还没执行完就会出现竞态。另外回调里emit信号时如果接收方已经销毁也会崩溃。解决在stop()里只调用pcap_breakloop然后wait()等待线程真正结束。主窗口关闭时先stop()再wait()最后才销毁对象。信号槽连接用Qt::QueuedConnection确保跨线程安全不要用DirectConnection。4.5 现象抓到的包时间戳不是北京时间原因pcap_pkthdr里的ts是 Unix 时间戳UTC 时区没有时区信息。QT5 的QDateTime默认按本地时区转换但如果你直接拿ts.tv_sec显示看到的就是 UTC。解决用QDateTime::fromSecsSinceEpoch(ts.tv_sec)再toLocalTime()或者手动加 8 小时。注意tv_usec是微秒显示毫秒要除以 1000。5. 进阶技巧用 BPF 过滤器精准抓取固定 IP 和 VLAN 报文5.1 过滤器语法在 Sniffer 里的实际写法WireShark 的显示过滤器和 WinPcap 的捕获过滤器是两套语法。Sniffer 里用的是 BPF 捕获过滤器在pcap_compile阶段生效能减少内核到用户态的数据拷贝。下面这张表列出几个高频场景的写法场景BPF 过滤器表达式说明抓固定 IPhost 192.168.1.100源或目的任一匹配抓固定端口tcp port 8080只抓 TCP 8080抓 ICMPicmp对应 ping 流量抓 VLAN 报文vlan需要网卡支持 VLAN 标签排除某 IPnot host 192.168.1.1排除网关组合条件tcp and host 10.0.0.5 and port 443与逻辑参数说明vlan过滤器在 WinPcap 上有时需要网卡驱动支持 VLAN 剥离如果抓不到可以尝试vlan and tcp或者直接在代码里解析以太头判断0x8100。另外host后面跟域名会在编译时解析成 IP如果 DNS 不可用会编译失败建议直接用 IP。5.2 在 QT5 界面里动态切换过滤器你可以在界面上放一个QLineEdit让用户输入过滤器点击「应用」后先stop()当前线程再setFilter()后重新start()。注意每次重新打开网卡都要重新pcap_compile和pcap_setfilter不能复用旧句柄。void MainWindow::onApplyFilter() { if (m_thread-isRunning()) { m_thread-stop(); m_thread-wait(); // 等待线程完全退出 } m_thread-setFilter(ui-filterEdit-text()); m_thread-start(); }逻辑说明wait()是必须的否则新线程可能和旧线程同时操作同一块网卡。参数上wait()可以带超时毫秒数但建议无限等待因为pcap_breakloop会让pcap_loop很快返回。5.3 一个我常用的调试习惯每次改完过滤器我会先用tcpdump风格的命令行工具验证表达式是否能编译通过再放进 QT5 程序里。WinPcap 自带windump用法和 tcpdump 一样。比如windump -i 1 -d tcp port 80会打印编译后的 BPF 指令如果表达式有语法错误会直接报出来。这个习惯帮我省了很多次「程序跑起来但抓不到包」的排查时间。另外抓包程序的内存管理要特别小心。pcap_pkthdr和u_char*指向的内存在回调返回后就失效必须立刻拷贝。我一般会在回调里只做拷贝和emit所有解析逻辑放到主线程或单独的解析线程避免在回调里做耗时操作导致丢包。希望帮到你。本文还有配套的精品资源点击获取
返回列表