简介这是一份基于npcap与Qt开发的网络抓包工具源码模仿Wireshark的界面与操作方式面向具备一定网络编程与C基础的开发者用于将数据包捕获与分析能力集成到自有系统中或按需定制过滤规则与解析算法。压缩包共86个文件约2.48MB以cpp与h源码、obj与tlog等编译中间文件、vcxproj与sln工程文件为主另含ui界面文件、qrc资源文件、png图标及少量日志与配置项工程结构完整可直接用Visual Studio打开编译。资源已有378人学习下载适合作为网络协议分析、系统编程与GUI开发的学习范例。读者可从中了解npcap抓包接口的调用方式、Qt信号与槽驱动的事件处理流程以及数据包列表展示、搜索与过滤等功能的实现思路并在此基础上扩展自定义解析模块或嵌入更大规模的网络监控系统。1. 从零造一个 Sniffernpcap Qt 抓包工具到底能做成什么样很多人第一次接触网络协议都是从 Wireshark 开始的。点一下网卡、刷出一屏花花绿绿的包、随手写个tcp.port 80过滤感觉协议栈一下就透明了。但真到自己动手写工具问题立刻变成网卡怎么枚举驱动怎么装抓到的原始帧怎么解析界面怎么不卡标题里这个「基于 npcap qt 开发的、模仿 wireshark 的网络抓包工具 - sniffer.zip」本质上就是把 Wireshark 最核心的那条链路——抓包驱动 协议解析 可视化界面——用一套自己能掌控的代码重新走一遍。它解决的不是「替代 Wireshark」而是让你拥有一个可裁剪、可嵌入、可加自定义解析逻辑的抓包壳子。适合三类人想搞懂抓包底层原理的后端/运维、需要把抓包能力塞进自己产品的 Qt 客户端开发者、以及做安全分析或协议逆向、嫌 Wireshark 太重想自己攒工具的人。npcap 负责在 Windows 上把网卡数据捞出来Qt 负责把数据变成能看、能筛、能存的界面中间那层协议解析才是真正决定这个工具好不好用的地方。下面按「环境怎么搭 → 抓包怎么跑通 → 协议怎么解 → 界面怎么不卡 → 坑在哪」的顺序把这条链路拆开讲清楚。2. 环境搭建npcap 安装、Qt 工程配置与第一个能编译的抓包骨架2.1 npcap 装什么、怎么装、为什么不是 WinPcapWindows 上抓包绕不开驱动层。老方案是 WinPcap但它早就停止维护在 Win10/Win11 上兼容性越来越差而且不支持环回抓包。npcap 是它的现代替代品由 Nmap 团队维护支持 Win7 到 Win11支持 Loopback 抓包还提供了兼容 WinPcap 的 API 模式。安装时有两个关键选项必须注意Install Npcap in WinPcap API-compatible Mode如果你打算用pcap.h那套老 API本方案就是这个要勾上否则头文件和库对不上。Support loopback traffic想抓127.0.0.1的包必须勾做本地服务调试时非常有用。装完之后头文件默认在C:\Program Files\Npcap\Include库在C:\Program Files\Npcap\Lib。注意 npcap SDK 和驱动是分开的开发要单独下 SDK 包里面才有pcap.h、wpcap.lib、Packet.lib。提示安装 npcap 需要管理员权限装完后普通用户跑抓包程序通常也要以管理员身份运行否则pcap_open_live会直接返回失败。2.2 Qt 工程里怎么把 npcap 链接进来Qt 这边用 qmake 或 CMake 都行核心是把 npcap 的 include 和 lib 路径加进去。以 qmake 为例.pro文件里这样写QT core gui widgets # npcap SDK 路径按实际安装位置改 NPCAP_DIR C:/Program Files/Npcap INCLUDEPATH $$NPCAP_DIR/Include LIBS -L$$NPCAP_DIR/Lib -lwpcap -lPacket # 如果编译报 Packet.lib 找不到检查是否装了 SDK 而非仅驱动CMake 的写法对应是find_path(NPCAP_INCLUDE_DIR pcap.h PATHS C:/Program Files/Npcap/Include) find_library(NPCAP_WPCAP_LIB wpcap PATHS C:/Program Files/Npcap/Lib) find_library(NPCAP_PACKET_LIB Packet PATHS C:/Program Files/Npcap/Lib) target_include_directories(sniffer PRIVATE ${NPCAP_INCLUDE_DIR}) target_link_libraries(sniffer PRIVATE ${NPCAP_WPCAP_LIB} ${NPCAP_PACKET_LIB})参数说明wpcap是主抓包库Packet提供发送原始包的能力本方案只抓不发但链接上不影响。INCLUDEPATH必须指向 SDK 的 Include不是驱动的安装目录。很多人编译报cannot open include file: pcap.h八成是只装了驱动没装 SDK。2.3 枚举网卡并打开第一个抓包句柄环境通了之后第一件事是列出所有网卡让用户选一个。npcap 提供pcap_findalldevs来干这个#include pcap.h #include QDebug void listDevices() { pcap_if_t *alldevs; char errbuf[PCAP_ERRBUF_SIZE]; // 枚举所有网卡errbuf 用于接收错误信息 if (pcap_findalldevs(alldevs, errbuf) -1) { qDebug() 枚举网卡失败: errbuf; return; } for (pcap_if_t *d alldevs; d ! nullptr; d d-next) { // d-name 是设备标识d-description 是给人看的名字 qDebug() 设备: d-name 描述: (d-description ? d-description : 无); } // 用完必须释放否则内存泄漏 pcap_freealldevs(alldevs); }逻辑说明pcap_findalldevs返回一个链表每个节点是一个网卡。d-name是类似\Device\NPF_{GUID}的标识打开网卡时要用它d-description才是「Intel(R) Ethernet Connection」这种可读名字界面上应该显示 description但内部保存 name。打开网卡的调用是pcap_t *handle pcap_open_live( deviceName.toLocal8Bit().constData(), // 设备名 65536, // 抓包缓冲区大小65536 能覆盖绝大多数帧 1, // 混杂模式1 开启能抓到非本机目标的包 1000, // 读超时毫秒配合循环抓包用 errbuf ); if (handle nullptr) { qDebug() 打开网卡失败: errbuf; }三个参数最容易踩坑缓冲区给太小会截断大帧混杂模式在交换网络里未必能抓到别人的包别指望它当嗅探神器超时设 0 会一直阻塞设太小又空转费 CPU1000ms 是常见折中。3. 抓包主循环把 pcap_next_ex 塞进 Qt 线程界面才不卡3.1 为什么不能在 UI 线程里抓包pcap_next_ex是阻塞的网卡没包时它会挂在那里等。如果你在主线程也就是 Qt 的 GUI 线程里写个while循环不停调它界面直接假死——按钮点不动、窗口拖不动任务管理器一看「未响应」。这是新手做抓包工具最常见的翻车点。正确做法是把抓包循环放到独立线程里抓到包之后通过 Qt 的信号槽机制把数据丢回 UI 线程。Qt 里可以用QThread配合一个 worker 对象或者直接继承QThread重写run()。我一般用 worker moveToThread 的方式逻辑更清晰。3.2 抓包线程的实现class CaptureWorker : public QObject { Q_OBJECT public slots: void startCapture(const QString deviceName) { char errbuf[PCAP_ERRBUF_SIZE]; pcap_t *handle pcap_open_live( deviceName.toLocal8Bit().constData(), 65536, 1, 1000, errbuf); if (!handle) { emit captureError(QString(errbuf)); return; } // 只抓 IP 和 ARP减少无关帧bpf 过滤在驱动层生效效率最高 struct bpf_program fp; if (pcap_compile(handle, fp, ip or arp, 1, PCAP_NETMASK_UNKNOWN) 0) { pcap_setfilter(handle, fp); pcap_freecode(fp); } struct pcap_pkthdr *header; const u_char *pktData; int res; while (m_running) { // 返回 1 表示抓到包0 表示超时-1 出错-2 到文件尾 res pcap_next_ex(handle, header, pktData); if (res 1) { // 拷贝一份数据因为 pktData 在下次调用后可能失效 QByteArray frame((const char*)pktData, header-caplen); emit packetCaptured(frame, header-ts); } else if (res -1) { emit captureError(抓包出错); break; } } pcap_close(handle); } void stopCapture() { m_running false; } signals: void packetCaptured(QByteArray data, timeval ts); void captureError(QString msg); private: std::atomicbool m_running{true}; };逻辑说明pcap_compilepcap_setfilter把 BPF 过滤规则下推到驱动层只有匹配的包才会被拷贝到用户态比抓上来再在应用层过滤效率高一个数量级。pcap_next_ex的返回值必须判断-1是真实错误0只是超时别把超时当错误处理。参数说明pcap_compile的第三个参数1表示优化编译第四个是子网掩码抓本机流量时用PCAP_NETMASK_UNKNOWN即可。header-caplen是实际抓到的长度header-len是帧的原始长度两者在截断时不一样解析时要用 caplen。3.3 信号槽跨线程传数据worker 对象moveToThread到子线程后emit packetCaptured默认是队列连接Qt 会自动把数据排队投递到 UI 线程的槽函数天然线程安全。但要注意QByteArray拷贝是深拷贝高频抓包时每秒几千个包拷贝开销不小。优化手段是攒一批再发或者用QSharedPointerQByteArray减少拷贝。// UI 线程侧 connect(worker, CaptureWorker::packetCaptured, this, MainWindow::onPacketCaptured, Qt::QueuedConnection); void MainWindow::onPacketCaptured(QByteArray data, timeval ts) { // 这里已经在 UI 线程可以安全操作 model m_model-appendPacket(data, ts); }注意timeval是 C 结构体跨线程传自定义类型需要qRegisterMetaTypetimeval(timeval)否则队列连接会静默失败包一个都收不到。这个坑我踩过排查了半天。4. 协议解析从以太网帧到 TCP 流手写解析器的分层套路4.1 分层解析的基本结构抓到的QByteArray是一串原始字节从最外层往里剥以太网头 14 字节 → IP 头 20 字节起 → TCP/UDP 头 → 应用层载荷。每一层都有固定的字段偏移用结构体映射或者手动按偏移读都行。我一般定义一个解析结果结构体逐层填充。struct ParsedPacket { QString srcMac, dstMac; QString srcIp, dstIp; quint16 srcPort 0, dstPort 0; QString protocol; // TCP / UDP / ICMP / ARP QString info; // 类似 Wireshark 的 Info 列 QByteArray payload; }; bool parseEthernet(const u_char *data, int len, ParsedPacket out) { if (len 14) return false; // 以太网头目的MAC(6) 源MAC(6) 类型(2) out.dstMac macToString(data); out.srcMac macToString(data 6); quint16 etherType (data[12] 8) | data[13]; if (etherType 0x0800) { // IPv4 return parseIPv4(data 14, len - 14, out); } else if (etherType 0x0806) { // ARP out.protocol ARP; out.info ARP 请求/应答; return true; } return false; }逻辑说明etherType决定上层是什么协议0x0800是 IPv40x0806是 ARP0x86DD是 IPv6。判断长度是必须的畸形包或者截断包会导致越界读程序直接崩。4.2 IP 和 TCP 头的关键字段IP 头里最重要的是协议号和首部长度。首部长度字段单位是 4 字节所以实际长度要乘 4因为 IP 头有选项时不止 20 字节。bool parseIPv4(const u_char *data, int len, ParsedPacket out) { if (len 20) return false; int ihl (data[0] 0x0F) * 4; // 首部长度单位4字节 quint8 proto data[9]; // 协议号 out.srcIp ipToString(data 12); out.dstIp ipToString(data 16); if (proto 6) { // TCP out.protocol TCP; return parseTCP(data ihl, len - ihl, out); } else if (proto 17) { // UDP out.protocol UDP; return parseUDP(data ihl, len - ihl, out); } else if (proto 1) { out.protocol ICMP; out.info ICMP 报文; return true; } return false; }TCP 头里源端口、目的端口在前 4 字节然后是序列号、确认号再往后是标志位。标志位那 2 个字节里藏着 SYN、ACK、FIN、RST做连接分析全靠它。bool parseTCP(const u_char *data, int len, ParsedPacket out) { if (len 20) return false; out.srcPort (data[0] 8) | data[1]; out.dstPort (data[2] 8) | data[3]; int dataOffset ((data[12] 4) 0x0F) * 4; // TCP 首部长度 quint8 flags data[13]; QStringList flagList; if (flags 0x02) flagList SYN; if (flags 0x10) flagList ACK; if (flags 0x01) flagList FIN; if (flags 0x04) flagList RST; if (flags 0x08) flagList PSH; out.info QString([%1] Seq%2 %3) .arg(flagList.join(,)) .arg((data[4]24)|(data[5]16)|(data[6]8)|data[7]) .arg(len - dataOffset 0 ? QString(Len%1).arg(len - dataOffset) : ); out.payload QByteArray((const char*)(data dataOffset), len - dataOffset); return true; }参数说明dataOffset是 TCP 头实际长度同样因为选项字段会超过 20 字节。flags是第 13 字节SYN 是 bit1ACK 是 bit4这些位定义是固定的。payload就是应用层数据HTTP 明文请求、DNS 查询都在里面。4.3 常见协议识别与 Info 列生成Wireshark 的 Info 列之所以好用是因为它按端口和内容猜协议。我们也可以做简化版80 端口看是不是GET/POST开头53 端口当 DNS443 当 TLS。不用解析到字段级能给出「HTTP GET /index.html」这种提示就够用了。协议识别依据Info 示例HTTP端口 80 或载荷以 GET/POST/HTTP 开头GET /api/user HTTP/1.1DNS端口 53Standard query A example.comTLS端口 443 且首字节 0x16TLSv1.2 Application DataARPetherType 0x0806Who has 192.168.1.1?提示端口猜协议只是启发式非标准端口跑 HTTP 很常见。想更准就做载荷特征匹配但别过度设计抓包工具的核心价值在「看得见」不在「猜得准」。5. 避坑与排查npcap 和 Qt 联调时最容易翻车的 5 个点5.1 报 failed to create the npcap service: 0x800f0203现象程序启动或调用pcap_open_live时报这个错抓不了包。原因npcap 驱动服务没装好或者装的是旧版本被系统策略拦了。解决卸载重装最新版 npcap安装时勾选 WinPcap 兼容模式装完重启一次。如果还不行检查是不是杀软把 npcap 服务禁了。5.2 fatal: cannot mix incompatible Qt library现象程序能编译一运行就崩报 Qt 库版本冲突。原因系统 PATH 里有另一套 Qt 的 DLL和编译时用的版本不一致。解决用windeployqt把依赖 DLL 拷到 exe 同目录运行时优先加载本地 DLL或者用QT_DEBUG_PLUGINS1环境变量看它到底加载了哪个插件。5.3 抓包线程退出时程序崩溃现象点「停止抓包」后程序闪退。原因pcap_next_ex还在阻塞worker 对象就被销毁了或者pcap_close和读循环并发。解决用一个std::atomicbool标志位控制循环stop 时先置 false等线程真正退出QThread::wait()再销毁 handle。别用terminate()强杀线程。5.4 抓到的包全是乱码或解析越界现象解析函数偶尔读到垃圾数据甚至崩溃。原因没判断长度就按固定偏移读遇到截断帧或畸形帧越界。解决每个 parse 函数入口先检查len是否够本层最小长度不够直接返回 false。caplen和len不一致时以caplen为准。5.5 混杂模式抓不到别人的包现象开了混杂模式还是只能看到本机流量。原因现在基本都是交换网络交换机不会把别人的单播包转发给你混杂模式只在集线器或镜像端口上有效。解决想抓全网流量得在交换机上配端口镜像或者抓本机网关流量。这不是代码问题是网络拓扑决定的。6. 进阶技巧用 BPF 过滤 环形缓冲把抓包工具做到能日常用工具能跑通只是起点能不能日常用取决于两件事过滤够不够快、长时间抓包会不会把内存吃爆。BPF 过滤前面提过把规则下推到驱动层tcp port 443 and host 192.168.1.100这种表达式直接交给pcap_compile应用层一个无关包都收不到。我一般会在界面上放一个过滤输入框用户改完规则重新pcap_setfilter就行不用重开网卡。长时间抓包的内存问题更隐蔽。如果每个包都new一个对象塞进 model抓一小时能吃掉几个 G。我的做法是环形缓冲固定容量的QList超过上限就从头覆盖同时界面上只渲染当前可见的行用QAbstractTableModel的懒加载。这样抓十万个包内存也稳。// 环形缓冲固定容量满了覆盖最旧的 void PacketModel::appendPacket(const QByteArray data, timeval ts) { if (m_packets.size() MAX_PACKETS) { m_packets.removeFirst(); // 或换成环形数组下标避免 O(n) } m_packets.append(parse(data)); // 只通知新增行别整表刷新 beginInsertRows(QModelIndex(), m_packets.size()-1, m_packets.size()-1); endInsertRows(); }参数说明MAX_PACKETS按内存预算定一个包平均 200 字节加解析开销十万个包大概几十 MB可以接受。removeFirst在 QList 上是 O(n)包量大时换成固定大小数组加写指针性能差一个数量级。验证工具有没有做对最简单的办法是同时开 Wireshark 抓同一块网卡对比包数量、时间戳和协议字段。如果数量对得上、字段一致说明解析链路是通的。我自己的习惯是每加一个新协议解析先拿 Wireshark 抓一段样本存成 pcap 文件再用pcap_open_offline离线喂给自己的解析器这样调试不用反复抓实时流量可复现得多。希望这套思路能帮你把 sniffer 从「能跑」做到「敢用」。本文还有配套的精品资源点击获取