这两天一直在研究如何去获取网络利用率(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 < comdef.h >
#include < Iphlpapi.h >
#pragma comment(lib , "Iphlpapi.lib")
同样为了使用智能指针,我又做了以下声明
_COM_SMARTPTR_TYPEDEF(IWbemServices, __uuidof(IWbemServices));
_COM_SMARTPTR_TYPEDEF(IEnumWbemClassObject, __uuidof(IEnumWbemClassObject));
_COM_SMARTPTR_TYPEDEF(IWbemClassObject, __uuidof(IWbemClassObject));
对于WMI的具体操作,我就不在这里多说了。最主要的是介绍一下我的实现方式。首先我们需要用WMI去查询获取网卡的一些必要的信息,如MAC地址,Interface Index和线路速度
先定义一个结构体来存储网卡与使用率的映射关系
{
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。
{
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的信息得到
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的数据大小。所以代码里还加了数值溢出处理。到这里我们的线路速度和网卡使用率就可以关联起来了。下面是测试代码
{
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之后的系统中有效。