简介:本资源面向在 Windows 平台使用 Visual Studio 2017 进行网络编程的开发者,聚焦于检测网线插入与拔出状态这一常见需求。内容围绕 Windows 网络接口状态 API 展开,重点讲解 GetAdaptersAddresses 的调用方式,通过遍历接口列表并检查 OperStatus 字段判断连接状态,同时涉及 WM_DEVICECHANGE 消息与 RegisterDeviceNotification 实现事件驱动的实时监控思路,并提醒多网卡、有线与无线差异等注意事项。压缩包共 27 个文件,约 14.93MB,包含 cpp 与 h 源码、vcxproj 与 sln 工程文件、exe 可执行程序,以及 pdb、obj、tlog 等编译调试中间产物,便于直接打开工程运行验证。目前已有 875 人学习下载,适合希望快速掌握网线插拔监测实现方法、复用简洁代码逻辑的 C++ 开发者参考。
1. 网线插拔检测:一个被低估的 Windows 桌面刚需
做工业上位机或者内网工具的朋友大概率遇到过这种场景:设备明明就在对面,程序却卡在连接超时上,排查半天发现是网线松了。用户不会看网口指示灯,他们只会说“你这软件又连不上了”。与其在日志里大海捞针,不如让程序自己盯着网线状态——插上就自动重连,拔掉就立刻提示。这个基于 VS2017 和 Windows 系统的网线插入拔出状态检测方案,解决的就是这个具体问题。它不依赖任何第三方网络库,直接用系统原生接口轮询或订阅网卡状态变化,编译出来就是一个干净的 Win32 控制台或 MFC 对话框程序。适合谁?写设备通讯、做产线测试工具、维护老旧工控软件的那批人。你不需要懂驱动开发,只要会 C++ 和基本的 Windows 消息机制就能上手改。
2. 技术选型与工程骨架:为什么不用 WMI 轮询
2.1 三种检测路线的取舍
在 Windows 上检测网线插拔,常见做法有三条路。第一条是定时器轮询GetAdaptersAddresses,看OperStatus字段是不是IfOperStatusUp。第二条是 WMI 事件订阅,监听MSFT_NetAdapter的State变化。第三条是注册网络配置变化通知,用NotifyAddrChange或NotifyIpInterfaceChange拿回调。
我一般会选第三条为主、第一条为辅。原因很直接:WMI 在 VS2017 默认工程里要引入wbemidl.h和一堆 COM 初始化代码,编译慢,运行时还依赖 WMI 服务,工控机上经常被裁剪掉。而NotifyIpInterfaceChange是iphlpapi.lib里的函数,链接即用,回调触发及时,不需要开线程轮询。轮询方案留作兜底,防止某些虚拟网卡驱动不触发通知。
注意:
NotifyIpInterfaceChange从 Vista 开始提供,VS2017 默认的 Windows SDK 版本完全支持,不需要额外装 SDK。
2.2 工程目录与依赖配置
新建一个“Windows 桌面应用程序”或者空项目都行,我习惯用空项目加main.cpp,结构清爽。目录大概长这样:
NetCableMonitor/ ├── NetCableMonitor.sln ├── NetCableMonitor/ │ ├── main.cpp │ ├── CableMonitor.h │ ├── CableMonitor.cpp │ └── targetver.h关键在项目属性里加两个依赖:iphlpapi.lib和ws2_32.lib。前者提供NotifyIpInterfaceChange和GetAdaptersAddresses,后者用于WSAStartup初始化网络环境。在 VS2017 里右键项目 → 属性 → 链接器 → 输入 → 附加依赖项,把这两个库名填进去。如果你用的是 MFC 对话框程序,把监控逻辑封装成一个类,在OnInitDialog里启动,OnDestroy里停止。
// CableMonitor.h #pragma once #include <winsock2.h> #include <iphlpapi.h> #include <netioapi.h> #include <functional> #include <string> class CableMonitor { public: // 回调:参数为网卡名称和是否已连接 using StateCallback = std::function<void(const std::wstring&, bool)>; CableMonitor(); ~CableMonitor(); // 启动监控,传入状态变化时的回调 bool Start(StateCallback cb); // 停止监控,释放通知句柄 void Stop(); private: // 系统通知回调,由 Windows 调用 static void CALLBACK InterfaceChangeCallback( PVOID callerContext, PMIB_IPINTERFACE_ROW row, MIB_NOTIFICATION_TYPE notificationType); // 查询指定网卡当前是否处于连接状态 bool QueryInterfaceConnected(NET_LUID luid); HANDLE m_notifyHandle = nullptr; StateCallback m_callback; bool m_running = false; };这段头文件定义了监控类的接口。StateCallback用std::function包装,方便你在外部传入 lambda 或者成员函数绑定。InterfaceChangeCallback是静态函数,因为 Windows API 要求回调是CALLBACK调用约定,静态成员函数正好匹配。QueryInterfaceConnected接收NET_LUID,这是网卡的唯一标识,比用索引号可靠,重启网卡后索引会变但 LUID 不变。
2.3 初始化与回调注册
Start函数里要做三件事:初始化 Winsock、注册通知、立即查一次当前状态。为什么立即查一次?因为通知只在状态变化时触发,如果程序启动时网线已经插着,你不主动查就不知道初始状态。
// CableMonitor.cpp #include "CableMonitor.h" #include <iostream> CableMonitor::CableMonitor() { WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); } CableMonitor::~CableMonitor() { Stop(); WSACleanup(); } bool CableMonitor::Start(StateCallback cb) { if (m_running) return true; m_callback = cb; // 注册接口变化通知,首次调用会立即回调一次 DWORD ret = NotifyIpInterfaceChange( AF_UNSPEC, // 监控 IPv4 和 IPv6 InterfaceChangeCallback, // 回调函数 this, // 上下文传 this FALSE, // 不立即触发初始回调 &m_notifyHandle // 输出句柄 ); if (ret != NO_ERROR) { std::cerr << "NotifyIpInterfaceChange failed: " << ret << std::endl; return false; } m_running = true; return true; } void CableMonitor::Stop() { if (m_notifyHandle) { CancelMibChangeNotify2(m_notifyHandle); m_notifyHandle = nullptr; } m_running = false; }NotifyIpInterfaceChange的第四个参数设为FALSE,表示注册时不立刻回调。如果你设成TRUE,系统会马上用当前所有接口的状态调一次回调,省去手动查询初始状态的代码。我选FALSE是因为手动查询更可控,能区分“初始状态”和“变化状态”。CancelMibChangeNotify2必须在程序退出前调用,否则句柄泄漏,反复启动停止会耗尽系统资源。
3. 核心模块拆解:回调里怎么判断网线插拔
3.1 从通知类型到物理状态
回调函数收到的是MIB_NOTIFICATION_TYPE,常见值有MibAddInstance、MibDeleteInstance、MibParameterNotification。网线插拔通常触发MibParameterNotification,因为网卡实例一直在,只是OperStatus变了。但虚拟网卡(比如某些虚拟化软件创建的)插拔时会触发MibAddInstance和MibDeleteInstance。所以不能只看一种类型。
void CALLBACK CableMonitor::InterfaceChangeCallback( PVOID callerContext, PMIB_IPINTERFACE_ROW row, MIB_NOTIFICATION_TYPE notificationType) { auto* self = static_cast<CableMonitor*>(callerContext); if (!self || !row) return; // 只关心物理网卡,过滤掉回环和隧道 if (row->InterfaceLuid.Value == 0) return; bool connected = self->QueryInterfaceConnected(row->InterfaceLuid); std::wstring name = L"Unknown"; // 获取网卡友好名称,方便日志输出 MIB_IF_ROW2 ifRow = {}; ifRow.InterfaceLuid = row->InterfaceLuid; if (GetIfEntry2(&ifRow) == NO_ERROR) { name = ifRow.Description; } if (self->m_callback) { self->m_callback(name, connected); } }回调里先做过滤,InterfaceLuid.Value == 0的直接跳过,那是无效接口。然后调QueryInterfaceConnected拿真实连接状态,再通过GetIfEntry2取网卡描述。MIB_IF_ROW2结构体里的Description字段就是设备管理器里看到的网卡名,比如“Realtek PCIe GbE Family Controller”。把这个名字传给回调,用户一看就知道是哪块网卡。
3.2 查询连接状态的正确姿势
QueryInterfaceConnected是判断网线物理连接的核心。注意OperStatus和MediaConnectState的区别:OperStatus表示接口是否可操作(可能因为软件禁用而 Down),MediaConnectState才真正反映物理链路状态。网线拔掉时,MediaConnectState变成MediaConnectStateDisconnected。
bool CableMonitor::QueryInterfaceConnected(NET_LUID luid) { MIB_IF_ROW2 row = {}; row.InterfaceLuid = luid; if (GetIfEntry2(&row) != NO_ERROR) { return false; } // MediaConnectState 才是物理链路状态 // MediaConnectStateConnected 表示网线插着且对端有信号 return row.MediaConnectState == MediaConnectStateConnected; }GetIfEntry2是GetIfEntry的增强版,支持NET_LUID和 IPv6。返回NO_ERROR表示查询成功,然后看MediaConnectState。有些老代码用OperStatus == IfOperStatusUp判断,这在网线插着但没插对端设备时会误判为 Down,因为链路没协商起来。用MediaConnectState更准,它直接对应物理层信号。
3.3 主程序里的调用与输出
把监控类用起来,写一个控制台版本,插拔网线时打印日志。这个版本可以直接编译运行,用来验证逻辑。
// main.cpp #include "CableMonitor.h" #include <iostream> #include <csignal> static CableMonitor g_monitor; BOOL WINAPI ConsoleHandler(DWORD signal) { if (signal == CTRL_C_EVENT) { g_monitor.Stop(); ExitProcess(0); } return TRUE; } int main() { SetConsoleOutputCP(CP_UTF8); SetConsoleCtrlHandler(ConsoleHandler, TRUE); std::wcout << L"开始监控网线状态,按 Ctrl+C 退出..." << std::endl; bool ok = g_monitor.Start([](const std::wstring& name, bool connected) { std::wstring state = connected ? L"已插入" : L"已拔出"; std::wcout << L"[" << name << L"] " << state << std::endl; }); if (!ok) { std::cerr << "监控启动失败" << std::endl; return 1; } // 主线程挂起,等待回调 while (true) { Sleep(1000); } return 0; }SetConsoleOutputCP(CP_UTF8)让控制台能正确显示中文网卡名。SetConsoleCtrlHandler捕获 Ctrl+C,在退出前调Stop释放通知句柄。回调里用std::wcout输出宽字符串,因为网卡描述是 Unicode 的。主线程用Sleep循环挂起,实际项目中你可以换成消息循环或者事件等待。
提示:如果你在 MFC 对话框里用,回调发生在系统线程,更新 UI 要用
PostMessage发自定义消息到主线程,不能直接调SetWindowText。
4. 避坑与排查:网线检测翻车的五个真实场景
4.1 回调不触发,插拔网线毫无反应
现象:程序启动后,拔掉网线再插上,日志里什么都没有。原因通常是NotifyIpInterfaceChange的第一个参数传了AF_INET,只监控 IPv4。有些网卡在链路断开时 IPv4 地址被移除,系统认为接口消失了,但通知类型是MibDeleteInstance,如果你只处理MibParameterNotification就会漏掉。解决:第一个参数用AF_UNSPEC,回调里不区分通知类型,统一查MediaConnectState。
4.2 虚拟网卡疯狂刷屏
现象:装了虚拟机或者某些开发工具后,日志里每秒几十条状态变化,全是“已插入”。原因是虚拟网卡驱动会频繁上报链路状态。解决:在回调里用GetIfEntry2拿到Type字段,过滤掉IF_TYPE_SOFTWARE_LOOPBACK和IF_TYPE_TUNNEL。更彻底的做法是只处理IF_TYPE_ETHERNET_CSMACD和IF_TYPE_IEEE80211,前者是有线,后者是无线。
4.3 程序退出时报内存泄漏
现象:用调试器跑,退出时输出窗口一堆Detected memory leaks。原因是NotifyIpInterfaceChange返回的句柄没取消。解决:确保Stop在析构里被调用,并且CancelMibChangeNotify2的返回值要检查。如果返回ERROR_NOT_FOUND,说明句柄已经失效,忽略即可。另外WSAStartup和WSACleanup要配对,别在Start里反复调WSAStartup。
4.4 网线插着但显示“已拔出”
现象:网线明明插在交换机和电脑上,指示灯也亮,程序却报断开。原因是MediaConnectState在某些老网卡驱动上返回MediaConnectStateUnknown。解决:加一个兜底判断,如果MediaConnectState是Unknown,再看OperStatus和AccessType。或者用GetAdaptersAddresses查OperStatus,两个结果取或。我一般会在QueryInterfaceConnected里加日志,把MediaConnectState和OperStatus都打出来,方便定位。
4.5 多网卡场景下分不清哪根线
现象:电脑有有线网卡和无线网卡,拔掉网线后程序提示“已拔出”,但用户以为说的是无线。原因是回调里只传了状态没传网卡类型。解决:在回调里把ifRow.Type和ifRow.Description一起传出去,UI 上显示“有线网卡 [Realtek ...] 已拔出”。如果只想监控特定网卡,可以在Start时传入目标网卡的描述关键字,回调里做匹配过滤。
5. 二次开发改造:从控制台到可配置的监控服务
5.1 把硬编码改成配置文件驱动
控制台版本里网卡过滤是写死的,实际项目里不同机器网卡名不一样。我一般会加一个config.ini,用GetPrivateProfileString读配置。配置项包括:要监控的网卡描述关键字、插拔时执行的命令、日志文件路径。这样部署到产线机器上不用重新编译。
// 读取配置示例 wchar_t keyword[256] = {}; GetPrivateProfileString(L"Monitor", L"AdapterKeyword", L"", keyword, 256, L".\\config.ini"); // 回调里做匹配 if (name.find(keyword) == std::wstring::npos) return;GetPrivateProfileString是 Win32 API,不需要额外库。AdapterKeyword填“Realtek”就能只监控 Realtek 网卡。如果留空,监控所有物理网卡。
5.2 插拔时触发外部动作
检测到网线插入后自动重连服务器,或者拔掉后弹窗提醒,这是最常见的二次开发需求。在回调里不要直接做耗时操作,因为回调运行在系统线程,阻塞久了会影响其他网络通知。正确做法是发一个自定义消息到主线程,或者用线程池异步执行。
// 在 MFC 对话框里定义自定义消息 #define WM_CABLE_CHANGED (WM_USER + 100) // 回调里发消息 ::PostMessage(g_hMainWnd, WM_CABLE_CHANGED, connected ? 1 : 0, (LPARAM)new std::wstring(name)); // 消息处理函数里做实际动作 LRESULT OnCableChanged(WPARAM wParam, LPARAM lParam) { bool connected = (wParam == 1); auto* name = reinterpret_cast<std::wstring*>(lParam); if (connected) { ReconnectServer(); } else { ShowBalloonTip(L"网线已拔出"); } delete name; return 0; }PostMessage是异步的,不会阻塞回调线程。lParam传new std::wstring要注意在消息处理完后delete,否则泄漏。如果嫌麻烦,可以用std::shared_ptr配合WM_COPYDATA,但代码量会多不少。
5.3 验证改造是否生效的笨办法
改完代码别急着上产线,先做三个验证。第一,在虚拟机里禁用网卡再启用,看是否触发通知。第二,拔掉网线等 10 秒再插上,看是否只报一次状态变化,而不是反复报。第三,同时插拔两根网线,看是否分别识别。我习惯在QueryInterfaceConnected里加一行OutputDebugString,用 DebugView 抓输出,比看控制台日志更实时。
注意:
OutputDebugString在 Release 版本里也会执行,只是没有调试器接收。如果日志量大,记得用#ifdef _DEBUG包起来。
从那以后我每次写网线检测,都强制走一遍“拔掉等十秒再插上”的测试,因为很多驱动在链路断开后会有几秒的抖动期,不测这个场景,上线后用户就会看到状态反复横跳。希望帮到你。
本文还有配套的精品资源,点击获取