這兩天一直在研究如何去獲取網絡利用率(usage)和網卡線路速度(link speed)的問題,找到了一個比較好的方案,寫出來跟大家分享一下。
記得我在以前的一篇博文中提到過這樣一個問題,有時我們添加兩個虛擬網卡時,兩個網卡名稱是一樣的,這樣的結果就是我們無法根據名稱去匹配指定的網卡。
通常我們獲取網卡的信息有兩種方式:1. WMI的win32_networkAdapter類;2. IpHlpApi框架。
而獲取網絡使用率的方式也有兩種:1. performance monitor編程接口;2. Win32_PerfFormattedData_Tcpip_NetworkInterface類。
但是我發現這些方式都沒辦法解決我以上提到的問題。因為無論是從performance monitor,還是Win32_PerfFormattedData_Tcpip_NetworkInterface來獲取網絡利用率都是依賴於網卡名。另外,我發現在Windows Task manager裡面看的網絡使用率和線路速度都匹配的很正常。所以,直覺是覺得應該有一種方式可以比較好的去獲取這兩個值,無論網卡名是否相同。通過研究發現,其實想要獲取這兩個值,並且建立匹配關系可以通過WMI和IpHlpApi框架來實現。順便說一句,我的目標是該程序能運行在win2000以後的所有系統上,所以出於兼容性的考慮,我會放棄那些只支持vista之後操作系統的方案。下面我們具體來看一下,如何用代碼來實現:
為了獲得WMI和IpHlpApi框架的支持,我們需要包含下面幾個頭文件和庫:
#include <Wbemidl.h>
#include <comdef.h>
#include <Iphlpapi.h>
#pragma comment(lib , "Iphlpapi.lib")
同樣為了使用智能指針,我又做了以下聲明
_COM_SMARTPTR_TYPEDEF(IWbemLocator, __uuidof(IWbemLocator));
_COM_SMARTPTR_TYPEDEF(IWbemServices, __uuidof(IWbemServices));
_COM_SMARTPTR_TYPEDEF(IEnumWbemClassObject, __uuidof(IEnumWbemClassObject));
_COM_SMARTPTR_TYPEDEF(IWbemClassObject, __uuidof(IWbemClassObject));
對於WMI的具體操作,我就不在這裡多說了。最主要的是介紹一下我的實現方式。首先我們需要用WMI去查詢獲取網卡的一些必要的信息,如MAC地址,Interface Index和線路速度
先定義一個結構體來存儲網卡與使用率的映射關系
typedef struct _tagNetworkUtilizationMapElement
{
wchar_t MAC[64];
unsigned int interfaceIndex;
unsigned __int64 linkSpeed;
DWORD preInBytes;
DWORD preOutBytes;
unsigned int usage;
}NetworkUtilization_Map_Element, *PNetworkUtilization_Map_Element;
之後通過一個WMI查詢去獲取MAC, InterfaceIndex和 linkSpeed。
代碼
DWORD InitNetworkAdapterInfo(vector<NetworkUtilization_Map_Element> & refNUArray)
{
HRESULT hres = S_OK;
IWbemLocatorPtr pLocPtr = NULL;
hres = CoCreateInstance(
CLSID_WbemLocator,
0,
CLSCTX_INPROC_SERVER,
IID_IWbemLocator, (LPVOID *) &pLocPtr);
if(FAILED(hres)) return hres;
IWbemServicesPtr pSvcPtr;
hres = pLocPtr->ConnectServer(
_bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace
NULL, // User name. NULL = current user
NULL, // User password. NULL = current
0, // Locale. NULL indicates current
NULL, // Security flags.
0, // Authority (e.g. Kerberos)
0, // Context object
&pSvcPtr // pointer to IWbemServices proxy
);
if(FAILED(hres)) return hres;
hres = CoSetProxyBlanket(
pSvcPtr, // Indicates the proxy to set
RPC_C_AUTHN_WINNT, // RPC_C_AUTHN_xxx
RPC_C_AUTHZ_NONE, // RPC_C_AUTHZ_xxx
NULL, // Server principal name
RPC_C_AUTHN_LEVEL_CALL, // RPC_C_AUTHN_LEVEL_xxx
RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
NULL, // client identity
EOAC_NONE // proxy capabilities
);
if(FAILED(hres)) return hres;
vector<wstring> attributes;
attributes.push_back(L"MACAddress");
attributes.push_back(L"InterfaceIndex");
attributes.push_back(L"Speed");
// Query Network Adapter information from WMI
IEnumWbemClassObjectPtr pEnumerator = NULL;
hres = pSvcPtr->ExecQuery(
bstr_t("WQL"),
L"SELECT * FROM Win32_NetworkAdapter where adapterTypeId = 0",
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
NULL,
&pEnumerator);
if (FAILED(hres)) return hres;
ULONG ulReturn = 0;
IWbemClassObjectPtr pclsObj = NULL;
while (pEnumerator != NULL)
{
hres = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &ulReturn);
if (hres == WBEM_S_FALSE || ulReturn == 0)
break;
if (hres == WBEM_S_NO_ERROR)
{
VARIANT vtProp;
NetworkUtilization_Map_Element nui = {0};
// Populate the network adapter information and store it
hres = pclsObj->Get(attributes[0].c_str(), 0, &vtProp, 0, 0);
if (SUCCEEDED(hres) && vtProp.vt == VT_BSTR)
{
swprintf_s(nui.MAC, _countof(nui.MAC), L"%s", vtProp.bstrVal);
}
hres = pclsObj->Get(attributes[1].c_str(), 0, &vtProp, 0, 0);
if (SUCCEEDED(hres) && vtProp.vt == VT_I4)
{
nui.interfaceIndex = vtProp.intVal;
}
hres = pclsObj->Get(attributes[2].c_str(), 0, &vtProp, 0, 0);
if (SUCCEEDED(hres) && vtProp.vt == VT_BSTR)
{
nui.linkSpeed = _wtoi64(vtProp.bstrVal);
}
if (SUCCEEDED(hres))
{
refNUArray.push_back(nui);
}
}
}
return hres;
}
為了方便,我並沒有對這段代碼進行優化,所以看起來比較冗長而且不通用,不過功能還是能work的。有了這些信息這後,我們就需要依靠這些信息去獲取網絡使用率了。因為沒有辦法是用PF和WMI這樣現成的類來直接獲取,那麼我們就只能用一種間接的方式還獲取。不知道大家是否還記得網卡使用率的計算公式:u = (send bytes + receive bytes) / link speed
也就是說,我們想得到u還必須得到send bytes和receive bytes。那麼如何去獲取這兩個值呢?仔細查看了一下msdn關於IpHlpApi的信息得到
代碼
typedef struct _MIB_IFROW {
WCHAR wszName[MAX_INTERFACE_NAME_LEN];
DWORD dwIndex;
DWORD dwType;
DWORD dwMtu;
DWORD dwSpeed;
DWORD dwPhysAddrLen;
BYTE bPhysAddr[MAXLEN_PHYSADDR];
DWORD dwAdminStatus;
DWORD dwOperStatus;
DWORD dwLastChange;
DWORD dwInOctets; --- 注意這個
DWORD dwInUcastPkts;
DWORD dwInNUcastPkts;
DWORD dwInDiscards;
DWORD dwInErrors;
DWORD dwInUnknownProtos;
DWORD dwOutOctets; --- 和這個
DWORD dwOutUcastPkts;
DWORD dwOutNUcastPkts;
DWORD dwOutDiscards;
DWORD dwOutErrors;
DWORD dwOutQLen;
DWORD dwDescrLen;
BYTE bDescr[MAXLEN_IFDESCR];
}MIB_IFROW, *PMIB_IFROW;
根據msdn的描述,看起來是我們要的兩個值。但是經過測試發現這兩個值是累計值而不是實時值,就是這兩個值記載了網卡從啟動開始傳輸的所有字節數。不過還好,不能直接用那就間接用,我們通過每隔一秒取一次變化還是能計算出我們需要的值。這個函數實現是這樣:
代碼
DWORD PopulateNetworkUtilization(vector<NetworkUtilization_Map_Element> & refNUArray)
{
PMIB_IFTABLE pIfTab;
DWORD dwSize = 0;
DWORD dwRetval = 0;
pIfTab = (PMIB_IFTABLE)malloc(sizeof(PMIB_IFTABLE));
if (pIfTab == NULL)
return 0x01;
if (GetIfTable(pIfTab, &dwSize, 0) == ERROR_INSUFFICIENT_BUFFER)
{
free(pIfTab);
pIfTab = (PMIB_IFTABLE)malloc(dwSize);
}
if ((dwRetval = GetIfTable(pIfTab, &dwSize, 0)) == NO_ERROR)
{
// Retrieve the transmission of each network card
for (int i=0; i<(int)pIfTab->dwNumEntries; i++)
{
for (int j=0; j<(int)refNUArray.size(); j++)
{
if (refNUArray[j].interfaceIndex == pIfTab->table[i].dwIndex)
{
DWORD inBytesDet = 0;
DWORD outBytesDet = 0;
// if it isn't first call
if (refNUArray[j].preInBytes != 0 && refNUArray[j].preOutBytes != 0)
{
// Deal with the overflow case
inBytesDet = (refNUArray[j].preInBytes > pIfTab->table[i].dwInOctets)
? 0xFFFFFFFF - refNUArray[j].preInBytes + pIfTab->table[i].dwInOctets
: pIfTab->table[i].dwInOctets - refNUArray[j].preInBytes;
outBytesDet = (refNUArray[j].preOutBytes > pIfTab->table[i].dwOutOctets)
?0xFFFFFFFF - refNUArray[j].preOutBytes + pIfTab->table[i].dwOutOctets
: pIfTab->table[i].dwOutOctets - refNUArray[j].preOutBytes;
}
refNUArray[j].preInBytes = pIfTab->table[i].dwInOctets;
refNUArray[j].preOutBytes = pIfTab->table[i].dwOutOctets;
unsigned __int64 totalBytesDet = ((unsigned __int64)inBytesDet + (unsigned __int64)outBytesDet) * 8;
unsigned int usage = (unsigned int) ((totalBytesDet > refNUArray[j].linkSpeed)
? 100 : (totalBytesDet * 100 / refNUArray[j].linkSpeed));
refNUArray[j].usage = usage;
break;
}
}
}
}
free(pIfTab);
return 0x00;
}
因為這兩個值是DWORD類型的,也就是說只能表達4G的數據大小。所以代碼裡還加了數值溢出處理。到這裡我們的線路速度和網卡使用率就可以關聯起來了。下面是測試代碼
代碼
int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
vector<NetworkUtilization_Map_Element> numeArray;
if (!SUCCEEDED(InitNetworkAdapterInfo(numeArray)))
return 1;
if (!SUCCEEDED(PopulateNetworkUtilization(numeArray)))
return 2;
for (int i=0; i<120; i++)
{
if (!SUCCEEDED(PopulateNetworkUtilization(numeArray)))
return 2;
// test
vector<NetworkUtilization_Map_Element>::iterator it;
for (it = numeArray.begin(); it != numeArray.end(); it++)
{
cout << "[" << endl;
wcout << "MAC: " << it->MAC << endl;
cout << "index: " << it->interfaceIndex << endl;
cout << "link speed : " << it->linkSpeed << endl;
cout << "usage : " << it->usage << endl;
cout << "]" << endl;
}
Sleep(1000);
}
CoUninitialize();
return 0;
}
到這裡這個方案基本就完成了,有心的朋友仔細查一下msdn可能會發現其實MIB_IFROW2的結構體裡面就有linkSpeed字段,為什麼不用呢?還是上面的話,因為這個結構體只在vista之後的系統中有效。