☰
轻量级Raw Socket流量监控系统:WinXP兼容、无驱动、单文件抓包
2026/11/5 4:41:53 网站建设 项目流程

简介:这是一套基于Visual C++开发的Windows平台网络流量监控系统源码,面向C++中级开发者、网络编程学习者及网络安全实践人员,用于理解数据包捕获、协议解析、实时流量统计与MFC界面集成等核心能力。资源共24个文件,含8个头文件(如Globals.h、MFNetTraffic.h)定义全局结构与网络模块接口,5个CPP源文件(如mftrafficbutton.cpp、NetTrafficButtonDlg.cpp)实现主逻辑与对话框功能,以及ICO图标、RC资源、DSP/DSP工程配置等配套文件,完整支撑从编译到运行的全流程,压缩包仅67KB,轻量易上手。已有602人学习下载,适合通过真实项目掌握WinPcap/Libpcap底层调用、多线程流量采集、连接级带宽计算及MFC图表化展示等关键技术点,代码目录结构清晰,模块职责分明,含Debug调试目录与标准StdAfx预编译支持,便于快速编译验证与二次扩展。

1. 这不是Wireshark的简化版:一个能嵌进工业网关、跑在WinXP SP3上、靠Raw Socket硬抓包的轻量级流量监控系统

某产线边缘设备突然频繁掉线,运维只看到交换机端口流量突增,但查不到是哪个进程在发疯。他们试过用现成工具——Wireshark太重,没GUI权限;NetLimiter要装服务驱动,客户环境禁用;Windows自带的Resource Monitor又只能看本机出向。最后翻出这个VC++老项目:编译后仅286KB的单文件exe,双击即启,不依赖任何运行库,连msvcr71.dll都不需要——它直接调用ws2_32.lib里的WSAIoctl和WSARecvFrom,用AF_INET + SOCK_RAW + IPPROTO_IP模式在网卡混杂模式下收包。它不解析HTTP/FTP应用层,只统计每IP每端口的字节数、连接数、SYN洪峰速率,数据存CSV,支持按阈值自动截断并触发回调函数。适合嵌入老旧工控机、做网络准入探针、或当自研IDS的底层流量采集模块。如果你正被“不能装第三方驱动”“必须兼容WinXP”“要C++裸写不许用MFC”三条红线捆住手脚,这个项目就是你翻盘的底牌。


2. 编译与运行:从VC6.0工程到VS2019兼容的完整链路

2.1 工程结构还原:为什么它能在VC6.0里编译成功?

解压后目录极简:

/NetworkMonitor/ ├── NetworkMonitor.dsp ← VC6.0工程文件(关键!) ├── NetworkMonitor.dsw ├── Main.cpp ← 主循环+消息泵 ├── PacketCapture.cpp ← 核心抓包逻辑 ├── FlowStat.cpp ← 流量聚合与阈值判断 ├── ExportCSV.cpp ← CSV写入(无第三方库,纯FILE*操作) └── Resource.h

这个结构暴露了它的设计哲学:零框架依赖。没有.rc资源脚本,界面用CreateWindow硬建;没有CWinApp派生类,WinMain里直接WSAStartup→socket→ioctlsocket(SIO_RCVALL)→while(1){recvfrom}。NetworkMonitor.dsp里明确写着:

# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /YX /c # ADD CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /YX /c

/MT表示静态链接CRT,所以打包后无需分发msvcrXX.dll;/GX启用异常处理(但实际代码里几乎没用try-catch,全靠if (SOCKET_ERROR == ret)判断);/O2优化体积而非速度——这解释了为何最终EXE才286KB。

提示:VC6.0默认生成__declspec(dllimport)符号,而此项目所有API调用都显式声明为extern "C",例如int __stdcall WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);。这是为了绕过VC6.0对ws2_32.lib的隐式链接规则,确保在无.lib导入库时仍能链接成功。

2.2 VS2019迁移实操:三步解决“无法打开输入文件‘wsock32.lib’”

