在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添加路由表時,必須指定所有的參數。並且,表格變量只允許添加與刪除兩種操作。