背景
如上一篇博客所说,因为项目上需要做一个snmp通信的上位机,之前一直在编译第三方的库,但是最后选择了Windows上的官方的库WINSNMP。网上查了老半天,百度上全是相同的一篇博客,然后google了一下,收获也不是很大,然后就只有自己啃一啃MS的官方文档。由于本人也是初次接触snmp,本文只是简单的说明一下WINSNMP的使用,并不对API函数进行讲解,如果文章有不足之处还望指出。
MS官方说明文档地址:https://docs.microsoft.com/zh-cn/windows/desktop/SNMP/winsnmp-api
正文
通信的初始化
使用WINSNMP第一步首先需要调用SnmpStartup()函数程序才能正常调用其他的WINSNMP函数,该函数的参数主要是用于返回一些参数。在调用该函数过后,就可以创建snmp通信的会话(session)了,一个程序可以创建多个session,这个暂时没有研究,在创建会话时,可以选择传入回调函数用以处理事件,也可以通过该函数的第二个参数wMsg来使用Windows的消息处理机制,我选择了前者传入回调函数:
void SNMP::startSNMP(void)
{
smiUINT32 major, minor;
smiUINT32 level;
smiUINT32 tslMode;
smiUINT32 tsmMode;
if (SnmpStartup(&major, &minor, &level, &tslMode, &tsmMode) == SNMPAPI_FAILURE) {
qDebug() << "SNMP service open failed.";
return ;
}
qDebug("Version:v%ld.%ld, level:%ld, translate mode:%ld, transmit mode:%ld\n", major, minor, level, tslMode, tsmMode);
m_hSession = SnmpCreateSession(Q_NULLPTR, 0, sessionCallback, this);
if (m_hSession == Q_NULLPTR) {
qDebug() << "SNMP create session failed.";
return ;
}
}
在启动snmp功能和建立好一个会话过后,我们要进行数据交互首先需要创建一个交互的实体(entity)和上下文(context),其实简单一点就是设置目标的IP地址和community,这种说话可能不太准确。
void SNMP::setEntity(const QString &entity)
{
m_hEntity = SnmpStrToEntity(m_hSession, entity.toLocal8Bit().constData());
}
void SNMP::setCommunity(const QString &community)
{
smiOCTETS contextName;
contextName.ptr = reinterpret_cast<smiLPBYTE>(community.toLocal8Bit().data());
contextName.len = static_cast<quint32>(community.size());
m_hContext = SnmpStrToContext(m_hSession, &contextName);
}
然后还可以设置重传模式和参数还有传输协议的模式,这里不着重去讲。
数据读写
数据的读写主要是通过变量绑定列表(VBL)和协议数据单元PDU实现。
写入
数据主要是放在VBL中,可以通过SnmpCreateVbl()函数创建一个新的VBL用于保存数据,如果在调用该函数时就传入数据,那么数据将保存在VBL的第一位,然后可以通过SnmpSetVb()将多个输入放入一个VBL中,当要传输的数据都放入到VBL中后,就可以通过SnmpCreatePdu()函数又放入PDU中,最后通过SnmpSendMsg()发送出去:
void SNMP::sendData(long pduType, quint32 dataType, const QString &oid, const QVariant &data)
{
HSNMP_VBL vbl;
vbl = SnmpCreateVbl(m_hSession, Q_NULLPTR, Q_NULLPTR);
/* package value */
smiVALUE value;
memset(&value, 0, sizeof(value));
value.syntax = dataType;
switch (dataType) {
case SNMP_SYNTAX_INT: // int number
value.value.sNumber = data.toInt();
break;
case SNMP_SYNTAX_UINT32: // unsigned 32bit number
value.value.uNumber = data.toUInt();
break;
case SNMP_SYNTAX_CNTR64: // 64bit number
value.value.hNumber.hipart = (data.toULongLong() >> 32);
value.value.hNumber.lopart = (data.toULongLong() & 0xFFFFFFFF);
break;
case SNMP_SYNTAX_OCTETS: // string
value.value.string.ptr = reinterpret_cast<smiLPBYTE>(data.toByteArray().data());
value.value.string.len = static_cast<quint32>(data.toByteArray().size());
break;
default:
break;
}
smiOID Oid;
SnmpStrToOid(oid.toLocal8Bit().constData(), &Oid);
SnmpSetVb(vbl, 0, &Oid, &value);
HSNMP_PDU pdu = SnmpCreatePdu(m_hSession, pduType, 0, 0, 0, vbl);
SnmpSendMsg(m_hSession, Q_NULLPTR, m_hEntity, m_hContext, pdu);
SnmpFreePdu(pdu);
SnmpFreeVbl(vbl);
}
接收
由于采用的异步回调机制,当接收到数据时,首先会调用回调函数,然后在回调函数中我这只是做了一个信号触发,不过这里并没有判断回调事件是否是接收数据,所以可能存在问题:
static SNMPAPI_STATUS CALLBACK sessionCallback(HSNMP_SESSION hSession, HWND hWnd, UINT wMsg,
WPARAM wParam, LPARAM lParam, LPVOID lpClientData)
{
Q_UNUSED(hSession);
Q_UNUSED(hWnd);
Q_UNUSED(wMsg);
Q_UNUSED(wParam);
Q_UNUSED(lParam);
SNMP *snmp = static_cast<SNMP *>(lpClientData);
emit snmp->readyRead();
return SNMPAPI_SUCCESS;
}
在槽函数中,只需要调用Recv()函数就可以返回当前读取到的数据了:
void MainWindow::test(void)
{
qDebug() << "receive data: " << snmp->recvData().toByteArray() << "\n";
snmp->getNext();
}
这里需要注意的是,有些时候我们需要获取一堆的数据,这时候我们可以通过设置PDU为SNMP_PDU_GETNEXT实现:
void SNMP::traverse(const QString &sOid, const QString &eOid)
{
if (!eOid.isEmpty() && eOid.compare(sOid) < 0) {
qDebug() << "SNMP end oid is invaild.";
return ;
}
m_sTraSOid = sOid;
m_sTraEOid = eOid;
sendData(SNMP_PDU_GETNEXT, SNMP_SYNTAX_NULL, sOid, QVariant(QVariant::Invalid));
m_bNext = true;
}
当接收到数据时,就调用getNext()用以获取下一个oid的数据:
void SNMP::getNext(void)
{
if (m_bNext && (m_sTraSOid.compare(m_sTraEOid) > 0)) {
m_bNext = false;
return ;
}
if (m_bNext) {
sendData(SNMP_PDU_GETNEXT, SNMP_SYNTAX_NULL, m_sTraSOid, QVariant(QVariant::Invalid));
}
}
在接收函数中,需要做的就是接收PDU,从PDU中获取VBL,然后从VBL中再获取到数据,其中VBL中的smiVALUE
的syntax值为数据类型,可以根据它来判断VBL中什么数据是我们想要的:
QVariant SNMP::recvData(void)
{
HSNMP_PDU pdu;
if (SnmpRecvMsg(m_hSession, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, &pdu)) {
qDebug() << "SNMP receive data failed.";
return QVariant(QVariant::Invalid);
}
smiINT pduType = 0;
HSNMP_VBL vbl;
SnmpGetPduData(pdu, &pduType, Q_NULLPTR, Q_NULLPTR, Q_NULLPTR, &vbl);
// qDebug() << "Num: " << SnmpCountVbl(vbl);
smiOID oid;
smiVALUE value;
SnmpGetVb(vbl, 1, &oid, &value);
char strOid[SNMP_MAX_OID_SIZE] = { 0 };
SnmpOidToStr(&oid, 256, strOid);
if (m_bNext) {
m_sTraSOid = QByteArray(strOid);
// qDebug() << "OID: " << m_sTraSOid;
}
QVariant data;
switch (value.syntax) {
case SNMP_SYNTAX_NULL:
case SNMP_SYNTAX_NOSUCHOBJECT:
case SNMP_SYNTAX_NOSUCHINSTANCE:
case SNMP_SYNTAX_ENDOFMIBVIEW:
data = QVariant(QVariant::Invalid);
break;
case SNMP_SYNTAX_OCTETS:
case SNMP_SYNTAX_BITS:
case SNMP_SYNTAX_OPAQUE:
case SNMP_SYNTAX_IPADDR:
case SNMP_SYNTAX_NSAPADDR:
data = QVariant(QByteArray(reinterpret_cast<const char *>(value.value.string.ptr),
static_cast<int>(value.value.string.len)));
break;
case SNMP_SYNTAX_INT:
case SNMP_SYNTAX_UINT32:
case SNMP_SYNTAX_CNTR32:
case SNMP_SYNTAX_GAUGE32:
case SNMP_SYNTAX_TIMETICKS:
case SNMP_SYNTAX_CNTR64:
data = QVariant(static_cast<qulonglong>(value.value.sNumber));
break;
default:
break;
}
SnmpFreeDescriptor(value.syntax, &value.value.string);
SnmpFreePdu(pdu);
SnmpFreeVbl(vbl);
return data;
}
这里我只是简单的将一些数据归类到了一起,可能不太准确,有需要的同学可以自己再细分。
Bulk
要使用SNMP_PDU_GETBULK必须设置SnmpSetTranslateMode(SNMPAPI_UNTRANSLATED_V2);因为Bulk是在SNMP v2中加入,如果使用传送模式为v2将只会获取到第一个Oid的内容。