VS2019新建空Win32项目后,需手动补全四件事:

  1. 替换头文件包含顺序
    原Main.cpp开头是:

    #include <windows.h> #include <winsock.h> // ❌ VS2019已废弃

    改为:

    #include <winsock2.h> // ✅ 必须在windows.h之后 #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") // 显式链接
  2. 关闭SDL检查(否则strcpy报错)
    项目属性 → C/C++ → 常规 → SDL检查 → 设为“否”

  3. 修正ioctlsocket参数类型
    原代码:

    DWORD dwBuffer = 1; ioctlsocket(sock, SIO_RCVALL, &dwBuffer); // ❌ VC6.0接受DWORD*

    VS2019要求:

    u_long ulBuffer = 1; // ✅ u_long是WSA定义的标准类型 ioctlsocket(sock, SIO_RCVALL, &ulBuffer);

编译后生成NetworkMonitor.exe,右键以管理员身份运行——这是硬性要求,因为SIO_RCVALL需要SE_NETWORK_MANAGE权限。

2.3 首次运行必做的三件事

  1. 确认网卡索引
    程序启动后弹窗显示“Select Adapter: 0-2”,这不是随便选的。执行以下命令查真实索引:

    netsh interface ipv4 show interfaces

    输出中Idx列对应程序里的数字。若选错,recvfrom会立即返回WSAENETDOWN。

  2. 关闭防火墙临时规则
    Windows Defender防火墙可能拦截原始套接字。临时执行:

    netsh advfirewall set allprofiles state off

    (生产环境请改用netsh advfirewall firewall add rule...放行特定端口)

  3. 验证抓包有效性
    启动后立刻开cmd执行:

    ping -n 5 192.168.1.1

    回到监控窗口,应看到ICMP协议行出现Bytes: 320(5×64字节),且Packets列+5。若为0,说明混杂模式未生效——检查是否以管理员运行,或网卡是否为虚拟网卡(VMware/VirtualBox虚拟网卡不支持SIO_RCVALL)。


3. 核心模块拆解:Raw Socket抓包、流状态机、阈值触发的三位一体实现

3.1 PacketCapture.cpp:如何用127行代码完成稳定抓包

核心函数CaptureLoop()结构如下:

