简介:这是一份基于npcap与Qt开发的网络抓包工具源码,模仿Wireshark的界面与操作方式,面向具备一定网络编程与C++基础的开发者,用于将数据包捕获与分析能力集成到自有系统中。资源包共86个文件,以cpp源码、h头文件、obj编译产物、vcxproj工程文件及sln解决方案为主,另含ui界面文件、qrc资源文件、png图标与log日志,压缩包约2.48MB,可直接用Visual Studio打开工程继续开发。工具依托npcap访问底层网络流量,支持数据包捕获、过滤与解码,并借助Qt的信号与槽机制构建图形界面,便于定制过滤规则与解析算法。目前已有378人学习下载,适合想研究抓包原理、二次开发网络监控工具或学习Qt与底层网络库结合的中高级开发者参考。
1. 从零造一个抓包工具:npcap + Qt 到底能做出什么
很多人第一次动了“自己写个抓包工具”的念头,都是因为 Wireshark 太重、太全,而自己只想盯住某几个端口、某几个 IP 的流量,顺手做个可视化统计。这个标题——基于 npcap + Qt 模仿 Wireshark 的网络抓包工具——说的正是这件事:用 npcap 拿到网卡上的原始数据帧,用 Qt 做界面和交互,最后拼出一个能选网卡、能起停抓包、能实时刷新报文列表、能看协议树和十六进制的小工具。它解决的不是“替代 Wireshark”,而是“把抓包能力嵌进自己的程序里”。适合两类人:一是想学网络协议栈和 Win32 抓包接口的开发者,二是需要在自有软件里内置流量监控、协议调试、设备通信分析功能的工程师。读完你应该能判断这套方案值不值得投入,以及怎么在本地把它跑起来。
2. 环境搭建:npcap 驱动、Qt 版本与工程骨架
2.1 为什么选 npcap 而不是 WinPcap
在 Windows 上做抓包,底层绕不开驱动。WinPcap 是上一代方案,已经停止维护,在 Win10/Win11 上安装经常报兼容性问题,而且不支持 NDIS 6 之后的很多特性。npcap 是它的继任者,由 Nmap 项目维护,支持 Win7 到 Win11,支持回环抓包、VLAN 标签保留、更细的过滤语法,安装时还能选择“WinPcap API 兼容模式”。如果你要模仿 Wireshark,Wireshark 本身在 Windows 上默认用的就是 npcap,所以选它最省心。
安装时有两个选项要留意:一是“Install Npcap in WinPcap API-compatible Mode”,勾上之后老代码也能跑;二是“Support loopback traffic”,做本机进程间通信调试时很有用。安装完在C:\Windows\System32\Npcap下能看到wpcap.dll和Packet.dll,这两个就是后面要链接的库。
Qt 版本方面,Qt 5.15.2 是长期支持版里比较稳的一个,社区版免费,在线安装器里勾选 MSVC 2019 64-bit 套件即可。不建议用 Qt 6 的早期版本,因为部分第三方库对 Qt 6 的兼容还在补。安装完确认qmake -v能输出版本号,Qt Creator能正常创建 Widgets Application。
2.2 工程文件怎么写:.pro 里的链接与包含路径
新建一个 Qt Widgets Application,工程名随意,比如MiniSniffer。打开.pro文件,加入 npcap 的头文件和库路径。npcap 安装后头文件在C:\Windows\System32\Npcap不一定有,通常需要下载 npcap 的 SDK,把pcap.h、pcap目录放到工程里,或者直接引用 Wireshark 开发包里的头文件。稳妥做法是把 SDK 里的Include和Lib目录拷到工程旁边。
QT += core gui widgets TARGET = MiniSniffer TEMPLATE = app # npcap SDK 头文件路径,按实际位置改 INCLUDEPATH += $$PWD/npcap-sdk/Include # 64 位用 x64,32 位用 x86 LIBS += -L$$PWD/npcap-sdk/Lib/x64 -lwpcap -lPacket # 如果开了 WinPcap 兼容模式,Packet.lib 可以不加 DEFINES += WPCAP HAVE_REMOTE SOURCES += main.cpp mainwindow.cpp HEADERS += mainwindow.h FORMS += mainwindow.ui-lwpcap是核心抓包库,-lPacket提供更底层的网卡操作。WPCAP和HAVE_REMOTE是 npcap 头文件里用来开启远程抓包相关声明的宏,不加的话某些函数原型不会暴露。编译时如果报cannot find -lwpcap,九成是路径写错或者位数不匹配——32 位 Qt 配 64 位库,或者反过来,都会链接失败。
2.3 用 Qt Designer 拖出主界面
打开mainwindow.ui,用 Qt Designer 拖出这几个控件:顶部一个QComboBox用来选网卡,旁边一个QPushButton做“开始/停止”,下面一个QTableWidget显示报文列表,右侧一个QTreeWidget显示协议树,底部一个QPlainTextEdit显示十六进制。布局用QVBoxLayout和QHBoxLayout嵌套,保证拉伸时比例正常。
对象名要记牢:comboBoxDev、btnStart、tablePackets、treeDetail、textHex。后面代码里全靠这些名字访问。表格列先设五列:序号、时间、源地址、目的地址、协议。协议树按帧、以太网、IP、TCP/UDP 分层。这些设计不是随便定的,Wireshark 的默认布局就是这样,用户迁移过来没有学习成本。
3. 抓包核心:从 pcap_open_live 到实时刷新表格
3.1 枚举网卡与打开设备
npcap 提供pcap_findalldevs来列出所有可抓包的网卡。返回的是一个链表,每个节点包含网卡名、描述、地址列表。网卡名是\Device\NPF_{GUID}这种形式,不能直接显示给用户,要显示描述字段。
#include <pcap.h> #include <QDebug> void MainWindow::refreshDevices() { pcap_if_t *alldevs = nullptr; char errbuf[PCAP_ERRBUF_SIZE] = {0}; // 枚举所有网卡,PCAP_IF_LOOPBACK 可过滤回环 if (pcap_findalldevs(&alldevs, errbuf) == -1) { qDebug() << "findalldevs failed:" << errbuf; return; } comboBoxDev->clear(); for (pcap_if_t *d = alldevs; d != nullptr; d = d->next) { // 描述为空时用网卡名兜底 QString desc = d->description ? QString::fromLocal8Bit(d->description) : QString::fromLocal8Bit(d->name); comboBoxDev->addItem(desc, QString::fromLocal8Bit(d->name)); } pcap_freealldevs(alldevs); }pcap_findalldevs失败通常是因为 npcap 服务没启动,或者程序没有管理员权限。返回的alldevs必须用pcap_freealldevs释放,否则每次刷新都漏内存。addItem的第二个参数是用户数据,存网卡名,后面打开设备时直接取,不用再解析显示文本。
打开设备用pcap_open_live:
bool MainWindow::openDevice(const QString &devName) { char errbuf[PCAP_ERRBUF_SIZE] = {0}; // snaplen 65536 保证整帧都能抓到,promisc 1 开混杂模式 // to_ms 1000 表示超时 1 秒,readTimeout 用 handle = pcap_open_live(devName.toLocal8Bit().constData(), 65536, 1, 1000, errbuf); if (!handle) { qDebug() << "open_live failed:" << errbuf; return false; } return true; }snaplen设 65536 是常见做法,保证再大的帧也不会被截断。promisc设 1 表示混杂模式,能收到经过网卡但不是发给本机的帧,做分析时通常需要。to_ms是读超时,设 1000 毫秒意味着pcap_next_ex最多阻塞一秒就返回,方便在循环里检查停止标志。
3.2 抓包线程与界面线程的分离
不能在 UI 线程里死循环调pcap_next_ex,否则界面直接卡死。标准做法是开一个QThread,把抓包循环放进去,每抓到一帧就通过信号发给主线程刷新表格。信号参数用自定义结构体,注册qRegisterMetaType才能跨线程传递。
// packetinfo.h struct PacketInfo { quint64 index; QString time; QString src; QString dst; QString protocol; QByteArray raw; }; Q_DECLARE_METATYPE(PacketInfo)抓包线程的run函数:
void CaptureThread::run() { while (!m_stop) { struct pcap_pkthdr *header = nullptr; const u_char *data = nullptr; int ret = pcap_next_ex(m_handle, &header, &data); if (ret == 0) continue; // 超时,继续 if (ret == -1) break; // 出错 if (ret == -2) break; // 离线文件读完 PacketInfo info; info.index = ++m_counter; info.time = QDateTime::currentDateTime() .toString("HH:mm:ss.zzz"); info.raw = QByteArray(reinterpret_cast<const char*>(data), header->caplen); parsePacket(info, data, header->caplen); emit packetCaptured(info); } }pcap_next_ex返回值有三种:1 表示抓到包,0 表示超时,-1 表示错误,-2 表示读到文件尾。循环里必须处理 0,否则超时会被当成错误退出。caplen是实际抓到的长度,len是帧原始长度,两者在没截断时相等。emit出去的PacketInfo按值传递,Qt 会自动做深拷贝,不用担心data指针失效。
主线程连接信号:
connect(captureThread, &CaptureThread::packetCaptured, this, &MainWindow::onPacketCaptured, Qt::QueuedConnection);Qt::QueuedConnection保证槽函数在 UI 线程执行。如果表格刷新太频繁,可以攒一批再刷,比如每 50 毫秒合并一次,避免高频抓包时界面卡顿。
3.3 协议解析:以太网头、IP 头、TCP/UDP 头
拿到原始帧后,按偏移量逐层解析。以太网头 14 字节,前 6 字节目的 MAC,接着 6 字节源 MAC,最后 2 字节类型。类型 0x0800 是 IPv4,0x0806 是 ARP,0x86DD 是 IPv6。
void MainWindow::parsePacket(PacketInfo &info, const u_char *data, int len) { if (len < 14) return; quint16 ethType = (data[12] << 8) | data[13]; int offset = 14; if (ethType == 0x0800 && len >= offset + 20) { // IP 头 int ihl = (data[offset] & 0x0F) * 4; quint8 proto = data[offset + 9]; info.src = QString("%1.%2.%3.%4") .arg(data[offset+12]).arg(data[offset+13]) .arg(data[offset+14]).arg(data[offset+15]); info.dst = QString("%1.%2.%3.%4") .arg(data[offset+16]).arg(data[offset+17]) .arg(data[offset+18]).arg(data[offset+19]); int l4 = offset + ihl; if (proto == 6 && len >= l4 + 20) { info.protocol = "TCP"; quint16 sport = (data[l4] << 8) | data[l4+1]; quint16 dport = (data[l4+2] << 8) | data[l4+3]; info.src += QString(":%1").arg(sport); info.dst += QString(":%1").arg(dport); } else if (proto == 17 && len >= l4 + 8) { info.protocol = "UDP"; quint16 sport = (data[l4] << 8) | data[l4+1]; quint16 dport = (data[l4+2] << 8) | data[l4+3]; info.src += QString(":%1").arg(sport); info.dst += QString(":%1").arg(dport); } else { info.protocol = QString("IP(%1)").arg(proto); } } else if (ethType == 0x0806) { info.protocol = "ARP"; } else { info.protocol = QString("ETH(0x%1)") .arg(ethType, 4, 16, QChar('0')); } }ihl是 IP 头长度,单位是 4 字节,所以乘 4。TCP 头固定 20 字节,UDP 头固定 8 字节。端口号是大端序,用移位拼。这段代码只解析了最外层,实际做协议树时还要处理 TCP 选项、IP 分片、VLAN 标签(类型 0x8100 时偏移要再加 4)。VLAN 报文是很多人第一次写抓包工具时翻车的地方——忘了判断 0x8100,结果所有带 VLAN 的帧都解析成未知协议。
4. 避坑与排查:npcap 服务、Qt 插件、内存泄漏
4.1 报 “failed to create the npcap service: 0x800f0203”
现象:程序启动调pcap_findalldevs返回空,或者安装 npcap 时直接弹这个错误码。原因:0x800f0203 是 Windows 更新组件相关的错误,通常出现在系统缺少某个补丁,或者 npcap 安装包版本和系统不匹配。解决:先确认系统是 Win7 SP1 以上,然后去 npcap 官网下最新稳定版,右键以管理员身份运行安装。如果还不行,检查系统是否禁用了“Windows Installer”服务,或者用sfc /scannow修复系统文件。装完重启一次,再跑程序。
4.2 Qt 程序发布后报 “could not find the qt platform plugin”
现象:在开发机上跑得好好的,拷到别的机器双击就闪退,命令行运行提示找不到windows平台插件。原因:Qt 的动态库和插件目录没跟着 exe 一起拷。解决:用windeployqt工具,在 Qt 命令行里执行windeployqt MiniSniffer.exe,它会自动把Qt5Core.dll、Qt5Gui.dll、platforms\qwindows.dll等拷到 exe 旁边。注意platforms目录必须存在,里面要有qwindows.dll,少一个都起不来。如果报的是linuxfb找不到,那是把 Linux 的插件配置带到 Windows 了,检查QT_QPA_PLATFORM环境变量,删掉或改成windows。
4.3 抓包线程退出时崩溃
现象:点“停止”后程序偶尔崩,或者关窗口时报访问违例。原因:pcap_next_ex还在阻塞,主线程就把handle用pcap_close关了,线程再去读就是野指针。解决:停止时先设m_stop = true,然后调pcap_breakloop(handle)打断阻塞,等线程wait()返回后再pcap_close。顺序不能反。pcap_breakloop是 npcap 提供的安全中断方式,比直接关句柄可靠得多。
4.4 表格刷新导致内存持续上涨
现象:抓包跑几分钟,任务管理器里内存一直涨。原因:每来一帧就new QTableWidgetItem,但表格行数没上限,旧行不删。解决:设一个最大行数,比如 10000 行,超过就删最前面的行。或者用QAbstractTableModel自己管理数据,只保留最近 N 条。另外PacketInfo里的raw如果每帧都存完整数据,内存也会涨,做实时显示时只存解析后的字段,原始字节按需再取。
4.5 混杂模式抓不到本机回环流量
现象:想抓本机两个进程之间的 TCP 通信,选物理网卡抓不到。原因:回环流量不走物理网卡,走的是 Windows 的 loopback 接口。解决:安装 npcap 时勾选“Support loopback traffic”,装完后网卡列表里会多出一个Npcap Loopback Adapter,选它就能抓到 127.0.0.1 的流量。如果列表里没有,重新运行安装程序,勾上那个选项再装一遍。
5. 进阶:过滤器语法、离线分析与性能调优
5.1 用 pcap_compile 做内核级过滤
抓包时如果不过滤,所有流量都往用户态送,CPU 占用很高。npcap 支持 BPF 过滤语法,在驱动层就把不想要的包丢掉。用法是先pcap_compile编译表达式,再pcap_setfilter应用。
struct bpf_program fp; // 只抓 80 端口和 443 端口的 TCP 流量 const char *filter = "tcp port 80 or tcp port 443"; if (pcap_compile(handle, &fp, filter, 0, PCAP_NETMASK_UNKNOWN) == -1) { qDebug() << "compile failed:" << pcap_geterr(handle); } else { pcap_setfilter(handle, &fp); pcap_freecode(&fp); }pcap_compile的第四个参数是优化开关,设 1 会做表达式优化。PCAP_NETMASK_UNKNOWN用于没有子网掩码的场景。编译成功后必须pcap_freecode释放,否则漏内存。过滤表达式支持host、net、port、portrange、src、dst以及and、or、not组合,和 Wireshark 的捕获过滤器语法一致。注意这是捕获过滤器,不是显示过滤器,写错了直接抓不到包,不会报语法错。
5.2 离线 pcap 文件读取与回放
除了实时抓包,pcap_open_offline能打开之前保存的.pcap文件,用同样的pcap_next_ex循环读。这样可以把抓包和分析解耦:现场只负责存文件,回来慢慢分析。保存文件用pcap_dump_open和pcap_dump,在抓包循环里顺手写盘。
pcap_dumper_t *dumper = pcap_dump_open(handle, "capture.pcap"); // 在循环里 pcap_dump(reinterpret_cast<u_char*>(dumper), header, data); // 结束时 pcap_dump_close(dumper);离线读取时pcap_next_ex返回 -2 表示文件读完,循环要正常退出。时间戳来自文件头,不是当前时间,显示时要用header->ts转换。做回放测试时,可以按时间戳差值sleep,模拟真实速率。
5.3 性能调优的几个参数
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
| snaplen | 65536 | 65536 | 小于 1500 会截断大帧 |
| to_ms | 1000 | 100 | 越小界面越跟手,但 CPU 略高 |
| 缓冲区 | 系统默认 | 4MB | pcap_setbuff可调,高流量时减少丢包 |
| 表格行数 | 无限制 | 10000 | 超过删旧行,防内存涨 |
| 刷新间隔 | 每帧 | 50ms 合并 | 高频抓包时降低 UI 压力 |
pcap_setbuff要在pcap_open_live之后、开始抓包之前调。高流量场景下,默认缓冲区可能只有几百 KB,瞬间打满就丢包。调到 4MB 能明显改善。另外抓包线程的优先级可以适当提高,但别设成实时,否则可能拖死系统。
5.4 一个我常犯的错误
早期写这个工具时,我总想把所有解析逻辑塞进抓包线程,觉得这样主线程轻松。结果协议树构建、十六进制转换这些耗时操作全在抓包线程里做,pcap_next_ex的调用间隔被拉长,高流量时丢包严重。后来改成抓包线程只做最轻量的帧拷贝和信号发射,所有解析、格式化、界面更新都放主线程,丢包率立刻降下来。抓包线程的职责越单一越好,它只负责“拿到帧、发出去”,别的都别管。希望帮到你。
本文还有配套的精品资源,点击获取