前兩天項目要求一個附加功能,遠端監視伺服器的運作狀況,要定期監視指定端口,指定業務,還包括伺服器的磁盤空間,記憶體,CPU使用率等等。這頭倆事還好說,ping和telnet也就搞定了,實在不行就開個socket連一下,關鍵後邊的事有點抓瞎,要是在本地,可以通過API得到系統資訊,分析磁盤空間,記憶體啥的,可是遠端得到别的機子的資訊,那不成木馬了麼。到網上一查,原來有SNMP可以幫着做這件事情。于是就了解了一下,發現這個東西挺有意思。簡單地做個筆記。
SNMP簡單網絡管理協定,是一個用來進行網絡管理的協定,之是以稱其簡單,是因為它是另一個網管協定的簡化版。并不是說簡化版就不頂用。相反,那個不簡化的網管協定号稱因為太複雜,幾乎不可能實作并推廣。不過說實在的,SNMP我都沒覺得它多簡單,它至今經過十多年的演進,廣泛用于各種網絡裝置及工業控制中,我個人感覺,随着網絡進一步發展,電器都會有IP位址的時候,SNMP或它的替代品的作用,應該是會更加重要的。
SNMP簡單說,包括了被管的裝置,代理和網管系統三個部分。被管的裝置指的就是我們要擷取資訊的裝置,這個任務裡我需要擷取它的磁盤空間。但是被管理裝置隻是産生這些資料,真正将資料報告回來的工作是由代理完成的。代理說白了就是一個運作在被管裝置上的程式。網絡程式設計的角度講它就是一個小的socket服務程式,監聽着好像是161端口。通過在被管裝置上,需要啟動snmp服務。比如在linux上,就需要執行service snmpd start來啟動snmp服務,也就是把代理跑起來。實際過程中,代理包含在各種網絡裝置上,包括交換機,路由器甚至列印機,手機,數據機上。網管系統就是我們要從被管裝置擷取資料并進行分析處理的這個程式。可以想像它運作在本地。隻要我将一個合格的請求發往正在運作的代理處,代理從請求中,了解請求的内容,并從被管裝置上用它自己的方式搜集到所需的資訊,并發送回來,這樣一個網管的簡單過程就完成了。
這樣的管理方式,還是很靈活的,隻要請求包符合協定标準,用哪種語言開發網管系統無所謂;從代理角度講,隻要能正常監聽并處理請求,代理可以任意實作。我不太清楚國際上有沒有一個統一的标準,比如讓我寫代理,有請求要查詢網絡端口的數量,我非把顯示器尺寸傳回去,不知道警察抓不抓我,至少客戶會要求退貨吧。也正是因為這種靈活,SNMP也可以支援大規模的集中管理,我同樣一個查詢存儲空間的請求,可以發給我的PC,也可以發給我的諾基亞手機,可能得到的會是存儲卡的容量吧,就看代理怎麼實作了。
至于如何表達我的請求,這裡有一個統一的标準,就是MIB,管理資訊庫的縮寫,就好像一個系統資料庫一樣,但是這個系統資料庫不實際存值,不像Windows的系統資料庫。它隻是給每一種裝置資訊編了一個号,稱為OID。這個号是以樹的形式組織的,中間用點隔開,便于管理。約定好了查的時候直接報告這個号就知道查什麼值了。這個号是大家統一的,好比如每個人都有眼睛,眼睛的好壞我們可以用視力來衡量,我們就可以這樣定:“人(1).身體(2).五官(3).眼睛(4).視力(5)”。那如果我向一個人發請求,問他的視力,那可以沖他說:那誰誰誰,我是那誰家的誰誰誰,告訴我.1.2.3.4.5.0 這裡的"1.2.3.4.5"是對象辨別符,後面再加上.0就是執行個體辨別符。
snmp為了簡單,規定的操作類型較少,基本常用的有擷取(Get),設定(Set),擷取下一個(GetNext),還有一個由代理主動發出的(Trap)用于報告事件。另外snmp已經發展到V3了,這個過程中又新增了什麼GetBulk等等。我還沒有用到,不敢亂說。回頭試試再作筆記。
在linux下,配置SNMP服務要修改/etc/snmp/snmpd.conf,也可以通過snmpconf指令配,挺友善的。
在windows下,直接在控制台,添加删除程式裡,添加Windows元件,然後選網絡管理,就裝上snmp服務了。在服務清單裡找到它,可以通過屬性進行配置。
基本上概念也就這些了。實際在開發的時候,有幾種選擇:
1.直接發UDP包,這樣最原始,最靈活,基本上完全自主。不過太麻煩。沒有非要重新造輪子玩。
2.在Windows上用winsnmp這個API。拼包的工作它就管了。
3.選擇一些開源的SNMP開發包。比如像NET-SNMP,SNMP++,還有其它的很多。.net下還有一個現成的元件OLEPRNLib,我不知道它應該算那種。
我這次主要用了winsnmp和SNMP++。
首先SnmpStartup一下。成了,就會通過5個參數傳回5個值。關于本地SNMPAPI的基本資訊,版本号和SNMP版本。頭兩個版本用團體名作認證。友善,但安全性較差。V3就比較安全了。
if (SNMPAPI_SUCCESS == SnmpStartup((smiLPUINT32)&m_nMajorVersion, (smiLPUINT32)&m_nMinorVersion, (smiLPUINT32)&m_nLevel, (smiLPUINT32)&m_nTranslateMode, (smiLPUINT32)&m_nRetransmitMode))
在我的機子上傳回的分别是:2,0,2,1,1
頭兩個數說明我的WinSnmp版本是2.0.
第3個2,說明WinSnmp支援标準的SNMPv2
第四個1,說明我的SNMP采用SNMPAPI_UNTRANSLATED_V1這種轉換模式。
最後一個1說明支援重傳。
任務比較緊,這幾個參數的具體意思,我回頭研究研究再作筆記。
m_hSession = SnmpCreateSession(NULL, 0, CSNMPManager::snmp_callbacknext, this);
建立一個會話。用這個會話,就可以發送請求了。
第一個參數是視窗句柄,用來接收消息。是這樣,具我了解,SNMP首先可以異步傳輸。也就是說發送出請求後,并不阻塞。那麼如果有回應消息了,就會給這個視窗句柄一個消息,如果在MFC下,可以做消息映射。第二個參數就是将來收到響應時,視窗會收到的對應的消息值。然後具體的響應是什麼,到消息的參數裡去看。
這裡我傳的是空,因為我測試的程式沒有視窗。
第三個參數是一個函數指針。不發消息給視窗,就可以申請回調。我現在用的就是回調。第四個參數是調回調的時候,傳給我的自定義參數,可以傳空。我現在傳this是因為我注冊的snmp_callbacknext是個靜态函數。我把執行個體指針傳進去,是為了把執行個體資訊帶進去。
接下來,就可以通過SnmpStrToEntity,得到SNMP實體了。這裡把用戶端和服務端統稱為實體。m_hsrcAgent = SnmpStrToEntity(m_hSession, m_pSrcAddr))得到實體句柄
再接下來,m_hContext = SnmpStrToContext(m_hSession, &community);得到上下文句柄。這裡的團體名,有人也叫共同體。總之是個帳戶一樣的東西。在配置SNMP的時候,要設定這個,并給它設定隻讀還是讀寫。一般安全起見,設成隻讀。已經夠用了。
現在可以建立變量綁定表了,HSNMP_VBL hVbl = SnmpCreateVbl(m_hSession, NULL, NULL);後兩個參數是OID和值。可以一次性建好變量綁定表。我這裡是先建了一個空的,一會兒再往裡填值。
這裡就用到OID了,但我們習慣的“1.3.6.1...”這種字元串形式,需要通過SnmpStrToOid轉成smiOID類型。這是為了API拼SNMP包友善,定義的一個新類型。因為最終的SNMP請求的UDP包,是位元組形式的。是要符合BER編碼規範的。所謂BER編碼,就是将一個資料封裝成:标志,長度,内容這樣一個資料段。比如規定整形标志是1,再假設1234的十六進制是0x12,0x34。那麼我要發一個1234出去就要這樣編碼:01 02 12 34。
将變量加入到變量綁定表:SnmpSetVb(hVbl, 0, &oid, NULL),第二個參數是索引,第三個參數是變量,第四個參數是值。
hSendPDU = SnmpCreatePdu(m_hSession, SNMP_PDU_GETNEXT, 0, 0, 0, hVbl);構造實際的UDP包。PDU:協定資料單元。第二個參數說明請求的類型:可以傳一開始提到的那幾種類型,snmp.h裡面有定義:
#define SNMP_PDU_GET (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x0)
#define SNMP_PDU_GETNEXT (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x1)
#define SNMP_PDU_RESPONSE (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x2)
#define SNMP_PDU_SET (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x3)
#define SNMP_PDU_V1TRAP (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x4)
#define SNMP_PDU_GETBULK (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x5)
#define SNMP_PDU_INFORM (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x6)
#define SNMP_PDU_TRAP (ASN_CONTEXT | ASN_CONSTRUCTOR | 0x7)
一切就緒,最後就要發包了:
SnmpSendMsg(m_hSession, m_hsrcAgent, m_hdstAgent, m_hContext, hSendPDU)
這裡面幾個參數都是前面準備好的,這個方法就沒啥可說的了。正确調用後,就可以等着回應了。
在回調函數SNMPAPI_STATUS CALLBACK CSNMPManager::snmp_callback(HSNMP_SESSION hSession,
HWND hWnd,
UINT wMsg,
WPARAM wParam,
LPARAM lParam,
LPVOID lpClientData
)
裡,我們可以得到一個hSession.這個是代理那邊扔過來的。和我們的hSession沒啥關系。
之後我們通過這個hSession,調用SnmpRecvMsg(hSession, &hsrcAgent, &hdstAgent, &hContext, &hRecvPDU)),得回實體,上下文,以及響應的PDU。
得到響應的PDU,比較激動的時刻就到了,因為我們要開始拆響應包了,如果從裡面拆出了我們想查的值,那就說明OK了,于是調用:
SnmpGetPduData(hRecvPDU, NULL, NULL, (smiLPINT)&nErrStatus, (smiLPINT)&nErrIndex, &hRecvVbl)
暈,得到了變量綁定表,那就是說還有一層,不過保佑啊,可千萬别一層層剝下來,最後得到4個位元組值,轉換編碼一瞅,倆字“撓撓”,呵呵。
int nVblCnt = SnmpCountVbl(hRecvVbl);
得一下變量綁定表的長度。
smiOID oid;
smiVALUE val;
for (int i=1; i<=nVblCnt; i++)
{
SnmpGetVb(hRecvVbl, i, &oid, &val);
parseVb(val);
}
循環,得一下每一個變量。得到OID的值,可以用SnmpOidToStr得回成字元串的形式看一下,是哪個OID的值。
然後值的類型是smiOID,很像COM裡的variant_t。一個類型值拖着一個大的聯合體。
有:
#define SNMP_SYNTAX_INT (ASN_UNIVERSAL | ASN_PRIMITIVE | 0x02)
#define SNMP_SYNTAX_BITS (ASN_UNIVERSAL | ASN_PRIMITIVE | 0x03)
#define SNMP_SYNTAX_OCTETS (ASN_UNIVERSAL | ASN_PRIMITIVE | 0x04)
#define SNMP_SYNTAX_NULL (ASN_UNIVERSAL | ASN_PRIMITIVE | 0x05)
#define SNMP_SYNTAX_OID (ASN_UNIVERSAL | ASN_PRIMITIVE | 0x06)
#define SNMP_SYNTAX_INT32 SNMP_SYNTAX_INT
#define SNMP_SYNTAX_IPADDR (ASN_APPLICATION | ASN_PRIMITIVE | 0x00)
#define SNMP_SYNTAX_CNTR32 (ASN_APPLICATION | ASN_PRIMITIVE | 0x01)
#define SNMP_SYNTAX_GAUGE32 (ASN_APPLICATION | ASN_PRIMITIVE | 0x02)
#define SNMP_SYNTAX_TIMETICKS (ASN_APPLICATION | ASN_PRIMITIVE | 0x03)
#define SNMP_SYNTAX_OPAQUE (ASN_APPLICATION | ASN_PRIMITIVE | 0x04)
#define SNMP_SYNTAX_NSAPADDR (ASN_APPLICATION | ASN_PRIMITIVE | 0x05)
#define SNMP_SYNTAX_CNTR64 (ASN_APPLICATION | ASN_PRIMITIVE | 0x06)
#define SNMP_SYNTAX_UINT32 (ASN_APPLICATION | ASN_PRIMITIVE | 0x07)
這幾種值類型。常用的有SNMP_SYNTAX_UINT32、SNMP_SYNTAX_OCTETS、SNMP_SYNTAX_OID這幾種吧,用法都差不多。沒啥問題。
子樹,下面有
hrStorage
GROUP
1.3.6.1.2.1.25.2
iso(1). org(3). dod(6). internet(1). mgmt(2). mib-2(1). host(25). hrStorage(2)
<a>hrMemorySize</a>
SCALAR
read-only
KBytes
1.3.6.1.2.1.25.2.2.0
<a>hrStorageTable</a>
TABLE
not-accessible
SEQUENCE OF
1.3.6.1.2.1.25.2.3
<a>hrStorageEntry</a>
ENTRY
HrStorageEntry
1.3.6.1.2.1.25.2.3.1
<a>hrStorageIndex</a>
TABULAR
Integer32 ( 1..2147483647 )
1.3.6.1.2.1.25.2.3.1.1
<a>hrStorageType</a>
AutonomousType
1.3.6.1.2.1.25.2.3.1.2
<a>hrStorageDescr</a>
DisplayString
1.3.6.1.2.1.25.2.3.1.3
<a>hrStorageAllocationUnits</a>
1.3.6.1.2.1.25.2.3.1.4
<a>hrStorageSize</a>
read-write
Integer32 ( 0..2147483647 )
1.3.6.1.2.1.25.2.3.1.5
<a>hrStorageUsed</a>
1.3.6.1.2.1.25.2.3.1.6
<a>hrStorageAllocationFailures</a>
Counter32
1.3.6.1.2.1.25.2.3.1.7
<a>hrDeviceTypes</a>
OBJ ID
1.3.6.1.2.1.25.3.1
這裡面有一個表,表下面有行,一個分區是一行,每行有索引,有分區描述(一般是卷标),有分區尺寸,有已用空間。
至此,眼前的任務,算是應付下來了。這個過程中,還會牽扯到傳輸設定,表的操作,GetNext操作的實作等内容,今天實在來不及全記下來了,我稍後再補充完整。