简介:网络嗅探器Sniffer源代码包是一套基于VC++实现的数据包捕获完整工程,内含可直接编译的解决方案,适合熟悉C++并希望深入网络底层编程的开发者。整个资源共21个文件,包括6个头文件、3个C++源文件,以及图标、资源脚本、工程配置等辅助文件,压缩包仅82KB,代码量适中、便于逐行研读。核心源码围绕WinPCAP库展开,从pcap_open_live建立捕获句柄,到pcap_loop循环抓包,再到IP、TCP、UDP协议头部解析,均给出了完整实现;对话框代码中则提供了启动/停止捕包、设置基于端口或协议的过滤条件等交互逻辑,同时还通过多线程处理好捕包与界面刷新的并发问题。仔细研读iphdr.h与Definition.h,不仅可以掌握协议字段提取方法和过滤规则写法,还能理解预编译头、资源文件在真实项目中的组织方式。目前已有750人学习下载,对于想自建网络抓包工具或从源码层面理解嗅探器原理的开发者,这是一份高性价比的实战参考资料。
1. 网络嗅探器 Sniffer:一份 VC++ 源码包能教会你的不止是抓包
想搞懂网络数据包长什么样,最快的路不是去背 TCP/IP 协议书,而是直接读一份能编译、能运行、能抓包的源码。这份基于 VC++ 写的网络嗅探器 Sniffer 源代码,正好是把“网卡收到的一堆字节”拆成“IP 头 + TCP 头 + 数据内容”的完整示例。它不是什么商业级工具,但作为学习 WinPcap 编程、MFC 对话框工程、协议解析三者如何拼在一起的教材,价值比大多数教程高得多——因为它没有把关键结构体和回调函数藏起来。适合三类人:刚学完 C++ 想碰网络编程的、课程设计要做抓包工具的、以及工作里被要求“做一个内网抓包分析器”但没头绪的工程师。源码包不大,核心逻辑集中在几个文件里,照着我下面的思路读,一晚上能通。
2. Sniffer 源码结构与 WinPcap 选型:先看懂文件再碰代码
2.1 文件清单背后的工程骨架:Sniffer.cpp 与 SnifferDlg.cpp 各自干什么
拿到Sniffer 源代码.zip解压后,第一反应别去翻Sniffer.cpp,先去猜这个工程分几层。我拆下来的结果是这样:Sniffer.cpp和Sniffer.h是程序入口和主逻辑,负责初始化 WinPcap、枚举网卡、启动捕获;SnifferDlg.cpp和SnifferDlg.h是 MFC 对话框类,管界面——按钮、列表控件、过滤条件输入框的响应函数全在这;iphdr.h是 IP 头结构体定义,Definition.h里放着自定义常量、全局变量和协议解析相关的宏;resource.h和Sniffer.rc是界面资源的 ID 映射和脚本。也就是说,捕包逻辑和界面代码是分开的,这正好对应了“捕获线程不能阻塞 UI 线程”的经典设计。
| 文件 | 职责 | 对应知识点 |
|---|---|---|
| Sniffer.cpp / Sniffer.h | 程序入口、全局初始化、捕获调度 | WinPcap API 生命周期 |
| SnifferDlg.cpp / SnifferDlg.h | 对话框控件响应、列表刷新 | MFC 消息映射 |
| iphdr.h | IP 头、TCP 头结构体 | 协议字段偏移 |
| Definition.h | 常量、回调函数声明 | 全局状态管理 |
| stdafx.h / stdafx.cpp | 预编译头、常用库导入 | 编译速度优化 |
| resource.h / Sniffer.rc | 控件 ID、图标、对话框模板 | Win32 资源体系 |
初学者最容易混淆的是SnifferDlg.cpp里的OnStartButton()和Sniffer.cpp里的CaptureThread()的关系:前者是用户点击“开始抓包”后在 UI 线程执行的,后者是真正跑pcap_loop()的独立线程。很多教材把这两层混在一起写,读这份源码时会发现它是有意识拆开的——这是加分项,说明作者懂 Windows 消息循环的脾气。
2.2 为什么选 WinPcap 而不是 raw socket:pcap_open_live 与网卡驱动
写抓包工具有两条路:调用 Socket 层的原始套接字,或者调用 WinPcap 的库函数。Sniffer 选的是 WinPcap。原因不复杂:raw socket 在 Windows 上有一堆限制,比如收不到以太网头、回环流量难处理、过滤规则要自己写匹配逻辑;而 WinPcap 在驱动层做镜像,网卡收到的每个包都会复制一份给应用层。这一点在源码里体现得很直接——它需要调pcap_findalldevs()枚举网卡,然后pcap_open_live()打开设备,这两个函数是干底层活的,普通 socket 接触不到。代价是程序发布时要带着 WinPcap 的运行时,或者用 npcap 替代,部署时多了个依赖项。
对学习来说,WinPcap 的优势是接口直观:打开设备、设置过滤、循环抓包、关闭设备,四个步骤对应四组 API,背下来就能用。pcap_open_live()的参数里有个promisc标记,Sniffer 源码里一般会把它设为 1,意思是开启混杂模式,这样交换机上不发给本机的包也能被截到。做实验时建议先拔掉公司内网网线,在自己虚拟机里测,避免不必要的麻烦。
2.3 预编译头与资源文件:stdafx.h 和 resource.h 的实际作用
stdafx.h这个文件名劝退过不少人,其实它就是一个“把常用头文件提前编译一次”的加速器。本项目里它大概会引入windows.h、pcap.h、afxdialog.h这些大头,避免每个 cpp 文件重复解析十几遍。修改它之后整个工程会强制全量重编,所以 Online Judge 作风的程序员一般不动它。resource.h里是类似#define IDC_BUTTON_START 1000这种映射,Sniffer.rc则描述对话框长什么样、图标用哪个文件。源码包里那堆shell32.dll#274.ico、Comp2Comp.ico是给界面图标用的,和抓包逻辑无关,编译报错时别怀疑它们。
读stdafx.h时有一个小技巧:看它 include 了哪些第三方头文件,基本就能判断工程的依赖墙。Sniffer 依赖pcap.h,就意味着安装 SDK 时得配好 include 目录。我用 VS2015 编译时经常翻车在这——WinPcap 的 Include 路径没加进“VC++ 目录”,报错全是找不到pcap.h,一度以为源码是坏的。
3. 从 pcap_open_live 到协议解析:核心代码的阅读路线
3.1 捕包主流程:pcap_findalldevs 与 pcap_loop 之间的四步动作
Sniffer 的捕包主流程不复杂,但每个函数的位置有讲究。我按最可能的实现方式给你还原一遍典型的 WinPcap 调用序列:
// 伪代码示意,对应 Sniffer.cpp 中的捕获初始化过程 pcap_if_t* alldevs; char errbuf[PCAP_ERRBUF_SIZE]; // 第一步:枚举本机网卡 if (pcap_findalldevs(&alldevs, errbuf) == -1) { AfxMessageBox(_T("获取网卡列表失败")); return; } // 第二步:选中第一块可用网卡并打开 // device: 从 alldevs 遍历得到,如 "\\Device\\NPF_{...}" pcap_t* handle = pcap_open_live(device, 65536, 1, 1000, errbuf); if (handle == NULL) { AfxMessageBox(_T("打开网卡失败")); return; } // 第三步:编译并设置过滤规则(如只抓 80 端口) struct bpf_program bpf; if (pcap_compile(handle, &bpf, "tcp port 80", 1, PCAP_NETMASK_UNKNOWN) == -1) { AfxMessageBox(_T("过滤规则编译失败")); return; } pcap_setfilter(handle, &bpf); // 第四步:进入捕获循环,回调函数处理每个数据包 pcap_loop(handle, -1, (pcap_handler)packet_handler, NULL);这段代码的调用顺序是固定的,不能乱。pcap_findalldevs()返回一个链表头指针,遍历时注意协议族AF_INET的不一定是物理网卡,也可能是虚拟网卡;pcap_open_live()的第三个参数1是混杂模式开关,1000是读超时毫秒数——设短了丢包,设长了回调触发不实时。pcap_compile()的过滤字符串用的是 BPF 语法,和 tcpdump 用的完全一致,这意味着你在这份源码里学的过滤规则写法的经验能直接迁移。最后一个-1是让pcap_loop()无限循环抓包,直到外部调用pcap_breakloop()停下来。Sniffer 的停止按钮逻辑里一定会有这个调用,你可以搜pcap_breakloop确认,如果你看到的版本没有,那停止抓包就会变成“关闭窗口才结束线程”,属于代码缺陷。
3.2 iphdr.h 与协议解析:从字节流里抠出 IP、端口和协议号
抓包只是把原始字节拿回来,真正见功夫的是解析。iphdr.h里的结构体是读懂后续代码的钥匙。IP 头的基本布局是这样的:
// iphdr.h 中典型的 IP 头定义 typedef struct ip_hdr { unsigned char ver_ihl; // 高4位版本号=4,低4位首部长度(单位4字节) unsigned char tos; // 服务类型 unsigned short tot_len; // 总长度(含IP头) unsigned short id; // 标识符 unsigned short frag_off; // 片偏移(含标志位) unsigned char ttl; // 生存时间 unsigned char protocol; // 上层协议:6=TCP,17=UDP unsigned short check; // 头部校验和 unsigned int saddr; // 源IP地址 unsigned int daddr; // 目的IP地址 } IP_HEADER;注意ver_ihl这个字段很坑:高 4 位才是版本号,低 4 位是以 4 字节为单位的首部长度。如果低 4 位是5,说明 IP 头长 20 字节,数据从20偏移开始。Sniffer 源码里解析 TCP 之前一定会先算这个偏移,然后指针跳过去。TCP 头的结构类似,但多了个data offset字段,同样是 4 字节单位。也就是说,拿到一个包要解析出“源 IP + 源端口 + 目的 IP + 目的端口”,需要先基于 IP 头长度跳过一个不定长区域,再基于 TCP 头长度跳到数据区。这两个长度字段都是半字节存的,新手最容易在这边读错,然后发现端口号大得离谱——实际是偏移算错了。
// 回调函数中典型的协议判断与解析片段 void packet_handler(u_char* user, const struct pcap_pkthdr* pkthdr, const u_char* packet) { IP_HEADER* ip = (IP_HEADER*)(packet + 14); // 14 是以太网头长度 if (ip->protocol == 6) { // 跳过IP头长度,进入TCP段 int ip_len = (ip->ver_ihl & 0x0F) * 4; TCP_HEADER* tcp = (TCP_HEADER*)((u_char*)ip + ip_len); // 主机字节序转换后用 ntohs 取出端口 // 源端口 = ntohs(tcp->sport); // 目的端口 = ntohs(tcp->dport); } }这里packet + 14是另一个关键点:以太网头固定 14 字节(6 字节目的 MAC、6 字节源 MAC、2 字节类型)。如果你抓的是无线网卡,前面可能还有 RadioTap 头,代码就得多跳一段。Sniffer 源码如果针对有线以太网写的,这 14 就是写死的。遇到发不出去的高端口号,先查一下是不是把 IP 头或 TCP 头长度字段用错了。
3.3 Definition.h 与回调里的过滤逻辑:从全部流量到只抓想要的
Definition.h这个名字听起来像常数定义,其实真正的价值在回调函数的声明和全局变量。为什么过滤要做两遍?第一遍是pcap_setfilter()在内核驱动层过滤,只把符合条件的包送到应用层;第二遍是回调函数里if判断,处理 UI 上临时勾选的协议类型。Sniffer 界面上可能有一排复选框“TCP / UDP / ICMP”,它们不会直接改 BPF 字符串,而是改回调函数里的判断条件。理解这个双层过滤设计,你才明白为什么Definition.h里会有int g_nShowProtocol;这类全局标志位。
// Definition.h 中常见的全局开关与回调声明 #define MAX_PACKET_SIZE 65536 #define BMP_INFO_SIZE sizeof(BITMAPINFOHEADER) extern int g_nFilterProtocol; // 0=全部, 1=TCP, 2=UDP typedef void (__stdcall* pcap_handler)(u_char*, const struct pcap_pkthdr*, const u_char*); // SnifferDlg.cpp 里按协议显示的逻辑示意 void AddPacketToGrid(const u_char* pPacket) { IP_HEADER* pIP = (IP_HEADER*)(pPacket + 14); if (g_nFilterProtocol == 1 && pIP->protocol != 6) return; // 只显示TCP if (g_nFilterProtocol == 2 && pIP->protocol != 17) return; // 只显示UDP // 写入列表控件 }这种设计是典型的“学习型代码”风格:把常用的抓包场景全拆成开关放在界面上,让你清楚地看到修改哪个全局变量会产生什么效果。实际商用工具不会这么干,它会用动态编译 BPF 表达式来过滤。不过对学习源码而言,越直白的越好看。Sniffer 的价值就在这——它能让你一行一行把 UI 操作映射到 API 调用上。
4. 编译与运行避坑指南:Sniffer 源码实战的五个翻车现场
4.1 WinPcap 驱动安装失败:代码没错,环境不对
现象:源码编译通过,一运行就弹“打开网卡失败”或者直接崩溃。原因:开发机只装了 WinPcap 开发包(SDK),没装运行时的驱动服务npf.sys。很多网上的压缩包只带 Include 和 Lib,没带驱动安装器。解决:去装一个 WinPcap 4.1.3 运行时,或者装 npcap 并勾选“WinPcap API 兼容模式”。装完在C:\Windows\System32\drivers下能看到 npf.sys 才算成功。我在虚拟机上踩过这个坑,折腾半天差点怀疑人生,最后发现只是驱动服务没启动。
4.2 混杂模式没生效:只能抓到进本机的包
现象:明明连的是交换机,却只能看到发给自己这台机器的流量,隔壁主机的包全不见。原因:pcap_open_live()的 promisc 参数传了 0,或者交换机上开了端口隔离。反直觉的是,WinPcap 开启混杂模式后,无线网卡常常抓不到别人家的帧——这是无线网卡本身的特性,硬件只把发给自己的帧交给驱动。解决:确认参数是1;无线环境换有线网卡或换 AP 所在网段测试。这不是源码 bug,是物理解释跑不掉。
4.3 VS 版本与预编译头不兼容:执行了不安全的操作
现象:用 VS2019 打开.vcproj,编译报大量 C4996 错误,或提示“这是较早版本的工程文件,需要转换”。原因:.vcproj是 VC6/VS2005 时代的工程格式,高版本 VS 强制升级后,字符集和 CRT 函数名都变了——比如strcpy会被要求换成strcpy_s。解决:把工程的“字符集”改成“使用多字节字符集”,把“SDL 检查”关闭,预处理定义里加_CRT_SECURE_NO_WARNINGS。这几步能干掉八成报错,剩下的无非是for(int i...)循环变量作用域问题。
4.4 界面卡死:pcap_loop 阻塞了消息循环
现象:点击开始抓包后窗口立刻变白、鼠标转圈,关闭按钮点不动。原因:pcap_loop()是阻塞调用,它在 UI 线程里执行时,Windows 消息循环收不到消息,界面自然就僵住了。这是网络编程里最经典的问题——长任务必须扔到独立线程去跑。解决:看代码里有没有AfxBeginThread(CaptureThread, this),如果没有,自己补一个工作线程,回调里用PostMessage(WM_CAPTURE_NOTIFY, ...)把数据显示到界面。这个改动本身就是最好的 C++ 多线程练手作业。
4.5 过滤规则写了等于没写:BPF 语法和 UI 输入没同步
现象:在界面上输入“tcp port 80”,点开始,结果什么包都没有,或者还是会显示其他端口的包。原因:pcap_compile()失败被忽略,或者在回调函数里又对端口号做了二次判断,导致原本合法的包被过滤掉。解决:pcap_compile()后必须检查返回值,失败时用pcap_geterr(handle)打印错误字符串;回调里的二次过滤判断条件反而要放宽,别在 IP 层判断 TCP 端口——那是 TCP 层的东西。
5. 把捕获线程和 MFC 消息循环拧在一起:一个必做的进阶改动
上面说过 Sniffer 的暂停按钮可能是摆设,现在给出具体改法。既然源码的目标是学原理,那不如把它升级成一个“捕包线程 + 消息通知 + 列表刷新”的标准结构,这套模式以后做任何实时数据采集工具都能复用。核心思路:捕获线程只负责把原始包解析成显示用的结构体,然后通过PostMessage丢给对话框线程,界面收到消息再往列表控件里塞数据。
// 显示用的轻量结构体,放在 Definition.h 中 typedef struct _DISPLAY_PACKET { CString src_ip; CString dst_ip; USHORT src_port; USHORT dst_port; BYTE protocol; DWORD timestamp; } DISPLAY_PACKET; // 捕获线程函数 UINT CaptureThread(LPVOID pParam) { CMainDlg* pDlg = (CMainDlg*)pParam; pcap_loop(handle, -1, (pcap_handler)packet_handler, NULL); return 0; } // 回调中把数据复制出来,发给界面线程 void packet_handler(u_char* user, const struct pcap_pkthdr* pkthdr, const u_char* packet) { DISPLAY_PACKET dsp; // 省略:从 packet 中解析 IP、TCP 并填写 dsp // 注意:不能直接把 packet 指针传出去,它指向的内存在回调返回后无效 ::PostMessage(g_hMainWnd, WM_CAPTURE_PACKET, 0, (LPARAM)&dsp); // 这里要深拷贝 }这段代码里有几个值得细看的点。第一,packet_handler里虽然能看到DISPLAY_PACKET,但PostMessage传的是地址,如果下一个包立刻覆盖了这块栈内存,界面收到的数据就是错的,所以一定要在回调里复制内容而不是复制指针。第二,WM_CAPTURE_PACKET是自定义消息,你得在对话框类的头文件里定义#define WM_CAPTURE_PACKET WM_USER+100,然后在消息映射里加上对应处理函数。第三,PostMessage是异步的,不会等界面刷新完就返回,这样即使界面列表控件刷新慢,捕包线程也不会被拖住,数据包丢不丢是另一个话题,至少 UI 不会再卡死。
把这份源码按上面的思路改完一遍,你对 Windows 消息循环、线程同步、网络字节序的理解会是直接背八股文的三倍。我从那次之后,凡是写带实时数据流的 MFC/Win32 程序,都强制走一遍“工作线程 + PostMessage + 自定义消息”的套路,再没遇到界面假死的问题。一句经验之谈:抓包工具最首要的需求不是优雅,是别把系统拖垮。这份 Sniffer 源码是个很好的起点,动手把上面五个坑踩一遍再填平,收获比读十篇教程都实在。希望帮到你。
本文还有配套的精品资源,点击获取