ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QT5+WinPcap从零实现仿WireShark抓包程序:原理、实现与避坑

QT5+WinPcap从零实现仿WireShark抓包程序:原理、实现与避坑 简介这是一份基于QT5与WinPcap实现的网络抓包程序源码功能与界面仿照WireShark面向具备C基础、希望深入理解网络协议与数据包捕获机制的开发者可用于网络安全分析、协议学习与故障排查等场景。压缩包共392个文件约8.15MB包含16个cpp与27个c源文件、29个vcproj与3个sln工程文件、28个h头文件以及ui界面、qrc资源、lib静态库和png、gif图标素材另附html说明文档与可执行程序工程结构完整便于直接编译运行与二次开发。资源已有196人学习下载。通过这份代码读者可以掌握QT5界面与抓包逻辑的交互设计、WinPcap底层数据包读写API的调用方式以及过滤规则设置、抓包结果展示与统计等核心模块的实现思路是理解网络抓包原理、实践网络分析技术的实用参考。1. 从零写一个仿 WireShark 的抓包程序QT5 WinPcap 到底能做成什么样很多人第一次接触网络抓包都是从 WireShark 开始的选网卡、点开始、过滤icmp、点开报文看十六进制。用久了就会冒出一个念头——我自己能不能写一个标题里的这个 Sniffer 项目走的就是这条路用 QT5 做界面用 WinPcap 抓底层数据包做一个能选网卡、能实时列包、能看协议解析的仿 WireShark 工具。它解决的不是“替代 WireShark”而是让你真正搞懂抓包这件事的每一层网卡怎么进混杂模式、驱动怎么把帧交给用户态、协议怎么一层层剥开。适合已经会一点 C、装过 QT5、想从“会用工具”跨到“会做工具”的开发者。下面我按自己踩过的路把选型、环境、抓包循环、协议解析和避坑一次讲透。2. 选型先立住为什么是 QT5 加 WinPcap而不是别的组合2.1 QT5 负责界面WinPcap 负责抓包边界要划清做这类工具最容易翻车的地方是把抓包逻辑和界面逻辑搅在一起。我的做法是严格分层WinPcap 只干三件事——枚举网卡、打开网卡、把原始帧回调出来QT5 只干三件事——把网卡列表画出来、把帧塞进表格、把选中的帧解析成树。中间用一个队列解耦抓包线程往队列里塞界面线程从队列里取。这样抓包再快界面也不会卡死。为什么是 QT5 而不是 MFC因为 QT5 的信号槽天然适合“后台来数据、前台刷新”这种模型QThread加moveToThread的写法比 MFC 的消息映射清爽太多。为什么是 WinPcap 而不是 NpcapWinPcap 是老牌方案接口稳定、资料多pcap.h那套 API 十几年没大变教学和自用足够。Npcap 是它的现代替代支持 Win10 之后的 NDIS 6但接口兼容学会 WinPcap 再换 Npcap 几乎零成本。这里要提醒一句WinPcap 官方早已停止更新新系统上装它可能遇到签名和兼容问题生产环境建议直接上 Npcap 的兼容模式。选型定下来整个项目的骨架就清楚了一个CaptureThread类封装 pcap 循环一个MainWindow类管界面一个PacketParser类管协议解析。三者通过信号槽和队列通信谁都不越界。2.2 环境搭建QT5 与 WinPcap 开发包的安装顺序有讲究环境这块是新手第一个坎。热搜里“qt5安装”“winpcap安装npf错误”“cmake error at qt5config.cmake”这些词说明踩坑的人一大片。我一般按这个顺序来能避开大部分问题。第一步装 QT5。去官网下 Online Installer选msvc2017_64或mingw套件都行关键是记住你的编译器。装完确认qmake -v能输出版本。如果后面 CMake 报qt5config.cmake找不到八成是CMAKE_PREFIX_PATH没指向 QT 的安装目录比如C:/Qt/Qt5.9.4/5.9.4/msvc2017_64/lib/cmake。第二步装 WinPcap。这里有个关键不要只装运行时的WinPcap_4_1_3.exe还要装开发包WpdPack。开发包里才有pcap.h、wpcap.lib、Packet.lib。安装时如果报“npf 错误”通常是系统里已经装了 Npcap两者驱动冲突先卸掉 Npcap 再装 WinPcap或者干脆统一用 Npcap 的开发包。第三步在.pro文件里把库和头文件路径配好# Sniffer.pro QT core gui widgets TARGET Sniffer TEMPLATE app # WinPcap 开发包路径按自己解压位置改 INCLUDEPATH $$PWD/WpdPack/Include LIBS -L$$PWD/WpdPack/Lib/x64 -lwpcap -lPacket SOURCES main.cpp mainwindow.cpp capturethread.cpp packetparser.cpp HEADERS mainwindow.h capturethread.h packetparser.hINCLUDEPATH指向pcap.h所在目录LIBS里-lwpcap是抓包主库-lPacket提供更底层的驱动接口。注意 x64 和 x86 要跟你的 QT 套件一致混用会报链接错误。配好之后先写个最小程序调pcap_findalldevs能列出网卡就说明环境通了。3. 抓包核心用 pcap 循环把网卡数据搬进 QT 界面3.1 枚举网卡与打开设备pcap_findalldevs 的三个参数坑抓包第一步是让用户选网卡。pcap_findalldevs返回一个链表每个节点有名字、描述、地址列表。这里有几个坑名字是\Device\NPF_{GUID}这种内部标识不能直接显示给用户要显示description链表用完必须pcap_freealldevs释放否则内存泄漏如果返回 0 个设备多半是没装驱动或权限不够。// capturethread.cpp 片段枚举网卡 #include pcap.h #include QDebug QStringList CaptureThread::listDevices() { pcap_if_t *alldevs nullptr; char errbuf[PCAP_ERRBUF_SIZE] {0}; QStringList result; // 第一个参数为 nullptr 表示枚举所有网卡 if (pcap_findalldevs(alldevs, errbuf) -1) { qDebug() 枚举失败: errbuf; return result; } for (pcap_if_t *d alldevs; d ! nullptr; d d-next) { // description 可能为空回退到 name QString desc d-description ? QString::fromLocal8Bit(d-description) : QString::fromLocal8Bit(d-name); result desc; // 实际使用时把 d-name 一起存起来打开设备要用 } pcap_freealldevs(alldevs); // 必须释放 return result; }errbuf是错误缓冲区PCAP_ERRBUF_SIZE是它的大小任何 pcap 调用失败都要读它。pcap_findalldevs的第一个参数传nullptr表示要全部设备传具体指针表示从某个设备开始。返回 -1 就是失败。这段代码只负责“列出来”真正打开设备要用d-name所以实际项目里我会用一个结构体把 name 和 description 一起存进QComboBox的userData。3.2 打开设备与设置混杂模式snaplen 和 timeout 怎么定选好网卡后调pcap_open_live。四个关键参数设备名、抓包长度snaplen、是否混杂模式promisc、超时to_ms。snaplen我一般设 65535保证整个帧都能抓到设小了会截断解析大包时数据不全。promisc设 1 表示混杂模式能抓到经过网卡但不是发给本机的帧做流量分析必须开。to_ms是超时设 1000 毫秒意思是即使没包pcap_next_ex也会每秒返回一次方便界面刷新和响应停止指令。// 打开设备 bool CaptureThread::openDevice(const QString devName) { char errbuf[PCAP_ERRBUF_SIZE] {0}; // snaplen65535 抓完整帧, promisc1 混杂模式, to_ms1000 超时 handle pcap_open_live(devName.toLocal8Bit().constData(), 65535, 1, 1000, errbuf); if (!handle) { qDebug() 打开失败: errbuf; return false; } // 只抓以太网帧过滤掉其他链路类型 if (pcap_datalink(handle) ! DLT_EN10MB) { qDebug() 不是以太网设备; pcap_close(handle); handle nullptr; return false; } return true; }pcap_datalink返回链路层类型DLT_EN10MB就是标准以太网。如果抓到的是回环或无线特殊帧类型不同解析偏移量就不一样所以这里先挡一道。handle是pcap_t*全局唯一停止抓包时pcap_close释放。3.3 抓包循环与线程安全pcap_next_ex 的返回值和退出条件抓包循环跑在独立线程里用pcap_next_ex逐包取。它的返回值有三种1 表示拿到包0 表示超时没包-1 表示出错-2 表示读到文件尾离线模式。循环里必须处理 0 和 -1否则要么空转要么崩。// 抓包主循环 void CaptureThread::run() { pcap_pkthdr *header nullptr; const u_char *pktData nullptr; int res 0; while (!m_stop) { res pcap_next_ex(handle, header, pktData); if (res 0) continue; // 超时继续等 if (res -1) break; // 出错退出 if (res -2) break; // 文件尾 // 拷贝一份数据避免 pcap 缓冲区被复用 QByteArray frame(reinterpret_castconst char*(pktData), header-caplen); // 通过信号发给界面线程跨线程自动排队 emit frameCaptured(frame, header-ts); } pcap_close(handle); handle nullptr; }关键点在QByteArray frame(...)这行。pktData指向的是 pcap 内部缓冲区下一次pcap_next_ex就会覆盖它。如果直接把指针 emit 出去界面线程拿到的是野指针这就是典型的“玄学崩溃”。必须拷贝。header-caplen是实际抓到的长度header-len是帧原始长度两者在没截断时相等。header-ts是时间戳后面显示时间要用。界面这边用connect把信号接到槽槽里把帧追加到表格。因为跨线程QT 默认用QueuedConnection自动排队不会阻塞抓包线程。表格行数多了会卡我的做法是限制显示最近 1000 条或者用QAbstractTableModel做虚拟化。4. 协议解析把一帧原始字节拆成能看懂的树4.1 以太网头与 IP 头偏移量和字节序是重灾区拿到一帧第一层是以太网头14 字节6 字节目的 MAC、6 字节源 MAC、2 字节类型。类型0x0800是 IPv40x0806是 ARP0x86DD是 IPv6。解析时用struct映射最方便但要注意字节序——网络字节序是大端x86 是小端ntohs必须用。// packetparser.cpp解析以太网和 IP 头 #pragma pack(push, 1) // 禁止结构体对齐填充 struct EthernetHeader { uint8_t dstMac[6]; uint8_t srcMac[6]; uint16_t etherType; // 网络字节序 }; struct IPv4Header { uint8_t verIhl; uint8_t tos; uint16_t totalLen; uint16_t id; uint16_t flagsFrag; uint8_t ttl; uint8_t protocol; uint16_t checksum; uint32_t srcIp; uint32_t dstIp; }; #pragma pack(pop) void PacketParser::parse(const QByteArray frame) { if (frame.size() 14) return; auto *eth reinterpret_castconst EthernetHeader*(frame.constData()); uint16_t type ntohs(eth-etherType); // 转主机字节序 if (type 0x0800 frame.size() 14 20) { auto *ip reinterpret_castconst IPv4Header*(frame.constData() 14); int ihl (ip-verIhl 0x0F) * 4; // 首部长度单位 4 字节 QString src ipToString(ip-srcIp); QString dst ipToString(ip-dstIp); uint8_t proto ip-protocol; // 6TCP, 17UDP, 1ICMP // 继续解析传输层偏移 14 ihl } }#pragma pack(push,1)是必须的否则编译器会按默认对齐在结构体里插填充字节偏移全错。verIhl高 4 位是版本低 4 位是首部长度单位是 4 字节所以乘 4 才是真实字节数。ntohs把 16 位网络序转主机序ntohl对应 32 位。IP 地址是 32 位整数要按字节拆成点分十进制。4.2 TCP/UDP/ICMP 分流端口和标志位怎么读IP 头的protocol字段决定下一层。TCP 和 UDP 都有源端口、目的端口各 2 字节用ntohs转。TCP 头还有序号、确认号、标志位标志位在偏移 13 的那个字节SYN是0x02ACK是0x10FIN是0x01。ICMP 简单第一个字节是类型8 是请求0 是应答这就是 ping 的两种包。// 传输层解析 struct TcpHeader { uint16_t srcPort; uint16_t dstPort; uint32_t seq; uint32_t ack; uint8_t dataOffset; // 高4位是首部长度 uint8_t flags; uint16_t window; uint16_t checksum; uint16_t urgent; }; void PacketParser::parseTcp(const QByteArray frame, int offset) { auto *tcp reinterpret_castconst TcpHeader*(frame.constData() offset); quint16 sport ntohs(tcp-srcPort); quint16 dport ntohs(tcp-dstPort); int headerLen ((tcp-dataOffset 4) 0x0F) * 4; bool syn tcp-flags 0x02; bool ack tcp-flags 0x10; // payload 从 offset headerLen 开始 }dataOffset高 4 位才是首部长度低 4 位保留别整个字节拿来乘。标志位用按位与判断一个字节里可能同时有多个标志。payload 的起始位置是 IP 偏移加 TCP 首部长度长度用 IP 总长减去 IP 首部再减 TCP 首部。4.3 用 QT5 的 QTreeWidget 呈现协议树解析结果要显示成 WireShark 那种可展开的树。QT5 的QTreeWidget正好顶层节点是以太网、IP、TCP子节点是各字段。选中某帧时清空树、重新填。字段多了要注意性能别每帧都new一堆 item可以复用。// mainwindow.cpp填充协议树 void MainWindow::showPacketDetail(const QByteArray frame) { ui-treeDetail-clear(); auto *ethItem new QTreeWidgetItem(ui-treeDetail); ethItem-setText(0, Ethernet II); new QTreeWidgetItem(ethItem, QStringList() 目的MAC getDstMac(frame)); new QTreeWidgetItem(ethItem, QStringList() 源MAC getSrcMac(frame)); new QTreeWidgetItem(ethItem, QStringList() 类型 getEtherType(frame)); // IP、TCP 节点同理逐层挂上去 ui-treeDetail-expandAll(); }QTreeWidgetItem的第一个参数是父节点传ui-treeDetail就是顶层。QStringList里第一个是列 0第二个是列 1。expandAll展开所有节点帧特别大时可以只展开前两层。表格和树之间用currentRowChanged信号联动点表格某行就刷新树。5. 避坑与排查那些让 Sniffer 跑不起来的真实原因5.1 抓不到任何包列表却是空的现象程序能启动网卡列表也有点开始后一条包都不来。原因通常有三个一是没开混杂模式只抓到本机流量如果本机没通信就看不到二是选错了网卡比如选了虚拟网卡或已断开的无线网卡三是权限不够WinPcap 需要管理员权限才能打开设备。解决确认promisc传 1用ipconfig核对网卡右键以管理员身份运行。还不行就在pcap_open_live后打印pcap_datalink看是不是DLT_EN10MB。5.2 界面卡死点停止没反应现象抓包一开界面就转圈按钮点不动。原因抓包循环跑在了 UI 线程里pcap_next_ex阻塞了事件循环。解决把CaptureThread继承QThread循环放run()里或者用moveToThread把对象移到子线程。停止时不要直接terminate设一个volatile bool m_stop循环里检查它退出后pcap_close。terminate会跳过清理句柄泄漏下次打开就失败。5.3 解析出来的 IP 和端口全是乱的现象MAC 看着对IP 显示成0.0.0.0或奇怪数字端口是五位数。原因忘了字节序转换或者结构体没#pragma pack。解决所有 16 位、32 位字段过ntohs/ntohl结构体前后加#pragma pack(push,1)和pop用reinterpret_cast前先检查frame.size()够不够越界读会拿到垃圾数据甚至崩溃。5.4 装 WinPcap 报 npf 错误或驱动起不来现象安装程序提示“npf 驱动安装失败”或者设备管理器里 WinPcap Packet Driver 有黄色感叹号。原因系统已装 Npcap两者驱动同名冲突或者系统禁用了测试签名老驱动装不上。解决控制面板卸载 Npcap 和 WinPcap重启再单独装 WinPcapWin10 以上建议直接用 Npcap安装时勾选“WinPcap API 兼容模式”代码不用改。热搜里“vivado winpcap安装失败”也是同类问题本质都是驱动冲突。5.5 抓到的包时间戳不对或显示乱码现象时间列显示 1970 年或者协议字段里中文乱码。原因header-ts是timeval秒和微秒要自己拼字符串字段用了QString::fromLocal8Bit还是fromUtf8取决于源编码。解决时间用QDateTime::fromSecsSinceEpoch(header-ts.tv_sec)再补微秒MAC 和 IP 自己格式化成十六进制字符串别直接当文本读。WireShark 里调北京时间也是类似思路时区偏移自己加。6. 进阶技巧让这个 Sniffer 从能用到好用写到这儿基本功能已经能跑了。但要让它在自己手里真正好用还有几个技巧值得加。第一个是过滤器。WinPcap 支持 BPF 过滤表达式pcap_compile加pcap_setfilter在抓包前设好能大幅减少无用包。比如只想看某个 IP 的流量表达式写host 192.168.1.100只看 ICMP 写icmp。这比抓完再在界面里筛高效得多因为过滤发生在内核层。// 设置 BPF 过滤器 struct bpf_program fp; // 只抓 80 端口的 TCP 包 if (pcap_compile(handle, fp, tcp port 80, 0, PCAP_NETMASK_UNKNOWN) 0) { pcap_setfilter(handle, fp); pcap_freecode(fp); // 编译结果用完释放 }pcap_compile把表达式编译成 BPF 字节码optimize传 0 或 1 都行netmask用PCAP_NETMASK_UNKNOWN表示不关心。pcap_setfilter之后pcap_next_ex只会返回匹配的包。pcap_freecode必须调否则每次设过滤器都泄漏。第二个技巧是保存和读取 pcap 文件。pcap_dump_open加pcap_dump能把抓到的包写成标准 pcap 格式WireShark 直接能打开反过来pcap_open_offline能读别人给的 pcap 文件做离线分析。这样你的工具就不只是实时抓包还能当分析器用。写文件时注意pcap_dump_flush定期刷盘不然程序崩了数据丢一半。第三个技巧是性能。抓千兆流量时逐包 emit 信号会压垮事件循环。我的做法是攒一批再发比如每 50 个包或者每 100 毫秒发一次QVectorFrame界面批量插入。表格用setUpdatesEnabled(false)包住插入过程插完再true能明显减少重绘。内存上别把所有帧都留着只存最近 N 条老的丢掉否则跑一晚上内存就爆了。最后一个习惯每次改完解析代码先拿 WireShark 抓一个已知的包存成 pcap用你的程序读它逐字段和 WireShark 对比。对不上就查偏移和字节序。这个“后悔药”比在实时流量里瞎猜快十倍。我自己就是这么把 TCP 选项字段一个个对齐的。希望帮到你。本文还有配套的精品资源点击获取
返回列表