void CaptureLoop(SOCKET sock, HWND hList) { char buffer[65536]; // MTU上限,避免分片 sockaddr_in addr; int addrLen = sizeof(addr); while (g_bRunning) { int nRet = recvfrom(sock, buffer, sizeof(buffer), 0, (sockaddr*)&addr, &addrLen); if (nRet == SOCKET_ERROR) { int err = WSAGetLastError(); if (err == WSAEMSGSIZE || err == WSAECONNRESET) continue; // 忽略分片/重置 Sleep(10); // 防止CPU占满 continue; } // 解析IP头(跳过以太网帧,因SIO_RCVALL返回的是IP层起始) IPHeader* ip = (IPHeader*)buffer; if (ip->ver_ihl != 0x45) continue; // 只处理IPv4+无选项 // 提取五元组(源IP、目的IP、协议、源端口、目的端口) FlowKey key; key.srcIP = ntohl(ip->srcIP); key.dstIP = ntohl(ip->dstIP); key.protocol = ip->protocol; key.srcPort = 0; key.dstPort = 0; // 若是TCP/UDP,继续解析传输层头 if (ip->protocol == IPPROTO_TCP || ip->protocol == IPPROTO_UDP) { TCPHeader* tcp = (TCPHeader*)(buffer + (ip->ver_ihl & 0x0F) * 4); key.srcPort = ntohs(tcp->srcPort); key.dstPort = ntohs(tcp->dstPort); } UpdateFlowStat(&key, nRet); // 交给统计模块 } }

关键点说明:

  • buffer[65536]大小是血泪经验——太小丢包(尤其UDP大包),太大导致recvfrom阻塞时间不可控;
  • ip->ver_ihl & 0x0F提取IP头长度(单位4字节),因为IP选项字段存在时头长>20字节;
  • UpdateFlowStat()不在此文件实现,体现模块解耦——抓包只负责“喂数据”,不碰业务逻辑。

3.2 FlowStat.cpp:基于哈希表的实时流聚合算法

FlowStat类用std::map<FlowKey, FlowRecord>存储,FlowKey重载operator<:

struct FlowKey { DWORD srcIP, dstIP; BYTE protocol; WORD srcPort, dstPort; bool operator<(const FlowKey& rhs) const { if (srcIP != rhs.srcIP) return srcIP < rhs.srcIP; if (dstIP != rhs.dstIP) return dstIP < rhs.dstIP; if (protocol != rhs.protocol) return protocol < rhs.protocol; if (srcPort != rhs.srcPort) return srcPort < rhs.srcPort; return dstPort < rhs.dstPort; } };

FlowRecord结构体精简到极致:

struct FlowRecord { DWORD totalBytes; // 累计字节数 DWORD packetCount; // 累计包数 DWORD lastActive; // GetTickCount()时间戳,用于超时清理 DWORD synCount; // 仅TCP:SYN包计数(检测SYN Flood) DWORD lastSynTime; // 上次SYN时间戳 };

UpdateFlowStat()逻辑:

void UpdateFlowStat(FlowKey* key, int bytes) { auto it = g_flowMap.find(*key); if (it == g_flowMap.end()) { g_flowMap[*key] = {bytes, 1, GetTickCount(), 0, 0}; } else { it->second.totalBytes += bytes; it->second.packetCount++; it->second.lastActive = GetTickCount(); // TCP SYN检测:仅当flags & 0x02为真时计数 if (key->protocol == IPPROTO_TCP && IsSynPacket(buffer)) { it->second.synCount++; it->second.lastSynTime = GetTickCount(); } } }

注意:IsSynPacket()需从buffer中定位TCP flags字段,位置=IP头长+20(TCP固定头长),取第13字节(flags所在偏移)。此处省略具体计算,但务必注意:buffer传入时是IP层起始,非以太网帧。

3.3 阈值触发机制:不是简单告警,而是可编程的回调链

FlowStat.cpp末尾定义:

typedef void (*AlertCallback)(const FlowKey*, const FlowRecord*); AlertCallback g_pAlertCB = nullptr; void SetAlertCallback(AlertCallback cb) { g_pAlertCB = cb; } // 在UpdateFlowStat内部,当满足条件时触发 if (it->second.synCount > 100 && GetTickCount() - it->second.lastSynTime < 1000) { // 1秒内超100个SYN if (g_pAlertCB) g_pAlertCB(&it->first, &it->second); }

这意味着你可以写自己的DLL,在DllMain里调用SetAlertCallback(MyHandler),实现:

  • 写入Syslog服务器
  • 调用netsh advfirewall firewall add rule...动态封IP
  • 发送SNMP trap
  • 甚至调用TerminateProcess杀掉本机可疑进程

这才是工业场景真正需要的“可扩展告警”,而非弹窗提示。


4. 避坑指南:那些让开发者凌晨三点还在抓头发的典型问题

4.1 现象:程序启动后列表始终为空,recvfrom返回0

原因:SIO_RCVALL在部分网卡驱动(尤其是Realtek RTL8168系列)上需先调用WSAIoctl设置SO_BINDTODEVICE绑定到具体网卡,否则静默失败。原代码假设所有网卡行为一致。
解决:在socket创建后、ioctlsocket前插入:

// 绑定到指定网卡索引(idx从netsh命令获取) char szAdapter[32]; sprintf_s(szAdapter, "0x%08X", idx); setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, szAdapter, strlen(szAdapter)+1);

4.2 现象:CSV导出中文路径乱码,文件名显示为?????.csv

原因:ExportCSV.cpp用fopen("D:\\监控\\流量.csv", "w"),但VC6.0默认ANSI编码,而Windows记事本读CSV默认UTF-8无BOM。
解决:改用_wfopen并写入UTF-8 BOM:

FILE* fp = _wfopen(L"D:\\监控\\流量.csv", L"w, ccs=UTF-8"); if (fp) { fwrite("\xEF\xBB\xBF", 1, 3, fp); // UTF-8 BOM fprintf(fp, "源IP,目的IP,协议,字节数\n"); }

4.3 现象:高流量下CPU占用率100%,界面卡死

原因:CaptureLoop是单线程死循环,UpdateFlowStat中std::map::find在万级流时耗时飙升,且GUI消息泵被阻塞。
解决:分离线程+无锁队列。在WinMain中:

// 创建抓包线程 HANDLE hCapThread = CreateThread(nullptr, 0, CaptureThreadProc, nullptr, 0, nullptr); // 主线程只处理GUI和定时刷新 SetTimer(hWnd, 1, 1000, nullptr); // 每秒刷新一次列表

CaptureThreadProc中用std::queue<FlowEvent>暂存事件,主线程WM_TIMER中批量消费。

4.4 现象:虚拟机中运行报错WSAENOPROTOOPT

原因:VMware Workstation 16+默认禁用混杂模式,且SIO_RCVALL在NAT模式下无效。
解决:

  1. VMware设置 → 网络适配器 → 高级 → 勾选“混杂模式:接受”;
  2. 网络连接方式改为“桥接模式”;
  3. 代码中增加fallback逻辑:若SIO_RCVALL失败,则尝试AF_PACKET(需Linux)或降级为GetIfTable轮询接口统计(精度低但可用)。

4.5 现象:导出CSV时程序崩溃,调试显示fwrite访问违规

原因:ExportCSV.cpp中fopen后未检查返回值,且fprintf格式化字符串含未转义的%(如IP地址192.168.1.100%被误认为格式符)。
解决:

// 安全写入IP地址(避免%被解析) fprintf(fp, "%s,%s,%d,%lu\n", inet_ntoa(*(in_addr*)&key->srcIP), inet_ntoa(*(in_addr*)&key->dstIP), key->protocol, record->totalBytes); // 或更稳妥:用sprintf先格式化到缓冲区 char line[1024]; snprintf(line, sizeof(line), "%s,%s,%d,%lu\n", ...); fputs(line, fp);

5. 二次开发实战:从“能跑”到“能定制”的五个关键改造点

5.1 协议识别增强:在IP层之上加一层轻量解析

原项目只识别TCP/UDP/ICMP,但工业协议如Modbus TCP(端口502)、S7comm(102)、DNP3(20000)需要深度识别。不必引入libpcap,只需在UpdateFlowStat()前插入协议探测:

// 在PacketCapture.cpp中recvfrom后添加 if (ip->protocol == IPPROTO_TCP) { TCPHeader* tcp = (TCPHeader*)(buffer + (ip->ver_ihl & 0x0F) * 4); if (ntohs(tcp->dstPort) == 502) { // Modbus TCP ModbusHeader* mb = (ModbusHeader*)((char*)tcp + 20); // TCP头长20 if (mb->length >= 6) { // 最小Modbus ADU key->appProtocol = APP_MODBUS; key->functionCode = mb->functionCode; } } }

FlowKey新增BYTE appProtocol和BYTE functionCode字段,FlowRecord中增加WORD modbusErrorCount。这样统计时就能区分“正常读寄存器”和“非法功能码攻击”。

5.2 存储后端替换:用SQLite替代CSV,支持SQL查询

ExportCSV.cpp可整体替换为ExportDB.cpp:

#include <sqlite3.h> sqlite3* g_db = nullptr; void InitDB() { sqlite3_open("flow.db", &g_db); sqlite3_exec(g_db, "CREATE TABLE IF NOT EXISTS flows(" "id INTEGER PRIMARY KEY," "src_ip TEXT, dst_ip TEXT, protocol INT," "src_port INT, dst_port INT," "bytes INT, packets INT, timestamp DATETIME);", nullptr, nullptr, nullptr); } void SaveToDB(const FlowKey* key, const FlowRecord* rec) { char sql[1024]; sprintf(sql, "INSERT INTO flows VALUES(NULL,'%s','%s',%d,%d,%d,%d,%d,datetime('now'))", inet_ntoa(*(in_addr*)&key->srcIP), inet_ntoa(*(in_addr*)&key->dstIP), key->protocol, key->srcPort, key->dstPort, rec->totalBytes, rec->packetCount); sqlite3_exec(g_db, sql, nullptr, nullptr, nullptr); }

优势:

  • 支持SELECT * FROM flows WHERE bytes > 1000000 AND timestamp > '2023-01-01';
  • 可用DB Browser for SQLite直接查看,无需Excel;
  • 后续可加索引加速WHERE src_ip = '192.168.1.100'查询。

5.3 实时可视化:用GDI+画流量热力图

在主窗口WM_PAINT中绘制:

case WM_PAINT: PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); // 绘制Y轴:IP地址(取后两段) for (int i = 0; i < min(100, (int)g_flowMap.size()); i++) { auto it = g_flowMap.begin(); advance(it, i); char ipStr[16]; sprintf(ipStr, "%d.%d", (it->first.srcIP >> 16) & 0xFF, (it->first.srcIP >> 24) & 0xFF); TextOut(hdc, 10, 20 + i*16, ipStr, strlen(ipStr)); // 按字节数画条形图(0-100%映射到0-200像素) int width = min(200, (int)(it->second.totalBytes / 10000.0)); HBRUSH hBrush = CreateSolidBrush(RGB(0, 128, 255)); FillRect(hdc, &RECT{30, 20+i*16, 30+width, 20+i*16+12}, hBrush); DeleteObject(hBrush); } EndPaint(hWnd, &ps); break;

效果:左侧IP列表,右侧蓝色条形图直观显示各IP流量占比,比数字更易发现异常峰值。

5.4 网络策略联动:调用netsh动态封禁IP

在AlertCallback中实现:

void MyAlertHandler(const FlowKey* key, const FlowRecord* rec) { if (rec->synCount > 200) { // SYN Flood阈值 char cmd[256]; sprintf(cmd, "netsh advfirewall firewall add rule name=\"Block %s\" dir=in action=block remoteip=%s enable=yes", inet_ntoa(*(in_addr*)&key->srcIP), inet_ntoa(*(in_addr*)&key->srcIP)); system(cmd); // 生产环境建议用CreateProcess并捕获输出 } }

提示:netsh规则名需唯一,建议拼接时间戳避免重复;封禁后需记录到日志,否则重启防火墙规则丢失。

5.5 跨平台移植预备:抽象Socket层,为Linux编译铺路

当前代码Windows强耦合,改造第一步是定义跨平台Socket封装:

// SocketWrapper.h #ifdef _WIN32 #include <winsock2.h> typedef SOCKET SocketHandle; #define INVALID_SOCKET_HANDLE INVALID_SOCKET #define CLOSE_SOCKET(s) closesocket(s) #else #include <sys/socket.h> #include <netinet/in.h> typedef int SocketHandle; #define INVALID_SOCKET_HANDLE -1 #define CLOSE_SOCKET(s) close(s) #endif class SocketWrapper { public: static SocketHandle CreateRawSocket(); static int RecvFrom(SocketHandle sock, char* buf, int len, sockaddr_in* addr); static void SetPromiscuousMode(SocketHandle sock, bool enable); };

然后将PacketCapture.cpp中所有socket/recvfrom/ioctlsocket调用替换为SocketWrapper::静态方法。这样后续只需实现Linux版SocketWrapper.cpp(用PF_PACKET+AF_PACKET),即可编译出Linux版本。

从那以后我每次接手老项目,第一件事就是检查#include链和#pragma comment——它们比注释更诚实。这个VC++流量监控系统教会我:真正的轻量,不是删功能,而是把每个字节的用途刻进代码逻辑里;真正的兼容,不是堆条件编译,而是用接口抽象把平台差异锁进一个cpp文件。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询