尧图精选

VC++ MFC网络嗅探器源码解析:WinPcap抓包与协议解析实战

🕒 发布时间:2026/10/1 23:32:35 📁 来源:尧图网络
简介这份基于VC实现的网络嗅探器Sniffer完整源代码面向希望学习网络数据包捕获、协议解析及WinPCAP编程的C开发者可直接用于二次开发或毕业设计参考。压缩包共21个文件涵盖6个C/C头文件、3个源文件、1个工程文件及7个界面图标等资源整体仅82KB结构清晰。代码中包含主对话框实现、嗅探核心逻辑、IP头部定义以及资源脚本重点演示了pcap_open_live、pcap_setfilter等API的调用、过滤规则设置、IP/TCP/UDP头部解析以及捕包线程与界面的交互方式。其中头文件负责协议结构与宏定义源文件包含界面响应和捕包逻辑资源文件管理菜单、对话框和图标便于按模块对照学习。已有750人学习下载适合想要通过实际源码理解网络嗅探器工作原理、提升网络编程和GUI编程技巧的读者。1. 网络嗅探器 Sniffer 源代码一份能跑通抓包全流程的 VC 工程接到一份网络嗅探器 Sniffer 源代码时你先别急着把 zip 解压就关掉。这份代码有意思的地方在于它不是那种只有几十行、把 pcap_open_live 调用一遍就结束的教学 demo而是一个基于 VC 和 MFC 对话框框架、完整实现了网卡选择、过滤条件设置、数据包捕获、协议头解析和列表展示的 Windows 抓包工具。对做网络协议分析、搞嵌入式上位机、或者想把抓包能力集成到自己 Windows 工具里的工程师来说它的价值不是“能抓包”而是把 WinPcap 从初始化到回调再到 UI 刷新的完整链路摊开在你面前。读完源码你会明白一个事实抓包工具的核心难点从来不是打开网卡而是线程模型、字节序和协议头解析这三件事。2. 源码包结构拆解从文件清单反推 MFC 工程架构拿到 zip 解压后第一件事不是急着双击 .sln而是看文件归属。这份代码的文件命名非常规矩基本能直接看出分层有主程序入口、有主对话框、有协议头定义、有资源文件还有几个从 shell32.dll 里引出来的图标。这一章先把文件角色讲清楚后面读代码时就不会迷路。2.1 入口与主对话框Sniffer.cpp 和 SnifferDlg.cpp 的分工Sniffer.cpp 和 Sniffer.h 是 MFC 应用类 CWinApp 的实现负责程序启动、初始化实例、创建主对话框。这里有一个很典型的设计点主流程基本在 InitInstance 里完成包括 AfxEnableControlContainer、创建 CDialog 实例并 DoModal。SnifferDlg.cpp 和 SnifferDlg.h 才是真正的业务核心对话框上通常有网卡下拉框、开始/停止按钮、过滤条件编辑框以及一个用来显示捕获结果的 ListView 或 CListCtrl。SnifferDlg 的 OnInitDialog 里一般会做三件事枚举本机网卡并填充下拉列表、初始化 ListView 列头、把“开始捕获”按钮置灰或置亮。这块代码的看点不在 UI 设计而在控件与捕包线程之间的消息传递方式后面第三章会展开。stdafx.h 和 stdafx.cpp 是预编译头里面包含 Windows SDK 的常用头文件、MFC 头文件以及项目级宏定义。老 VC 工程里 stdafx.h 里常见 #define VC_EXTRALEAN 和 _WIN32_WINNT 版本宏。如果你拿到代码后在编译时报一堆“找不到头文件”的错误八成是预编译头里的系统宏和当前开发环境的 SDK 版本不匹配。2.2 协议相关定义iphdr.h 里是 IP 头Definition.h 里是业务常量iphdr.h 从文件名就能判断定义的是 IP 头结构。实际解析时数据包的前 14 字节是以太网头接着才是 IP 头。所以 iphdr.h 里的结构体一般是直接从 IP 头起始位置映射的。这里要提醒一句老代码里经常用位域来定义 IP 头的 version 和 ihl 字段这在 x86 小端机器上能跑但如果今天你要把这份代码移植到其他平台或换编译器位域的内存布局就会变得不可控后面第四章会给出更稳妥的按字节解析写法。Definition.h 这个文件容易被忽略但它往往藏着一些自定义协议常量、缓冲区大小、消息号定义。比如自定义 Windows 消息 WM_CAPTURE_PACKET用来从捕包线程把数据包送达 UI 线程或者定义一些显示用的枚举值。如果你在 SnifferDlg.cpp 里看到 PostMessage 一个自定义消息号回去 Definition.h 里大概率能找到它的原始定义。读代码时别跳过这种“看起来没什么内容”的头文件老项目的全局约定都写在里面。2.3 资源文件与工程配置Sniffer.rc、resource.h 和 vcprojSniffer.rc 是资源脚本定义了对话框模板、菜单、字符串表、图标和版本信息。resource.h 是这些资源的 ID 定义两者一一对应。图标文件那一排很有时代感Comp2Comp.ico、TCPHeader.ico、IPHeader.ico 是功能按钮的图标而 shell32.dll#274.ico、shell32.dll#322.ico 这种写法是从系统 shell32.dll 里按索引取图标常见做法是直接 LoadIcon 或 SHGetFileInfo 拿到系统图标省去自己画图标的成本。Sniffer.manifest 是 XP 风格视觉样式的清单文件让程序使用公共控件 v6 版本。Sniffer.vcproj 是 Visual Studio 2008 及更早版本的项目文件格式如果你用 VS2010 以上版本打开会提示项目格式不兼容或建议转换。Sniffer.sln 是解决方案文件负责组织工程。需要特别注意的是这份工程依赖 WinPcap 开发包。编译时必须保证安装了 WinPcap 或 Npcap 的 SDK即 WpdPack 或 Npcap SDKC/C 附加包含目录指向 SDK 里的 Include 目录链接器附加依赖库里有 wpcap.lib部分场景还需要 Packet.lib运行目标机器上安装有 WinPcap 驱动或 Npcap 的 WinPcap 兼容模式。这些配置细节在第五章会逐个踩坑说明。你在 vcproj 里不一定能看到这些路径因为它们可能是通过 VC 全局目录配置的换一台机器就失效。拿到代码先检查这一层再谈编译。2.4 老工程的文件组织对今天的借鉴意义这个项目的文件组织虽然老但分层思路清晰资源、入口、对话框、数据定义、协议头各归其位。对照今天很多人写抓包工具时把所有代码塞进一个 .cpp 的做法这份老代码反而更适合作为学习样本。阅读顺序建议是 Definition.h → iphdr.h → SnifferDlg.cpp。先知道有哪些自定义消息和常量再看协议头最后才看界面逻辑这样读 SnifferDlg.cpp 的时候不会被一堆 PostMessage 和指针转换打乱节奏。3. 抓包主流程网卡枚举、打开设备、回调线程与过滤器这一章是整份源码含金量最高的部分。SnifferDlg.cpp 里的捕包逻辑基本可以归纳为四个步骤枚举网卡、打开设备设置参数、编译并设置过滤规则、进入循环抓包。每一步都有隐藏细节。3.1 网卡枚举pcap_findalldevs 与下拉框填充对话框初始化时代码会调用 pcap_findalldevs 获取本机所有网络接口。函数原型如下int pcap_findalldevs(pcap_if_t **alldevs, char *errbuf);常见写法是pcap_if_t *alldevs NULL; pcap_if_t *d; char errbuf[PCAP_ERRBUF_SIZE]; CComboBox *pCombo (CComboBox*)GetDlgItem(IDC_DEVICE_COMBO); if (pcap_findalldevs(alldevs, errbuf) -1) { AfxMessageBox(errbuf); return; } int idx 0; for (d alldevs; d ! NULL; d d-next) { CString desc d-description ? d-description : d-name; pCombo-AddString(desc); // 把设备名存到下拉项的附加数据里 pCombo-SetItemData(idx, (DWORD_PTR)d-name); idx; } pcap_freealldevs(alldevs);对这段代码有三点需要说明。第一errbuf 的大小必须是 PCAP_ERRBUF_SIZE它定义在 pcap.h 中一般靠宏确保长度不要自己写死 256 之类的数字。第二pcap_findalldevs 返回的链表包含了本机所有接口包括虚拟网卡和回环接口所以在存在 VMware、VirtualBox 或 WSL 网卡的机器上下拉框会多出很多项。老代码通常直接把所有设备都塞进去不区分物理网卡和虚拟网卡实际使用时会很痛苦。第三SetItemData 存设备名的时候设备名指向的是 alldevs 链表内部的内存而后面调用了 pcap_freealldevs这块内存就失效了。正确做法是把设备名拷贝出来用 strdup 或 new 出来存到 ItemData 里。这就是一个典型的老代码隐患第一次启动可能没问题一旦网卡数量变化或系统空闲内存紧张下拉框里的数据就是野指针。3.2 打开设备pcap_open_live 的参数敏感性用户选好网卡点“开始”代码会拿到设备名调用 pcap_open_live。函数原型pcap_t *pcap_open_live(const char *device, int snaplen, int promisc, int to_ms, char *errbuf);参数含义分别是设备名、快照长度、混杂模式、超时毫秒数、错误缓冲。常见参数是 pcap_open_live(dev, 65536, 1, 1000, errbuf)。65536 表示捕获每个数据包的前 64KB能保证不截断任何常见以太网帧混杂模式置 1 表示接收所有经过网卡的数据包1000 是读超时表示若没有数据到达回调最多等待 1 秒也会被触发一次。这里有一个容易翻车的点to_ms 参数在旧版 WinPcap 里直接影响回调触发频率设置为 0 时网卡缓冲区满才会上报导致界面看起来“卡住”。老代码里如果看到 pcap_open_live 的第四参数写 0 或 -1建议改成 500 或 1000否则数据包量少时列表刷新几乎是停滞的。如果是 Windows 10 以上系统配合 Npcap还要确认安装时勾选了兼容 WinPcap API 的选项否则 pcap_open_live 会返回 NULL 报错。3.3 pcap_loop 回调与线程模型别让抓包阻塞 UI捕包最忌讳的是直接在 UI 线程里跑循环。Sniffer 的做法符合正规套路用 AfxBeginThread 启动一个工作线程在线程函数里调用 pcap_loop每次捕获一个数据包就触发一次回调。UINT CaptureThreadProc(LPVOID pParam) { CSnifferDlg *pDlg (CSnifferDlg*)pParam; pcap_loop(pDlg-m_pcapHandle, 0, packet_handler, (u_char*)pDlg); return 0; } void packet_handler(u_char *user, const struct pcap_pkthdr *header, const u_char *pkt_data) { CSnifferDlg *pDlg (CSnifferDlg*)user; // 这里不能直接操作 CListCtrl // 只做轻量解析把数据包指针投递回 UI 线程 pDlg-PostMessage(WM_CAPTURE_PACKET, (WPARAM)header-len, (LPARAM)pkt_data); }pcap_loop 的第二个参数传 0 表示无限循环直到出错或 pcap_breakloop 被调用传正整数表示最多捕获多少个包后返回。回调函数里绝对不能直接调用 SetItemText、InsertItem 之类的 MFC 控件操作因为回调是在工作线程上下文中执行的直接操作控件会导致资源冲突和界面卡死。正确姿势是把数据包内容拷贝一份或用 PostMessage 把指针传出去在 UI 线程的消息处理函数里更新列表。这里还有个细节PostMessage 传出的 pkt_data 指针是 pcap 库内部的缓冲区循环继续后该内存会被覆盖。所以消息处理时要么同步处理完要么在回调里就把头部字段提取成值再传过去。Sniffer 源码的解析逻辑大概率在回调里做了但如果你看到直接把指针 Post 出去再异步读取的写法那就是一个等着翻车的设计。3.4 过滤器设置pcap_compile 与 pcap_setfilter 的时机过滤功能本质上是把用户输入的 BPF 表达式编译成内核可执行的过滤程序。调用链是 pcap_compile 加 pcap_setfilterstruct bpf_program fp; char filter_exp[] tcp port 80 or udp port 53; if (pcap_compile(handle, fp, filter_exp, 1, PCAP_NETMASK_UNKNOWN) -1) { // 表达式语法错误 return; } if (pcap_setfilter(handle, fp) -1) { // 设置过滤失败 return; } pcap_freecode(fp);pcap_compile 的第三个参数是“优化”标志传 1 表示让编译器优化过滤代码第四个是网络掩码用于处理广播地址类过滤传 PCAP_NETMASK_UNKNOWN 则按 255.255.255.255 处理。这里最常犯的错误是把过滤设置放在 pcap_loop 之后调用结果前几个包已经被抓上来才发现过滤条件没生效。正确顺序是先打开设备、编译过滤、设置过滤最后才进循环。BPF 表达式的语法优先级也值得注意tcp and port 80 和 tcp or port 80 的结果完全不同要抓非标准端口需要写成 portrange 而不是 port 加区间。4. 协议解析IP 头、TCP 头、字节序与位域布局抓包拿到的是裸流量从以太网帧到 TCP/UDP 载荷每一层都要手动掐头。Sniffer 源码里的 iphdr.h 定义了 IP 头结构但直接用结构体映射线上数据有一个隐藏的地雷字节序和位域。4.1 IP 头之字节解析别直接映射位域IP 头的前两个字段 version 和 ihl 各占 4 位。老代码惯用位域表示但位域的内存分布由编译器决定在 Windows x86 上 low-order bits 对应 version到了 Linux 或者做了结构体对齐调整后就会出错。一个更适合跨平台或新编译器的做法是直接按字节取#pragma pack(push, 1) typedef struct { u_char ver_ihl; // 高4位是 version低4位是 ihl u_char tos; // 服务类型 u_short total_len; // 总长度网络字节序 u_short id; // 标识 u_short flags_frag; // 标志与分片 u_char ttl; // 生存时间 u_char protocol; // 协议号 u_short checksum; // 校验和 u_int src_ip; // 源 IP u_int dst_ip; // 目的 IP } ip_header_t; #pragma pack(pop) BYTE version (pIpHdr-ver_ihl 4) 0x0F; BYTE ihl (pIpHdr-ver_ihl 0x0F) * 4; // 单位为 4 字节这段代码用 #pragma pack(push, 1) 强制结构体对齐为 1避免编译器在字段之间插入填充字节。ver_ihl 一个字节拆成两个 4 位字段右移和掩码是手工解位域不受编译器端序影响无论小端大端都能正确拿到 4 和 5 这两个典型值。ihl 要乘 4 才是真实的字节数IP 头通常 20 字节所以这个值通常是 5。为什么要强调字节解析因为很多人在回调里看到 ip_header_t 里的 version 字段 0x45 或 0x40 就蒙了其实是原始字节的最高位恰好是 version 的值位域自动完成了拆解一旦换编译器这个自动拆解就崩了。4.2 TCP 端口与标志位提取协议号判断与偏移IP 头之后要根据 protocol 字段判断上层是 TCP、UDP 还是 ICMP。protocol 为 6 是 TCP为 17 是 UDP为 1 是 ICMP。TCP 头结构如下#pragma pack(push, 1) typedef struct { u_short src_port; u_short dst_port; u_int seq; u_int ack; u_char offset_res; // 高4位为头部长度 u_char flags; // TCP 标志位 u_short window; u_short checksum; u_short urgent; } tcp_header_t; #pragma pack(pop)提取端口时一定要注意字节序src_port 和 dst_port 在线上是大端存储在 x86 机器上直接读到的数值是反的。必须 ntohs 转换u_short src ntohs(pTcp-src_port); u_short dst ntohs(pTcp-dst_port); BYTE tcp_len (pTcp-offset_res 4) * 4;offset_res 高 4 位是数据偏移单位也是 4 字节通常为 5即 TCP 头 20 字节。flags 字节里每个 bit 对应一个标志0x02 是 SYN、0x01 是 FIN、0x10 是 ACK。4.3 字节序转换与显示格式化肉眼对齐的最后一公里解析完成后的显示环节同样有坑。IP 地址在结构体里虽然是 32 位无符号整数但直接在内存里按小端解释会导致 192.168.1.1 显示成 1.1.168.192。正确做法是用 inet_ntoa 或 inet_ntop或者手工转换CString FormatIp(UINT ip) { BYTE *p (BYTE*)ip; CString s; s.Format(%d.%d.%d.%d, p[0], p[1], p[2], p[3]); return s; }这里云诡的地方在于p[0] 到底是 IP 地址的高位还是低位取决于运行平台。Windows 小端机器上网络字节序的 192.168.1.1 存储在内存里是 C0 A8 01 01从低地址开始读分别是 0xC0、0xA8、0x01、0x01正好符合预期。但如果拿这份代码到其他平台这种直接按内存字节读的写法就不稳定。推荐方式是先用 ntohl 把网络序 IP 转成主机序再用 inet_ntoa 格式化或者直接用现代版的 inet_ntop它输出的是标准点分十进制不依赖平台字节序。时间戳和包长也有类似问题。pcap_pkthdr 里的 ts 是 timeval 结构在 Windows 上 tv_sec 是 long单位是秒在 Linux 上可能是长整型。跨平台移植这份 Sniffer 时时间格式化部分要单独封装用自定义函数统一输出 HH:MM:SS.mmm。5. 编译与运行避坑这份老代码在今天的 Windows 上最常见的五个问题这个项目我是实际编译过的踩过的坑比看代码花的时间还多。以下问题按出现频率排序每条给出现象、原因和解决方式。5.1 编译报错找不到 pcap.h现象fatal error C1083: Cannot open include file: pcap.h: No such file or directory。原因WinPcap 开发库没装或者附加包含目录没配置。老工程里常在全局 VC 目录里配置 include 和 lib 路径换机器后这些配置会丢。解决安装 Npcap SDK或旧版 WpdPack解压后会得到 Include 和 Lib 目录。在项目属性里设置 C/C → General → Additional Include Directories指向 Include 目录链接器 → Input → Additional Dependencies 里加 wpcap.lib。同时要把 Lib 目录按目标平台加对编译 32 位程序用 x86 目录下的库64 位程序用 x64 目录下的库。很多人在这一步翻车是 Lib 路径选错导致一堆 LNK2019。5.2 打开 vcproj 闪退或提示版本过低现象双击 Sniffer.sln 后 VS2022 提示“此版本的应用程序不支持项目类型”或者干脆拒绝加载工程。原因Sniffer.vcproj 是 VS2008 及更早版本的项目格式VS2012 之后就不再支持直接加载。解决不要浪费时间做旧格式转换直接新建一个 MFC 对话框工程把 Sniffer.cpp、SnifferDlg.cpp、Definition.h、iphdr.h 全部添加到工程里再把 Sniffer.rc 中的对话框资源手工挪过去。这样做大概花十几分钟比重试转换工具稳定得多。如果你只需要编译命令行版本也可以用 cl 命令手动编译但那样 UI 资源全部要重做得不偿失。5.3 程序启动就弹窗“wpcap.dll 缺失”现象编译链接都过了但 exe 一运行就提示找不到 wpcap.dll。原因编译时成功是因为链接到了 wpcap.lib但目标机器上没有安装 WinPcap 或 Npcap 运行时或者安装了 Npcap 却没勾选兼容 WinPcap 模式。解决开发机安装 Npcap 并勾选“WinPcap API 兼容模式”若是发布到其他机器把 wpcap.dll 和 packet.dll 和 exe 放到同一目录或做成静默安装包。注意现在官网新版 Npcap 很在意兼容模式选项安装时如果漏勾抓包 API 会返回错误。5.4 过滤条件设置不生效不该抓的包全进来了现象在过滤框里输入 tcp port 80点击开始后依然收到 SSH、DNS 等非 80 端口的包。原因过滤条件设置时机太晚或者 pcap_setfilter 返回了非 0 但代码没有检查错误返回值。解决严格遵守“打开 handle → pcap_compile → pcap_setfilter → pcap_loop”的顺序并且每次调用 pcap_setfilter 后检查返回值。如果返回 -1调用 pcap_geterr(handle) 打印错误信息。大多数情况是用户没有在代码里调用 pcap_compile直接调 pcap_setfilter 传了一个空表达式导致过滤器整体失效。5.5 抓包过程界面假死停止后才刷屏现象点击开始捕获后窗口无法拖动和点击数据包列表没有实时更新停止捕获后列表突然一次性填充出几百条记录。原因这是最典型的线程模型反例——pcap_loop 被放在了 UI 线程里跑WinPcap 阻塞等待数据窗口消息循环根本没机会执行。回调里的控件更新实际上是在循环每帧之间做但主线程卡在 pcap_loop 里所有消息被堵死。解决用 AfxBeginThread 起独立线程跑 pcap_loop回调里只做字段提取并通过 PostMessage 发给 UI 线程。同时在 OnDestroy 里调用 pcap_breakloop 和线程等待保证关闭窗口时线程能顺利退出。修复后界面流畅度立竿见影列表是按包逐条刷新而不是整屏重绘。6. 扩展方向把演示工具改造成日常能用的抓包助手Sniffer 源码拿下来跑通只是第一步真要落到日常排障还得加两个小功能自定义过滤器控件和源/目的 IP 快速过滤。前者在对话框上加一个编辑框把用户输入直接用 pcap_compile 编译成过滤程序后者在列表双击某一条记录时自动以该 IP 为条件重新过滤。这两个改造都不动核心架构各自二十几行就能做完但能把“演示工具”变成“日常能顺手用的排障工具”。如果还要更进一步建议关注 TCP 流重组。现在这份代码只是逐包解析做应用层分析时基本不够用。常见做法是维护一个以连接四元组为 key 的缓冲表struct ConnKey { UINT src_ip; UINT dst_ip; u_short src_port; u_short dst_port; };按这个键把同一连接的所有 TCP 载荷拼起来同时处理 SYN、FIN、RST 和超时来管理连接状态。工程量不大但已经超出 Sniffer 源码本身的范围属于进阶方向。另外可以加一个“导出为 pcap”按钮把内存中已经解析过的数据包按 pcap 文件格式写出来这样就能拿到 Wireshark 里二次分析。pcap 文件格式很简单24 字节全局头之后每个包一组 16 字节包头加原始数据。对着格式写代码不需要额外依赖。我自己改这份代码时最大的教训是千万别直接信任回调传来的指针。那一次我把 pkt_data 保存到一个全局变量里打算“稍后用”结果下一帧数据就把内容覆盖了排查了大半个下午才发现是缓冲区生命周期问题。从那以后我每次处理抓包回调都强制走一遍“先拷贝再投递指针不过线程”的纪律这个习惯一直带到了后来写 Npcap 工具和协议分析模块的所有项目里。希望这份源码的整理和避坑记录能帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →