第三章 Snmp在Windows下的实现----WinSNMP编程原理
在Windows下实现SNMP协议的编程,可以采用Winsock接口,在161,162端口通过udp传送信息。在Windows 2000中,Microsoft已经封装了SNMP协议的实现,提供了一套可供在Windows下开发基于SNMP的网络管理程序的接口,这就是WinSNMP API。
3.1 什么是WinSNMP
WinSNMP的目的是为在Windows下开发基于SNMP的网络管程序提供解决方案。它为SNMP网管开发者提供了必须遵循的开放式单一接口规范,它定义了过程调用、数据类型、数据结构和相关的语法。
图3.1显示了一个网络管理站(NMS)和网络管理代理(Agent)之间端到端的SNMP连接中WinSNMP所处的层次。这是一个WinSNMP的参考模型。
图3.1WinSNMP参考模型
总的来说,WinSNMP以函数的形式封装了SNMP协议的各部分(在VC++6.0开发环境中体现为wsnmp32.dll、wsnmp32.lib和winsnmp.h),且针对SNMP是使用UDP的特点而设置了消息重传、超时机制等。
3.2 一些基本概念
在WinSNMP编程中,我们需要考虑的基本概念主要有以下几点:
SNMP支持层次
Entity/Context转换模式
本地数据库
会话
异步模式
内存管理
下面我们将分别对它们作介绍。
3.2.1 SNMP支持层次(Levels of SNMP Support)
WinSNMP支持四个层次的SNMP操作:
Level 0 = 只有消息编码/解码
Level 1 = Level 0 + 与SNMPv1代理的通信
Level 2 = Level 1 + 与SNMPv2代理的通信
Level 3 = Level 2 + 与其它SNMPv2管理站的通信
因为SNMP协议支持SNMPv1与SNMPv2的共存,所以WinSNMP实现能提供对两个版本协议的支持。
SnmpStartup函数能返回当前WinSNMP实现所能提供的最大支持层次。
3.2.2 Entity/Context转换模式(Entity/Context Translation Modes)
WinSNMP应用程序能够让WinSNMP实现把entity和context参数按不同的方式解释:
(1)按字面解释为SNMPv1代理的地址和共同体(community)字符串。
(2)解释为SNMPv2的party和context标识符(context IDs)。
(3)通过查询本地数据库将其转换为各自的SNMPv1或SNMPv2元素。
三种Entity/Context转换模式如下:
SNMPAPI_TRANSLATED = 通过本地数据库查询转换
SNMPAPI_UNTRANSLATED_V1 = 转换为地址和共同体(community)字符串
SNMPAPI_UNTRANSLATED_V2 = SNMPv2的party和context IDs.
我们可以通过SnmpStartup函数获得当前默认的entity/context转换模式,SnmpSetTranslatedMode函数可以用来设置entity/context转换模式。
当在系统中采用SNMPv1协议时,我们可以将其设置为SNMPAPI_UNTRANSLATED_V1,具体实现如下:
HSNMP_ENTITY hAgent;
HSNMP_CONTEXT hView;
LPCSTR entityName = “202.120.86.71”;
smiOCTETS contextName;
contextName.ptr = “public”;
contextName.len = lstrlen (contextName.ptr);
hAgent = SnmpStrToEntity (hSomeSessin, entityName);
hView = SnmpStrToContext (hSomeSession, const &contextName);
通过这样的设置,我们就可以在161端口通过UDP访问IP地址“202.120.86.71”上的SNMP代理了。
3.2.3 本地数据库(Local Database)
本地数据库主要存储重传模式(RetransmitMode)、重试次数(Retry)、超时(timeout)、转换模式(TranslateMode)等值。我们可以对其中的数据进行读(get)、写(set)操作。
3.2.4 会话(session)
会话是用来管理WinSNMP应用程序和WinSNMP实现之间的连接,由SnmpCreateSession(推荐)或SnmpOpen函数创建。会话是资源管理的最小单位,也是WinSNMP应用程序和WinSNMP实现之间通信管理的最小单位。一个良好的WinSNMP应用程序应该使用会话结构逻辑地管理它的操作,并将实现中的资源需求控制在最小。
调用SnmpCreateSession或SnmpOpen函数创建一个会话时,会返回一个“session id”,这是一个句柄(handle)变量,WinSNMP用它来管理自己的资源。应用程序最终应调用SnmpClose函数将会话释放。
3.2.5 异步模式(Asynchronous Model)
当代编程模式的一个很大特点就是消息驱动。WinSNMP采用了异步消息驱动模式,主要基于两个原因:
(1) 异步消息驱动模式非常适合于面向对象理论、SNMP分布式管理模型以及Windows编程、运行环境。
(2) SNMP再管理站和代理之间传送数据没有什么特别的传输机制,它基本上是基于数据报的,没有在远程实体之间建立实际通道(虚电路)。这样的事实使得WinSNMP非常适合采用异步模式。
现代的消息驱动程序必须响应各种重要事件,有些则完全依赖于异步关系。事实上,WinSNMP API中几乎所有函数都有异步成分,有些则是完全异步的。有三个非常重要的异步函数:
SnmpSendMsg (发送数据)
SnmpRecvMsg (接收数据)
SnmpRegister (注册接受trap消息)
WinSNMP的整个编程模式就是基于异步的,我们将在后面做详细介绍。
3.2.6 内存管理(Memory Management)
在Windows编程中,内存管理一向是一个令人头疼的问题。在这里,我们将对WinSNMP的内存管理做一个较为详尽的描述。
WinSNMP包括三种不同的内存“对象”:
句柄式资源 (HANDLE’d Resources)
C风格(以NULL结尾)的字符串
WinSNMP API结构类型
3.2.6.1 句柄式资源 (HANDLE’d Resources)
有五种句柄式资源的变量:
Sessions
Entities
Contexts
Protocol Data Units (PDUs)
VarBindLists (VBLs)
所有句柄对象都表示为“HSNMP_<object_tag>”的形式,它为WinSNMP实现(以DLL方式)所拥有。
3.2.6.2 C风格字符串 (C-Stytle Strings)
C风格的字符串主要用来为通用的字符串表示与Entity和对象标识符(OID)对象之间的转换提供便利。WinSNMP中使用C风格字符串的函数有:SnmpStrToEntity、SnmpEntityToStr、SnmpStrToOid、SnmpOidToStr。
C风格字符串的内存分配、管理和释放完全由应用程序负责。因此我们还需要传递“size”参数给使用它的函数。
3.2.6.3 描述符 (Descriptors)
WinSNMP中有三种结构类型:
smiOCTETS
smiOID
smiVALUE
前两种类型的定义如下:
typedef struct {
smiUINT32 len; /*unsigned long integer 类型,表示ptr中的字节数*/
smiLPBYTE ptr; /*指向包含octet string的字节数组的far指针*/
} smiOCTETS;
typedef struct {
smiUINT32 len; /**unsigned long integer 类型,表示ptr中无符号长整形的个数*/
smiLPUINT32 ptr; /*指向由OID各个标识符组成的无符号长整形数祖的far指针*/
} smiOID;
smiVALUE稍微复杂一点,它的定义如下:
typedef struct { /* smiVALUE portion of VarBind */
smiUINT32 syntax; /* Insert SNMP_SYNTAX_<type> */
union {
smiINT sNumber; /* SNMP_SYNTAX_INT
SNMP_SYNTAX_INT32 */
smiUINT32 uNumber; /* SNMP_SYNTAX_UINT32
SNMP_SYNTAX_CNTR32 SNMP_SYNTAX_GAUGE32 SNMP_SYNTAX_TIMETICKS */
smiCNTR64 hNumber; /* SNMP_SYNTAX_CNTR64 */
smiOCTETS string; /* SNMP_SYNTAX_OCTETS
SNMP_SYNTAX_BITS
SNMP_SYNTAX_OPAQUE
SNMP_SYNTAX_IPADDR
SNMP_SYNTAX_NSAPADDR */
smiOID oid; /* SNMP_SYNTAX_OID */
smiBYTE empty; /* SNMP_SYNTAX_NULL
SNMP_SYNTAX_NOSUCHOBJECT
SNMP_SYNTAX_NOSUCHINSTANCE
SNMP_SYNTAX_ENDOFMIBVIEW */
} value; /* union */
} smiVALUE;
当一个应用程序得到一个smiVALUE变量时,首先必须检查它的“syntax”成员,已决定怎样取到它的第二个成员。
当“syntax”成员变量显示“value”值是一个smiOCTETS或smiOID对象时,我们就应该考虑内存管理,约定如下:
(1) 当其作为输入参数时,应用程序负责为变长对象分配内存;
(2) 当其作为输出参数时,由WinSNMP实现(表现为DLL)为变长对象分配
内存。
3.2.6.4 内存的释放
WinSNMP应用程序必须负责释放所有通过调用WinSNMP API函数所分配的资源,主要有以下三类函数:
SnmpFree<xxx>: 释放Entity、Context、Pdu、Vbl、Descriptor
SnmpClose : 关闭会话
SnmpCleanup : 必须在程序结束之前调用,释放所有资源
应用程序推荐使用上述的顺序来释放所有的WinSNMP资源。
3.3 WinSNMP基本编程模式
WinSNMP API按照SNMP协议封装了各种操作,包括PDU、VarBindList以及协议操作的各项函数。我们可以按照SNMP协议的描述,调用WinSNMP相关函数,完成一次完整的SNMP。我们下面将以笔者完整的系统(采用SNMPv1协议)为例,具体描述WinSNMP的一般编程模式。我们分发送请求消息与接受响应消息两部分来实现。
3.3.1 WinSNMP发送请求消息
WinSNMP发送请求消息的过程可以分为四个部分,主要有:WinSNMP的初始化、PDUs的创建、发送信息以及资源的释放。
3.3.1.1 WinSNMP的初始化
(1) 调用SnmpStartup函数启动WinSNMP。
(2) 调用SnmpCreateSession函数创建一个会话session。
(3) 调用SnmpSetRetransmitMode函数设置重传模式。
(4) 调用SnmpSetRetry函数设置重传次数。
(5) 调用SnmpSetTimeout函数设置超时时间。
其中第3、4、5步都是对本地数据库的操作,完成了对WinSNMP相关参数的设置。
3.3.1.2 创建协议数据单元(PDUs)
在创建PDU之前,我们必须先创建变量绑定表(varbindlists)。
(1) 调用SnmpStrToOid函数创建读取对象的OID,例如,我们创建MIB变量ipInReceives(一个实例的OID为1.3.6.1.2.1.4.3.0),我们可以采用下面的代码:
LPCSTR name="1.3.6.1.2.1.4.3.0";
smiOID Oid;
SnmpStrToOid(name,&Oid);
(2) 调用SnmpCreateVbl函数创建变量绑定表。
HSNMP_VBL m_hvbl=SnmpCreateVbl(session,&Oid,NULL);/*NULL表示该OID的值为空*/
(3) 调用SnmpSetVb函数往变量绑定表中添加变量绑定,我们需先创
建一个OID,命名为Oid。
SnmpSetVb(m_hvbl,0,&Oid,NULL);/*0表示往变量绑定表中添加变量绑定,非0值表示修改此位置的变量绑定*/
创建好了变量绑定表后,我们调用SnmpCreatePdu函数创建协议数据单元,在这个函数中,我们必须设定error_index、error_status、request_id参数,它们都与协议中相应的量对应。
HSNMP_PDU m_hpdu=SnmpCreatePdu(session,SNMP_PDU_GET,
NULL,NULL,NULL,m_hvbl);
3.3.1.3 发送信息
我们首先调用SnmpStrToContext和SnmpStrToEntity函数创建共同体(community)字符串和代理entity,具体实现见3.2.2。
然后,我们调用SnmpSendMsg函数发送信息。
SnmpSendMsg(session,NULL,hAgent,hView,m_hpdu);
3.3.1.4 资源的释放
最后,我们应该释放所有分配的资源。
3.3.2 WinSNMP接受响应消息
还记得前面的SnmpCreateSession函数吗?它可以说是WinSNMP异步消息驱动模式的一个关键,让我们先来看看它的函数原型:
HSNMP_SESSION SnmpCreateSession(
HWND hWnd, // handle to the notification window
UINT wMsg, // window notification message number
SNMPAPI_CALLBACK fCallback, // notification callback function
LPVOID lpClientData // pointer to callback function data
);
它提供了两种方式的异步消息驱动,我们可以让WinSNMP在有响应消息到达时发送一个消息给系统,也可以让它自动调用一个函数。笔者采用了第一种方式,实现如下:
session=SnmpCreateSession(m_hWnd,wMsg,NULL,NULL);
我们可以给消息wMsg创建一个消息处理函数,在这个函数里处理消息的接收、信息的提取与处理等事务。
下面我们将具体描述WinSNMP接受响应消息的步骤。
(1) 调用SnmpRecvMsg函数接收数据
(2) 调用SnmpGetPduData函数从PDU中析取出数据,
(3) 调用SnmpCountVbl获得变量绑定列表中变量绑定的个数
(4) 调用SnmpGetVb函数取得PDU变量绑定表中每个变量绑定的OID及其对应的值,可以指明该变量绑定在变量绑定表中的位置。参考实现如下:
int nCount=SnmpCountVbl(varbindlist);
for(int index=1;i<=nCount;i++)
SnmpGetVb(varbindlist,index,&Oid,value[i]);
其中,index指定了变量绑定的位置,value[i]表示接收到的OID变量的值,是smiLPVALUE类型的,Oid表示接收到的变量绑定的OID。
对于value[i],我们可以参考3.2.6.3节,按照它的syntax成员,用select case语句,分别转换为字符串或整数类型。
(5) 调用SnmpOidToStr函数将Oid转换为字符串。并将接收到的Oid与发送数据包的各OID做比较,已决定各自值的归属。引用一段代码
if(strcmp(m_sOid[i],m_initOid[1])==0)
m_sDesr= str[i];
else if (strcmp(m_sOid[i],m_initOid[2])==0)
m_sSysOid=str[i];
else if (strcmp(m_sOid[i],m_initOid[3])==0)
m_sSysTime=str[i];
else if (strcmp(m_sOid[i],m_initOid[4])==0)
m_sName=str[i];
else if (strcmp(m_sOid[i],m_initOid[5])==0)
{m_sIpin=str[i];
m_nIpin=nIpin;}
else if(strcmp(m_sOid[i],m_initOid[6])==0)
m_sIpout=str[i];
当我们比较发送的OID与接收到的OID时,我们就知道了这个str[i]是属于哪个OID的值,应当放在哪里显示,以m_s开头的变量都代表了不同的label,这样,相应的值就在相应的字符串中显示。
通过这样的步骤,我们就完成了一个简单的SNMP网络管理程序的设计。但是,在具体的应用中,我们应该考虑更多的问题,如内存管理、错误处理等问题,还有很多问题需要我们在系统开发的过程中去发现、解决。下面,我将描述几个我在系统开发中遇到的问题,有的已经解决,有的还在探索中,希望能为同仁提供参考。
3.4 几个问题
3.4.1 读IP地址
前面讲到,IpAddress是SMIv1的一个应用数据类型,表示IP地址,它的定义为:
IpAddress::=[APPLICATION 0] IMPLICIT OCTET STRING(SIZE(4))
当我们读取一个表示IP地址的OID时,我们应该分别读出IpAddress四个字节的值,再将它们处理成我们平时见到的IP地址的形式。代码如下:
case SNMP_SYNTAX_IPADDR:
strIp.Format("%d",*m_value[i]->value.string.ptr);
strIp+=".";
strTemp.Format("%d",*(m_value[i]->value.string.ptr+1));
strIp+=strTemp;
strIp+=".";
strTemp.Format("%d",*(m_value[i]->value.string.ptr+2));
strIp+=strTemp;
strIp+=".";
strTemp.Format("%d",*(m_value[i]->value.string.ptr+3));
strIp+=strTemp;
3.4.2 GETNEXT操作的实现
GETNEXT是SNMP中用来读取表格变量的一个操作。在WinSNMP中,我们可以通过SnmpCreatePdu(session,SNMP_PDU_GETNEXT,NULL,NULL,NULL,m_hvbl)来创建一个GETNEXT操作的PDU。
关键的问题是我们如何对这个表格作遍历。(1).如何判断表格的结束;(2).在接收到响应消息时如何处理。
我们下面将以笔者系统为例,说明这些问题。我们将获得本机的路由表的一部分。先构造一个函数,代码如下:
void CSnmpManagerDlg::Next(LPTSTR Oid)
{
CString str(Oid);
if(!strcmp(str.Left(20),"1.3.6.1.2.1.4.21.1.7"))
{
file://处理接收到的数据
pSnmp.CreateVbl(Oid,NULL);
pSnmp.CreatePdu(SNMP_PDU_GETNEXT,NULL,NULL,NULL);
pSnmp.Send("127.0.0.1","public");
}
else
{
m_bNext=FALSE;
file://送去显示
}
}
我们把接收到的OID的前20位与路由next hop MIB变量("1.3.6.1.2.1.4.21.1.7")作比较,假如不等,就说明这一列已经结束。把数据送去显示或进一步处理。
我们可以为这一操作创建一个新的会话(session),或继续使用前面GET操作的会话。创建一个新的会话时,我们为这个会话指定一个消息处理函数,并在这个函数中,处理接收到的数据,以及调用Next(LPTSTR Oid)函数继续发送GETNEXT操作。
假如继续使用以前的会话,我们要依靠标志m_bNext,判断m_bNext的真假以决定是否继续发GETNEXT数据包。
void CSnmpManagerDlg::OnRecv()
{ file://接收、处理消息
if(m_bNext==TRUE)
Next(m_sOid);
}
这样,我们就完成了对表格中一列的遍历。同样,我们可以完成对整个表格的遍历,我们只需strcmp(str.Left(18),"1.3.6.1.2.1.4.21.1"),就可以获得整个表格的结束。再在Next(LPTSTR Oid)函数中用switch-case语句按各个MIB变量的值分类,就可以得到整个表格。
3.4.3 对表格变量的SET操作
在整个系统的开发中,我们曾经对SysName变量进行SET操作。证明是可行的。但当我们SET一个表格变量时,报告变量绑定(VB)错误,类型为bad value。可能有两个原因。
(1). 代理进程(Agent)不支持对这些表格变量的SET操作。(具体见RFC1212)
(2). 当SET一个表格变量时,我们应该对表格中的所有变量都赋值,并封装成一个PDU发出去。因为当我们用route add添加路由表时,必须指定所有的参数。并且,表格变量只允许添加与删除两种操作。