简介:这是一份基于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 而不是 Npcap?WinPcap 是老牌方案,接口稳定、资料多,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}; // snaplen=65535 抓完整帧, promisc=1 混杂模式, to_ms=1000 超时 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_cast<const 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是 IPv4,0x0806是 ARP,0x86DD是 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_cast<const EthernetHeader*>(frame.constData()); uint16_t type = ntohs(eth->etherType); // 转主机字节序 if (type == 0x0800 && frame.size() >= 14 + 20) { auto *ip = reinterpret_cast<const 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; // 6=TCP, 17=UDP, 1=ICMP // 继续解析传输层,偏移 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是0x02,ACK是0x10,FIN是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_cast<const 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,重启,再单独装 WinPcap;Win10 以上建议直接用 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 毫秒发一次QVector<Frame>,界面批量插入。表格用setUpdatesEnabled(false)包住插入过程,插完再true,能明显减少重绘。内存上,别把所有帧都留着,只存最近 N 条,老的丢掉,否则跑一晚上内存就爆了。
最后一个习惯:每次改完解析代码,先拿 WireShark 抓一个已知的包存成 pcap,用你的程序读它,逐字段和 WireShark 对比。对不上就查偏移和字节序。这个“后悔药”比在实时流量里瞎猜快十倍。我自己就是这么把 TCP 选项字段一个个对齐的。希望帮到你。
本文还有配套的精品资源,点击获取