当我们把网线插到计算机上时,WINDOWS任务栏的托盘图标都会更改相应的网络图标,拔掉也会有相应的处理。一直都对这个机制感兴趣,却不知道如何做,而且公司的某个产品也需要这么一个功能。昨天在家测试某个程序的时候,发现了其中一个线程的栈中有一个叫wininet!CheckForNetworkChange的函数,IDA分析了WINNET.DLL后,有了本文。

微软在WINDOWS VISTA之后提供了一个叫NLA(Network List Manager API)的接口,用于获取网络状态变化通知的一个接口。以COM技术实现。
主要导出的COM接口如下:

  • IEnumNetworkConnections
  • IEnumNetworks
  • INetwork
  • INetworkConnection
  • INetworkConnectionEvents
  • INetworkEvents
  • INetworkListManager
  • INetworkListManagerEvents

其中INetworkListManager是一个根对象,可以获取计算机是否连接到因特网(INetworkListManager->get_IsConnectedToInternet)。还可以查询有哪些可用的网络和连接.更关键的是INetworkListManagerEvents和INetworkEvents两个类。这两个类在MSDN文档里的描述如下:

is a message sink interface that a client implements to get overall machine state related events.

也就是说我们要自己实现这两个类。而回调的方式是通过COM技术中特有的机制IConnectionPoint来搞定。实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class CNetworkListManagerEvent : public INetworkListManagerEvents
{
public:
CNetworkListManagerEvent() : m_ref(1)
{

}

~CNetworkListManagerEvent()
{

}

HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject)
{
HRESULT Result = S_OK;
if (IsEqualIID(riid, IID_IUnknown))
{
*ppvObject = (IUnknown*)this;
}
else if (IsEqualIID(riid, IID_INetworkListManagerEvents))
{
*ppvObject = (INetworkListManagerEvents*)this;
}
else
{
Result = E_NOINTERFACE;
}

return Result;
}

ULONG STDMETHODCALLTYPE AddRef()
{
return (ULONG)InterlockedIncrement(&m_ref);
}

ULONG STDMETHODCALLTYPE Release()
{
LONG Result = InterlockedDecrement(&m_ref);
if (Result == 0)
delete this;
return (ULONG)Result;
}

virtual HRESULT STDMETHODCALLTYPE ConnectivityChanged(
/* [in] */ NLM_CONNECTIVITY newConnectivity)
{
return S_OK;
}

private:
LONG m_ref;
};

int _tmain(int argc, TCHAR** argv, TCHAR** Env)
{
CoInitialize(NULL);

//
// 通过NLA接口获取网络状态
//
IUnknown* pUnknown = NULL;

HRESULT Result = CoCreateInstance(CLSID_NetworkListManager, NULL, CLSCTX_ALL, IID_IUnknown, (void**)&pUnknown);
if (SUCCEEDED(Result))
{
INetworkListManager* pNetworkListManager = NULL;
Result = pUnknown->QueryInterface(IID_INetworkListManager, (void**)&pNetworkListManager);
if (SUCCEEDED(Result))
{
VARIANT_BOOL IsConnect = VARIANT_FALSE;
Result = pNetworkListManager->get_IsConnectedToInternet(&IsConnect);
if (SUCCEEDED(Result))
{
printf("IsConnect Result %s\n", IsConnect == VARIANT_TRUE ? "TRUE" : "FALSE");
}

IConnectionPointContainer* pCPContainer = NULL;
Result = pNetworkListManager->QueryInterface(IID_IConnectionPointContainer, (void**)&pCPContainer);
if (SUCCEEDED(Result))
{
IConnectionPoint* pConnectPoint = NULL;
Result = pCPContainer->FindConnectionPoint(IID_INetworkListManagerEvents, &pConnectPoint);
if (SUCCEEDED(Result))
{
DWORD Cookie = NULL;
CNetworkListManagerEvent* NetEvent = new CNetworkListManagerEvent;
Result = pConnectPoint->Advise((IUnknown*)NetEvent, &Cookie);
if (SUCCEEDED(Result))
{
printf("Loop Message\n");
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);

if (msg.message == WM_QUIT)
{
break;
}
}

pConnectPoint->Unadvise(Cookie);

pConnectPoint->Release();
}
}

pCPContainer->Release();
}

pNetworkListManager->Release();
}

pUnknown->Release();
}

CoUninitialize();
return 1;
}

因为NLA API是WINDOWS VISTA之后才有的,对于WINDOWS XP是不兼容的,但是WINDOWS XP下有一个方法可以达到同样的效果,可以参考文章Network Awareness in Windows XP,这个文章里的代码是C#的,其中关键的代码转换成 C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void WaitForNetworkChnages()
{
WSAQUERYSET querySet = { 0 };
querySet.dwSize = sizeof(WSAQUERYSET);
querySet.dwNameSpace = NS_NLA;

HANDLE LookupHandle = NULL;
WSALookupServiceBegin(&querySet, LUP_RETURN_ALL, &LookupHandle);
DWORD BytesReturned = 0;
WSANSPIoctl(LookupHandle, SIO_NSP_NOTIFY_CHANGE, NULL, 0, NULL, 0, &BytesReturned, NULL);
WSALookupServiceEnd(LookupHandle);
}

void Test()
{
WSAData data = { 0 };
WSAStartup(MAKEWORD(2, 0), &data);
int i = 0;
while (1)
{
printf("BeginWait %d\n", i++);
WaitForNetworkChnages();
printf("EndWait\n");
}

WSACleanup();
}