
简介针对网络协议分析与抓包学习场景该原创工程基于MFC与WinpCap实现了一款界面友好的网络嗅探器适合C开发者、网络安全初学者及需要本地抓包验证的工程师。代码支持IPv4、IPv6、ARP、ICMP、TCP、UDP、HTTP等常见协议解析可直接编译运行并逐层查看数据包结构作者提供的预览效果可直观了解工具的操作界面与解析方式。资源包共22个文件以7个头文件和4个源文件为主体另含界面图标、工程配置sln/vcproj、资源脚本及说明文档压缩包整体仅1.92MB结构紧凑、模块边界清晰便于阅读和二次修改。目前已吸引5398人浏览学习。借助完整工程源码与配套界面资源读者可快速掌握WinpCap抓包初始化、协议字段解析、列表控件展示等关键环节也可作为毕业设计、课程实验或相关网络监测工具的原型参考。1. MFC WinPcap 网络嗅探器一个能跑的抓包界面而不是命令行玩具MFC 界面配 WinPcap 抓包驱动这套组合在网络嗅探器sniffer工具里算是最容易闭环的方案之一。界面用 Visual Studio 里的 MFC 对话框直接画抓包能力交给 WinPcap 的内核驱动你需要写的实际只有三件事枚举网卡、跑抓包循环、把原始字节解析成协议字段。这份资源给的正是这个最小骨架不是那种只有两三个函数的半成品而是一份能编译、能选网卡、能抓包、能列表刷新的完整工程。它解决两类需求一是课程设计、毕业设计里要交出「有界面、有抓包、有解析」的完整 demo二是公司内部想做一个只抓指定流量、能落盘、能统计的轻量 sniffer又不想背 Wireshark extcap 插件那套开发成本。适合已经把 C 基本语法看完、想拿真实网络编程项目练手的人也适合被 WinPcap 安装和 64 位链接折磨过、想直接看一份能过的工程的人。2. 为什么是 WinPcap MFC抓包选型与工程初始化2.1 在 Windows 上抓包的三种方式里为什么选中 WinPcap普通 socket 只能收到发给本机的数据包广播和组播看运气局域网里别的机器互发流量根本拿不到。Windows 的 SOCK_RAW 更尴尬虽然能构造原始报文但收包方向仍然受协议栈约束而且从 Vista 开始普通用户基本用不了。真正能稳定抓到整条链路上流量的方案WinPcap 是历史最久、资料最多的一种。它的工作模型和 socket 不一样。WinPcap 通过内核驱动 npf.sys 在网卡驱动层复制一份收到的帧数据不经过 TCP/IP 协议栈所以那些被系统丢弃的、发给别的 MAC 的、甚至畸形包应用层都能看到。代价也很明确抓到的不是「HTTP 请求」「TCP 流」这种高层数据而是最原始的以太网帧。IP 分片不会帮你重组TCP 字节流不会帮你还原成 socket 数据所有解析都得自己按协议栈逐层剥离。这一点必须想清楚否则后面八成会把链路层帧错当成应用层数据。我一般在项目里选型会这么判断如果只是临时看流量直接 Wireshark如果要把抓包能力嵌进自研工具、还要和 MFC 界面做联动WinPcap或它的继任者 Npcap是唯一划算的路。MFC 负责界面和线程调度WinPcap 负责底层到链路层数据的获取分工非常干净。2.2 VS 工程接入 WinPcap包含目录、宏与库依赖把 WpdPack 解压到工程目录后要做四件事配置 include 路径、配置 lib 路径、加预处理宏、填附加依赖库。WpdPack 是 WinPcap 的开发者包安装版 WinPcap 不包含 SDK需要单独下载这是第一处容易绕远的地方。工程配置里我习惯这样做属性页 → C/C → 常规 → 附加包含目录填入WpdPack\Include链接器 → 常规 → 附加库目录填入WpdPack\LibC/C → 预处理器 → 预处理器定义追加WPCAP和HAVE_REMOTE链接器 → 输入 → 附加依赖项追加三个库库文件作用wpcap.libWinPcap 抓包 API 的导入库pcap_ 开头的函数都在这Packet.lib底层驱动接口WinPcap 特有的扩展功能用到ws2_32.libWinsock 基础库ntohs、inet_ntoa这些字节序转换函数需要它有一个版本上的老坑必须提前说WinPcap 官方 WpdPack 的 Lib 目录里只有 32 位库VS 默认的 x64 配置会直接链接失败所以刚上手建议把活动解决方案平台设成 x86。想编 64 位得换 Npcap 的 SDK接口不变但库文件是 64 位的这个后面避坑章还会展开。配置完先不要写业务逻辑用一个最小文件验证链路通没通#include pcap.h #pragma comment(lib, wpcap.lib) #pragma comment(lib, ws2_32.lib) #pragma comment(lib, Packet.lib) char szErrBuf[PCAP_ERRBUF_SIZE] { 0 }; pcap_if_t* pAllDevs NULL; int CheckWinPcap() { if (pcap_findalldevs(pAllDevs, szErrBuf) -1) { // 枚举网卡失败驱动或 SDK 配置有问题 return -1; } return 0; }这段代码的作用不是抓包而是确认头文件能找到、三个库都链上、驱动接口能正常调用。PCAP_ERRBUF_SIZE是 WinPcap 定义的错误缓冲区大小所有可能失败的 pcap 函数都要用这个宏来声明错误缓冲。第一次能编译过去说明工程接入已经成功后面可以放心加功能。3. 设备枚举与抓包线程把选网卡和收数据拆成两件事3.1 枚举网卡并填充到下拉框name 与 description 的区别抓包前必须先让用户选一张网卡。WinPcap 提供pcap_findalldevs枚举所有接口返回一个pcap_if_t链表每个节点包含name和description两个关键字段。name是驱动内部用的设备名类似\Device\NPF_{GUID}最终传给pcap_open_live的是它description是给人看的人话类似「Realtek PCIe GbE Family Controller」。这里有一个非常容易踩的坑有人直接把description存进下拉框的 ItemData然后把pcap_if_t*指针也存进去最后调用pcap_freealldevs释放链表指针就悬空了。正确做法是把name复制进一个CStringArray下拉框存数组下标不直接存裸指针pcap_if_t* pAllDevs NULL; char szErrBuf[PCAP_ERRBUF_SIZE] { 0 }; if (pcap_findalldevs(pAllDevs, szErrBuf) -1) { AfxMessageBox(_T(枚举网卡失败请检查 WinPcap 驱动是否安装)); return; } m_arrDeviceNames.RemoveAll(); m_cboDevice.ResetContent(); for (pcap_if_t* pDev pAllDevs; pDev ! NULL; pDev pDev-next) { if (pDev-name NULL) continue; m_arrDeviceNames.Add(CString(pDev-name)); CString strText pDev-description; if (strText.IsEmpty()) strText CString(pDev-name); int nIdx m_cboDevice.AddString(strText); m_cboDevice.SetItemData(nIdx, m_arrDeviceNames.GetSize() - 1); } pcap_freealldevs(pAllDevs);逻辑说明m_arrDeviceNames是对话框类的成员变量作用是把设备的驱动名保存到程序生命周期结束。SetItemData存的是CStringArray的下标这样下拉框选中项能直接反查出设备名又不会因为释放链表而悬空。遍历结束后立刻pcap_freealldevs干净利落。还有一个显示上的细节Unicode 工程里description是从char*转成CString的中文网卡描述在某些系统语言下会乱码抓包功能本身不受影响但界面体验会差。我一般处理成优先显示英文描述实在没有才显示name。3.2 抓包线程的启动、循环与退出pcap_next_ex 的超时陷阱抓包必须放线程里跑因为pcap_next_ex在没有包到达时会阻塞等待放在 UI 线程会让界面直接假死。MFC 里用AfxBeginThread起一个工作线程是最省事的方案线程函数要写成静态函数或全局函数然后通过this指针访问对话框成员。打开设备的参数是第二个容易翻车的点CT2A asciiDevName(strSelectedDevName); // Unicode 转 ANSI m_hPcap pcap_open_live( asciiDevName, // 设备名必须是 char* 65536, // snaplen单包最大捕获长度 1, // promisc 混杂模式1 开启 1000, // to_ms 超时毫秒数每 1 秒返回一次 szErrBuf); if (m_hPcap NULL) { AfxMessageBox(CString(_T(打开设备失败: )) CString(szErrBuf)); return; }snaplen设 65536 是为了保证大包不被截断抓完整包做解析一定要给足。promisc设 1 开启混杂模式否则只能收到发给本机的包嗅探器就名存实亡。to_ms设 1000 是为了让pcap_next_ex在没有流量时也能每秒醒一次这个超时值直接决定线程退出的响应速度。线程主体是一个标准的读取循环UINT WINAPI CSnifferDlg::SniffThreadProc(LPVOID pParam) { CSnifferDlg* pDlg (CSnifferDlg*)pParam; pcap_pkthdr* pHeader NULL; const u_char* pData NULL; int nRet 0; while (pDlg-m_bStop FALSE) { nRet pcap_next_ex(pDlg-m_hPcap, pHeader, pData); if (nRet 1) { // 成功抓到一包交给解析函数 pDlg-OnPacketArrived(pHeader, pData); } else if (nRet 0) { // 超时返回不是错误继续循环 continue; } else { // 驱动错误或设备被关闭 break; } } return 0; }pcap_next_ex的返回值是新手最容易误解的地方返回 1 才代表抓到包返回 0 表示在to_ms超时时间内没有包这是正常情况不是错误返回 -1 才是驱动层面的错误。很多人把nRet 0当错误退出线程结果网络一旦空闲就自己停了。停止抓包也有标准顺序顺序错了就会出第五章里那个崩溃问题m_bStop TRUE; pcap_breakloop(m_hPcap); WaitForSingleObject(m_hThread, 3000); pcap_close(m_hPcap); m_hPcap NULL;pcap_breakloop的作用是从另一个线程打断阻塞中的pcap_next_ex让它立刻返回。先置m_bStop再 breakloop再等待线程退出最后才关句柄这个顺序不能乱。m_hThread是AfxBeginThread返回的 CWinThread 指针WaitForSingleObject超时给 3 秒已经足够如果 3 秒还没退出多半是m_bStop循环条件没生效或 breakloop 没调用。4. 数据包解析与界面刷新从原始字节到协议字段4.1 手工拆包偏移量优先拒绝结构体位域WinPcap 给到的是一段连续的字节流从以太网帧头开始。解析思路非常直接用偏移量逐层读字段不要用 C 结构体位域去映射网络头。位域在 C 里的内存布局和字节序都依赖编译器实现网络字节序又是大端x86 是小端两套东西凑一起纯属给自己找麻烦。我自己是坚持「先按偏移量读出几字节再手动拼数值」。以太网头固定 14 字节目的 MAC 6 字节、源 MAC 6 字节、类型 2 字节。类型0x0800是 IPv4。IP 头第一个字节的低 4 位是 IHL单位是 4 字节常见值是 5表示 20 字节头长。协议字段在第 9 字节TCP 是 6UDP 是 17。void CSnifferDlg::OnPacketArrived(const pcap_pkthdr* pHeader, const u_char* pData) { if (pHeader-len 34) return; // 太短的包直接丢弃 WORD wEtherType (pData[12] 8) | pData[13]; if (wEtherType ! 0x0800) return; // 非 IPv4 暂时不解析 BYTE bVerIHL pData[14]; int nIHL (bVerIHL 0x0F) * 4; BYTE bProtocol pData[23]; BYTE bTtl pData[22]; DWORD dwSrcIP ((DWORD)pData[26] 24) | ((DWORD)pData[27] 16) | ((DWORD)pData[28] 8) | (DWORD)pData[29]; DWORD dwDstIP ((DWORD)pData[30] 24) | ((DWORD)pData[31] 16) | ((DWORD)pData[32] 8) | (DWORD)pData[33]; WORD wSrcPort 0; WORD wDstPort 0; if (bProtocol 6 || bProtocol 17) { int nTcpOffset 14 nIHL; wSrcPort (pData[nTcpOffset] 8) | pData[nTcpOffset 1]; wDstPort (pData[nTcpOffset 2] 8) | pData[nTcpOffset 3]; } // 组装成一条显示记录 DWORD dwInfo[4] { bProtocol, dwSrcIP, dwDstIP, 0 }; // 这里可以把字段 PostMessage 到主线程见下一节 }代码里所有多字节数值都用手动移位拼接而不是*(WORD*)(pData12)原因有两个一是 x86 上非对齐访问可能触发性能惩罚二是直接读出来的字节序是反的移位反而最直观。IP 地址我是直接存成了 DWORD显示时再自己拆成点分十进制没有调用inet_ntoa因为那个函数内部用静态缓冲多线程调用会串数据。这套解析只覆盖 IPv4 TCP/UDP已经能应付大多数 sniffer 演示。要扩展 ARP、ICMP、IPv6就在wEtherType和bProtocol两个分支处继续加整个解析框架不需要动。4.2 跨线程刷新列表PostMessage 与批量插入抓包线程把解析结果直接往 MFC 控件里插是新手最喜欢犯的错。MFC 的窗口对象不是线程安全的工作线程里调用m_lstPacket.InsertItem轻则界面刷新异常重则直接崩溃。正确做法是抓包线程只负责解析和打包数据通过PostMessage把数据指针丢回 UI 线程由 UI 线程统一刷新控件。自定义消息需要先定义消息 ID并添加到消息映射#define WM_PACKET_READY (WM_USER 100) BEGIN_MESSAGE_MAP(CSnifferDlg, CDialogEx) ON_MESSAGE(WM_PACKET_READY, CSnifferDlg::OnPacketReady) END_MESSAGE_MAP()抓包线程负责填充一个结构体并发送typedef struct _PACKET_INFO_ { SYSTEMTIME stTime; // 捕获时间 BYTE bProtocol; // 6TCP, 17UDP DWORD dwSrcIP; // 源 IP DWORD dwDstIP; // 目的 IP WORD wSrcPort; // 源端口 WORD wDstPort; // 目的端口 WORD wLen; // 包长度 } PACKET_INFO, *PPACKET_INFO; void CSnifferDlg::OnPacketArrived(const pcap_pkthdr* pHeader, const u_char* pData) { PACKET_INFO* pInfo new PACKET_INFO; pInfo-wLen (WORD)pHeader-len; GetLocalTime(pInfo-stTime); // ... 上一节解析出的字段填进 pInfo ... PostMessage(WM_PACKET_READY, 0, (LPARAM)pInfo); }UI 线程收到消息后把结构体内容填进列表然后释放内存LRESULT CSnifferDlg::OnPacketReady(WPARAM wParam, LPARAM lParam) { PPACKET_INFO pInfo (PPACKET_INFO)lParam; if (pInfo NULL) return 0; int nRow m_lstPacket.GetItemCount(); m_lstPacket.InsertItem(nRow, _T()); CString strTime, strIp, strPort; strTime.Format(_T(%02d:%02d:%02d), pInfo-stTime.wHour, pInfo-stTime.wMinute, pInfo-stTime.wSecond); strIp.Format(_T(%u.%u.%u.%u), (pInfo-dwSrcIP 24) 0xFF, (pInfo-dwSrcIP 16) 0xFF, (pInfo-dwSrcIP 8) 0xFF, pInfo-dwSrcIP 0xFF); m_lstPacket.SetItemText(nRow, 0, strTime); m_lstPacket.SetItemText(nRow, 1, strIp); // ... 其余列同理 ... delete pInfo; return 0; }这里new出来的结构体通过消息传递接收方负责delete谁接收谁释放避免泄漏。PostMessage不会阻塞发送方即使 UI 正在重绘抓包线程也不会停下来等。有一个性能边界要说透每抓一包就 PostMessage 一次在百兆局域网演示没问题但流量稍大就会出现消息队列堆积界面刷新跟不上。常见做法是积攒 20~50 条解析结果凑够一批再发一个WM_PACKET_BATCH消息接收端循环插列表。批量插入时配合m_lstPacket.SetRedraw(FALSE)先暂停重绘插完再SetRedraw(TRUE)列表不会一条条闪。5. 避坑手册WinPcap 安装、64 位编译与 UI 崩溃的五个常见问题5.1 安装与链接类问题现象一WinPcap 4.1.3 在 Win10、Win11 上安装到最后一步提示驱动安装失败设备管理器里能看到 npf 服务但启动报错。原因WinPcap 的内核驱动签名比较老新版 Windows 默认不允许加载这种未更新的驱动和抓包代码本身没关系换任何程序来都跑不起来。解决第一步右键安装包 → 属性 → 兼容性 → 勾选「以兼容模式运行」系统选 Windows 7同时勾选「以管理员身份运行」。第二步如果还是不行直接放弃 WinPcap装 Npcap 并在安装时勾选「WinPcap API Compatibility Mode」。Npcap 的 wpcap.lib 接口和 WinPcap 完全一致原工程代码一行不用改这个方案也是我现在推荐的首选因为 WinPcap 项目本身已经停止维护。现象二编译报error LNK2019提示无法解析pcap_findalldevs等外部符号但头文件明明已经包含了。原因两类情况。一是三个库没有全部加到附加依赖项只加了 wpcap.lib漏了 Packet.lib 或 ws2_32.lib二是 VS 的活动解决方案平台是 x64而 WinPcap 官方 WpdPack 的 Lib 目录没有 64 位版本的 .lib链接器找不到符号自然报错。解决先确认附加依赖项完整再确认平台是 x86。如果确实要编 64 位装 Npcap SDK它会同时提供 x64 的 lib 和 DLL接口与代码不变。顺带说一个玄学有时候配置都对但 Release 和 Debug 一个能编一个不能编去检查两个配置的附加库目录是不是都填了VS 的 Debug 和 Release 配置是分开保存的。5.2 运行与停止类问题现象三Unicode 工程下pcap_compile设置过滤表达式时一直返回 -1查看错误信息是乱码但过滤表达式看起来没问题。原因MFC 默认是 Unicode 字符集CString实际是CStringW里面是宽字符。pcap_compile要求的是char*单字节字符串直接把CString强转成LPCSTR编译器只截断指针底层拿到的是宽字符的字节流驱动自然解析不了。解决用CT2A做一次字符集转换把宽字符转成 ANSI 字节串再传给 pcap 接口CString strFilter _T(tcp port 80); CT2A asciiFilter(strFilter); pcap_compile(m_hPcap, stFCode, asciiFilter, 1, PCAP_NETMASK_UNKNOWN);这个坑同样出现在pcap_open_live的设备名参数上凡是要char*的地方都要先CT2A转换。不要用T2A它在函数退出时释放缓冲区返回的指针就悬空了。现象四抓包功能正常但列表刷几秒后程序崩溃Debug 下偶尔弹「Debug Assertion Failed」Release 下直接闪退。原因抓包线程直接调用了CListCtrl::InsertItem或SetDlgItemText跨线程操作了 MFC 窗口对象。MFC 的控件封装不是线程安全的底层窗口句柄在消息循环线程手里工作线程从外部改它时机一冲突就崩。解决所有控件操作必须回到 UI 线程执行。用第四章的PostMessage自定义消息方案抓包线程只生产数据UI 线程消费数据并刷新列表。这是标准做法不是临时规避别图省事在抓包线程里加AfxMessageBox调试那个都会出问题。现象五点击停止抓包后界面卡死或程序退出时崩溃调试断点停在pcap_next_ex内部。原因pcap_next_ex在没有包到达时阻塞在内核等待事件上主线程只设置了m_bStop标志或直接调用了pcap_close阻塞中的调用没有被唤醒线程永远退不出来最后对话框销毁时线程还在用已经关掉的句柄。解决停止顺序必须是「置标志 →pcap_breakloop唤醒 →WaitForSingleObject等线程退出 →pcap_close」。pcap_breakloop是专门用来打断阻塞读取的它会设置一个内部标志让pcap_next_ex从内核等待中返回。如果设了to_ms1000理论上 1 秒内也会醒但显式 breakloop 才保证 100% 立刻响应不要靠超时时间撞运气。6. 把嗅探器升级成分析工具过滤表达式、pcap 落盘与离线回放6.1 用过滤表达式把抓包范围收窄默认抓包会把网卡上所有流量都收进来调试时列表跑得飞快。WinPcap 的 BPF 过滤语法和 Wireshark 的显示过滤器接近但它是内核级过滤不符合条件的包根本不会交到应用层struct bpf_program stFCode { 0 }; CString strFilter _T(tcp port 443 or host 192.168.1.1); CT2A asciiFilter(strFilter); if (pcap_compile(m_hPcap, stFCode, asciiFilter, 1, PCAP_NETMASK_UNKNOWN) 0) { pcap_setfilter(m_hPcap, stFCode); pcap_freecode(stFCode); }pcap_compile的第三个参数是过滤表达式第四个参数是优化开关填 1 启用第五个是掩码不确定子网时用PCAP_NETMASK_UNKNOWN。表达式语法举几个常用的只看某台主机host 192.168.1.10只看某个网段net 192.168.1.0/24组合条件用and、or不用的包在驱动层就被丢掉了压力比应用层过滤小一个数量级。6.2 让包落盘并支持离线回放演示和调试的时候把原始包写进 pcap 文件特别有用抓完用 Wireshark 再分析一遍比自己写的解析代码看得全。WinPcap 的文件写入接口很直白// 打开抓包设备后打开一个 dump 文件 pcap_dumper_t* pDumper pcap_dump_open(m_hPcap, sniff.pcap); // 在 OnPacketArrived 里每抓到一包就写入 pcap_dump((u_char*)pDumper, pHeader, pData); // 停止抓包时关闭 pcap_dump_close(pDumper);注意pcap_dump_open第一个参数传的是pcap_t*它要从里面读取链路类型并写进文件头。pcap_dump的写入是一次一包的同步写入流量大时会影响抓包线程我一般会在批量 PostMessage 的同时把原始包也写进 dump把性能和持久化两边都照顾到。配套的离线分析套路是pcap_open_offline打开 pcap 文件后后续的pcap_next_ex循环和在线抓包完全一致pcap_t* pFile pcap_open_offline(sniff.pcap, szErrBuf); // 后续 pcap_next_ex 循环代码完全复用不用改解析逻辑这样调试时可以先落盘再慢慢回放解析同一套解析函数能同时处理在线和离线数据源这是不少 sniffer 教程没讲透的设计点。从那以后我每次改这个抓包工具都会强制走一遍停止流程置标志、breakloop、等线程退出、再关句柄这套顺序已经成了肌肉记忆。排序、刷新、过滤这三块骨架搭好之后剩下的协议解析深度和统计展示都是往里填内容的事这份工程拿回去可以直接在这个框架上改希望帮到你。本文还有配套的精品资源点击获取