一、前言
之前有在弄监控服务器这块的工作,今天来整体总结下。因为有些服务器(路由器、交换机等都是基于snmp协议的)必须使用snmp协议去监控采集和接收信息,所以必须去了解snmp相关内容,以及如何在基于java上开发。关于了解snmp相关内容,必看《SNMP简单网络管理协议》这本书里面介绍的很详细,另外推荐这位前辈的博文写的很到位《snmp学习总结》。关于snmp4j的介绍也可以看看前面这位前辈关于《snmp学习总结》的最后一篇博文《snmp4j介绍》。当然本篇主要记录如何基于Java如何使用snmp4j去开发实现监控与采集,下面我们直接结合源码以及实例讲解:
二、针对源码进行分析:
1、核心对象SNMP的初始化。
源码中有四种初始化方法及四个构造函数,其实都大同小异:参数少的就必须后续添加,参数多的必须提前初始化。
我们先看看第一个无参构造函数,源码很简单,但是注释很多,所以看源码必须要先看注释。
/**
* Creates a {@code Snmp} instance that uses a
* {@code MessageDispatcherImpl} with no message processing
* models and no security protols (by default). You will have to add
* those by calling the appropriate methods on
* {@link #getMessageDispatcher()}.
* <p>
* At least one transport mapping has to be added before {@link #listen()}
* is called in order to be able to send and receive SNMP messages.
* <p>
* To initialize a {@code Snmp} instance created with this constructor
* follow this sample code:
* <pre>
14 * Transport transport = ...;
15 * Snmp snmp = new Snmp();
16 * SecurityProtocols.getInstance().addDefaultProtocols();
17 * MessageDispatcher disp = snmp.getMessageDispatcher();
18 * disp.addMessageProcessingModel(new MPv1());
19 * disp.addMessageProcessingModel(new MPv2c());
20 * snmp.addTransportMapping(transport);
21 * OctetString localEngineID = new OctetString(
22 * MPv3.createLocalEngineID());
23 * // For command generators, you may use the following code to avoid
24 * // engine ID *es:
25 * // MPv3.createLocalEngineID(
26 * // new OctetString("MyUniqueID"+System.currentTimeMillis())));
27 * USM usm = new USM(SecurityProtocols.getInstance(), localEngineID, 0);
28 * disp.addMessageProcessingModel(new MPv3(usm));
29 * snmp.listen();
* </pre>
*/
public Snmp() {
this.messageDispatcher = new MessageDispatcherImpl();
if (SNMP4JSettings.getSnmp4jStatistics() != SNMP4JSettings.Snmp4jStatistics.none) {
counterSupport = CounterSupport.getInstance();
}
}
从上面注释中可以看出要初始化snmp需要设置messageDispatcher里面的参数和TransportMapping参数,如果没有设置好这个两个参数,发送报文时会报错(见下面案例).所以我们可以直接使用第三个构造函数。
接下来我们来看第二个构造函数Snmp(TransportMapping<? extends Address> transportMapping):
/**
* Creates a <code>Snmp</code> instance that uses a
* <code>MessageDispatcherImpl</code> with all supported message processing
* models and the default security protols for dispatching.
* <p>
* To initialize a <code>Snmp</code> instance created with this constructor
* follow this sample code:
* <pre>
* Transport transport = ...;
* Snmp snmp = new Snmp(transport);
* OctetString localEngineID =
* new OctetString(snmp.getMPv3().getLocalEngineID());
* USM usm = new USM(SecurityProtocols.getInstance(), localEngineID, 0);
* SecurityModels.getInstance().addSecurityModel(usm);
* snmp.listen();
* </pre>
*
* @param transportMapping TransportMapping
* the initial <code>TransportMapping</code>. You can add more or remove
* the same later.
*/
public Snmp(TransportMapping<? extends Address> transportMapping) {
this();
initMessageDispatcher();
if (transportMapping != null) {
addTransportMapping(transportMapping);
}
}
protected final void initMessageDispatcher() {
this.messageDispatcher.addCommandResponder(this);
this.messageDispatcher.addMessageProcessingModel(new MPv2c());
this.messageDispatcher.addMessageProcessingModel(new MPv1());
this.messageDispatcher.addMessageProcessingModel(new MPv3());
SecurityProtocols.getInstance().addDefaultProtocols();
}
从源码中可以看到它帮我们设置了messageDispatcher里面的参数,只要我们提供TransportMapping参数即可。第四个构造函数Snmp(MessageDispatcher messageDispatcher)其实跟第一个类似同样需要提供两个参数,所以第三个和第四个就列出来了。其中涉及到接口有MessageDispatcher接口、MessageProcessingModel接口,涉及到的类有MPv1、MPv2和MPv3分别对应snmp版本v1、v2c和v3。
/**
* @description MessageDispatcher接口定义了处理传入的SNMP消息并将其分派到感兴趣的CommandResponder实例的实例的公共服务。它还提供了一个发送出去的SNMP消息的服务。
*/
public interface MessageDispatcher extends TransportListener {}
/**
* @description MessageProcessingModel 接口为所有SNMP消息处理模型定义了通用方法。
*/
public interface MessageProcessingModel {}
/**
* @description TransportMapping定义了SNMP传输映射的公共接口。传输映射只能支持单个传输协议。
*/
public interface TransportMapping<A extends Address> {}
2、核心对象Target
我们先看下Target对象下的继承关系。其中主要用到的子对象是CommunityTarget和UserTarget,CommunityTarget用于SNMPv1和SNMPv2c这两个版本,而UserTarget用于SNMPV3版本。
在初始化CommunityTarget时默认使用的是snmpv1(适用于snmpv2).
/**
* Default constructor.
*/
public CommunityTarget() {
setVersion(SnmpConstants.version1);
setSecurityLevel(SecurityLevel.NOAUTH_NOPRIV);
setSecurityModel(SecurityModel.SECURITY_MODEL_SNMPv1);
}
3、核心对象PDU
跟Target一样,针对snmp的不同版本是使用不同的子类去实现。PDUv1用于SNMPv1和SNMPv2c这两个版本,而ScopedPDU用于SNMPV3版本。
三、案例分析:
1、snmpGet功能测试:
第一步:要初始snmp并开启监听。其中有点不同的是,为了支持snmpv3版本的处理需要增加用户并设置安全名称和加密算法。(关于那些静态变量的值,最好放到配置文件中显得灵活点)。
另外再说明下:snmp是基于udp协议发送报文的,且snmp端口默认为161。
public class SnmpUtil {
private static Logger log = LoggerFactory.getLogger(SnmpUtil.class);
public static Snmp snmp = null;
private static String community = "public";
private static String ipAddress = "udp:10.10.112.105/"; /**
* @description 初始化snmp
* @author YuanFY
* @date 2017年12月16日 上午10:28:01
* @version 1.0
* @throws IOException
*/
public static void initSnmp() throws IOException{
//1、初始化多线程消息转发类
MessageDispatcher messageDispatcher = new MessageDispatcherImpl();
//其中要增加三种处理模型。如果snmp初始化使用的是Snmp(TransportMapping<? extends Address> transportMapping) ,就不需要增加
messageDispatcher.addMessageProcessingModel(new MPv1());
messageDispatcher.addMessageProcessingModel(new MPv2c());
//当要支持snmpV3版本时,需要配置user
OctetString localEngineID = new OctetString(MPv3.createLocalEngineID());
USM usm = new USM(SecurityProtocols.getInstance().addDefaultProtocols(), localEngineID, 0);
UsmUser user = new UsmUser(new OctetString("SNMPV3"), AuthSHA.ID, new OctetString("authPassword"),
PrivAES128.ID, new OctetString("privPassword"));
usm.addUser(user.getSecurityName(), user);
messageDispatcher.addMessageProcessingModel(new MPv3(usm));
//2、创建transportMapping
UdpAddress updAddr = (UdpAddress) GenericAddress.parse("udp:10.10.112.177/161");
TransportMapping<?> transportMapping = new DefaultUdpTransportMapping(updAddr);
//3、正式创建snmp
snmp = new Snmp(messageDispatcher, transportMapping);
//开启监听
snmp.listen();
}
}
其中要注意的是UdpAddress updAddr = (UdpAddress) GenericAddress.parse("udp:10.10.112.177/161"); 只能指定本机ip,要么不要设置地址。请看DefaultUdpTransportMapping的源码
/**
* Creates a UDP transport on the specified address. The address will not be
* reused if it is currently in timeout state (TIME_WAIT).
*
* @param udpAddress
* the local address for sending and receiving of UDP messages.
* @throws IOException
* if socket binding fails.
*/
public DefaultUdpTransportMapping(UdpAddress udpAddress) throws IOException {
super(udpAddress);
socket = new DatagramSocket(udpAddress.getPort(),
udpAddress.getInetAddress());
}
第二步: 根据snmp版本创建Target对象,其中针对snmpV3版本需要设置安全级别和安全名称,其中安全名称是创建snmp指定user设置的new OctetString("SNMPV3"),针对snmpv1和snmpv2c需要设置团体名。另外必须设置ipAddress,且对应的主机要配置snmp否则获取不到值。如下:
private static Target createTarget(int version, int port) {
Target target = null;
if (!(version == SnmpConstants.version3 || version == SnmpConstants.version2c || version == SnmpConstants.version1)) {
log.error("参数version异常");
return target;
}
if (version == SnmpConstants.version3) {
target = new UserTarget();
//snmpV3需要设置安全级别和安全名称,其中安全名称是创建snmp指定user设置的new OctetString("SNMPV3")
target.setSecurityLevel(SecurityLevel.AUTH_PRIV);
target.setSecurityName(new OctetString("SNMPV3"));
} else {
//snmpV1和snmpV2需要指定团体名名称
target = new CommunityTarget();
((CommunityTarget)target).setCommunity(new OctetString(community));
if (version == SnmpConstants.version2c) {
target.setSecurityModel(SecurityModel.SECURITY_MODEL_SNMPv2c);
}
}
target.setVersion(version);
//必须指定,没有设置就会报错。
target.setAddress(GenericAddress.parse(ipAddress+port));
target.setRetries(5);
target.setTimeout(3000);
return target;
}
第三步:创建报文。其中要注意的是pdu可以设置类型,如果想要用snmpget方法,就设置PDU.GET.
private static PDU createPDU(int version, int type, String oid){
PDU pdu = null;
if (version == SnmpConstants.version3) {
pdu = new ScopedPDU();
}else {
pdu = new PDUv1();
}
pdu.setType(type);
//可以添加多个变量oid
pdu.add(new VariableBinding(new OID(oid)));
return pdu;
}
最后一步发送报文也是最重要的一步,需要前面三步的支撑才能进行。如下:
public static void snmpGet(String oid){
try {
//1、初始化snmp,并开启监听
initSnmp();
//2、创建目标对象
Target target = createTarget(SnmpConstants.version2c, SnmpConstants.DEFAULT_COMMAND_RESPONDER_PORT);
//3、创建报文
PDU pdu = createPDU(SnmpConstants.version2c, PDU.GET, oid);
System.out.println("-------> 发送PDU <-------");
//4、发送报文,并获取返回结果
ResponseEvent responseEvent = snmp.send(pdu, target);
PDU response = responseEvent.getResponse();
System.out.println("返回结果:" + response);
}
catch (IOException e) {
e.printStackTrace();
}
}
测试如下:
public static void main(String[] args) {
snmpGet("1.3.6.1.2.1.1.1.0");
}
output:
-------> 发送PDU <-------
返回结果:RESPONSE[requestID=1344419162, errorStatus=Success(0), errorIndex=0, VBS[1.3.6.1.2.1.1.1.0 = Linux localhost.localdomain 3.10.0-327.36.2.el7.x86_64 #1 SMP Mon Oct 10 23:08:37 UTC 2016 x86_64]]
从中可以得知,snmpget是可以根据指定的oid获取其对应的内容的。
2、SNMPWalk功能测试
查看了下PDU的源码,发现没有对应snmpwalk的类型,所以使用getNext类型来实现snmpwalk功能。
public static void snmpWalk(String oid) {
try {
//1、初始化snmp,并开启监听
initSnmp();
//2、创建目标对象
Target target = createTarget(SnmpConstants.version2c, SnmpConstants.DEFAULT_COMMAND_RESPONDER_PORT);
//3、创建报文
PDU pdu = createPDU(SnmpConstants.version2c, PDU.GETNEXT, oid);
System.out.println("-------> 发送PDU <-------");
//4、发送报文,并获取返回结果
boolean matched = true;
while (matched) {
ResponseEvent responseEvent = snmp.send(pdu, target);
if (responseEvent == null || responseEvent.getResponse() == null) {
break;
}
PDU response = responseEvent.getResponse();
String nextOid = null;
Vector<? extends VariableBinding> variableBindings = response.getVariableBindings();
for (int i = 0; i < variableBindings.size(); i++) {
VariableBinding variableBinding = variableBindings.elementAt(i);
Variable variable = variableBinding.getVariable();
nextOid = variableBinding.getOid().toDottedString();
//如果不是这个节点下的oid则终止遍历,否则会输出很多,直到整个遍历完。
if (!nextOid.startsWith(oid)) {
matched = false;
break;
}
//System.out.println(variable);
}
if (!matched) {
break;
}
pdu.clear();
pdu.add(new VariableBinding(new OID(nextOid)));
System.out.println("返回结果:" + response);
}
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
测试如下:
public static void main(String[] args) {
//snmpGet("1.3.6.1.2.1.1.1.0");
snmpWalk("1.3.6.1.2.1.25.3.3.1.2");//CPU的当前负载,N个核就有N个负载 }
-------> 发送PDU <-------
返回结果:RESPONSE[requestID=1014693266, errorStatus=Success(0), errorIndex=0, VBS[1.3.6.1.2.1.25.3.3.1.2.196608 = 1]]
返回结果:RESPONSE[requestID=1014693268, errorStatus=Success(0), errorIndex=0, VBS[1.3.6.1.2.1.25.3.3.1.2.196609 = 0]]
用命令获取的结果是跟代码输出的结果是一样的,如下:
3、前面两个案例都是跟采集有关,接下来介绍如何监控接收服务器发过来的故障然后提示个用户,这就需要用到snmptrap了。接下来我们直接看案例:
处理流程:
1、必须实现CommandResponder接口
2、初始化snmp并开启监听。这步跟上面初始化一样,只是面对并发的情况使用MultiThreadedMessageDispatcher进行信息处理。
3、将当前实现CommandResponder的对象添加至snmp的addCommandResponder才能接收到信息。
4、处理接收到信息,通知用户。
package com.yuanfy.study.snmp; import java.io.IOException; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.CommandResponder;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.MessageDispatcher;
import org.snmp4j.MessageDispatcherImpl;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping;
import org.snmp4j.mp.MPv1;
import org.snmp4j.mp.MPv2c;
import org.snmp4j.mp.MPv3;
import org.snmp4j.security.AuthSHA;
import org.snmp4j.security.PrivAES128;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.security.UsmUser;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.snmp4j.util.MultiThreadedMessageDispatcher;
import org.snmp4j.util.ThreadPool; public class SnmpTrapHandler implements CommandResponder{
private static Logger log = LoggerFactory.getLogger(SnmpTrapHandler.class);
private static int threadNum = 200;
private static String ipAddress = "udp:10.10.112.177/162";
private Snmp snmp = null;
public void init(){
//1、初始化多线程消息转发类
ThreadPool threadPool = ThreadPool.create("SnmpTrap", threadNum);
MessageDispatcher messageDispatcher = new MultiThreadedMessageDispatcher(threadPool, new MessageDispatcherImpl());
//其中要增加三种处理模型。如果snmp初始化使用的是Snmp(TransportMapping<? extends Address> transportMapping) ,就不需要增加
messageDispatcher.addMessageProcessingModel(new MPv1());
messageDispatcher.addMessageProcessingModel(new MPv2c());
OctetString localEngineID = new OctetString(MPv3.createLocalEngineID());
USM usm = new USM(SecurityProtocols.getInstance().addDefaultProtocols(), localEngineID, 0);
UsmUser user = new UsmUser(new OctetString("SNMPV3"), AuthSHA.ID, new OctetString("authPassword"),
PrivAES128.ID, new OctetString("privPassword"));
usm.addUser(user.getSecurityName(), user);
messageDispatcher.addMessageProcessingModel(new MPv3(usm));
//2、创建transportMapping
TransportMapping<?> transportMapping = null;
try {
UdpAddress updAddr = (UdpAddress) GenericAddress.parse(System.getProperty("snmp4j.listenAddress", ipAddress));
transportMapping = new DefaultUdpTransportMapping(updAddr);
//3、正式创建snmp
snmp = new Snmp(messageDispatcher, transportMapping);
//开启监听
snmp.listen();
} catch (IOException e) {
log.error("初始化transportMapping失败:", e.getMessage());
e.printStackTrace();
}
} public void start() {
init();
//一定要将当前对象添加至commandResponderListeners中
snmp.addCommandResponder(this);
System.out.println("开始监听trap信息:");
}
/**
* 处理信息方法
*/
@Override
public void processPdu(CommandResponderEvent event) {
String version = null ;
String community = null;
if (event.getPDU().getType() == PDU.V1TRAP) {
version = "v1";
community = new String(event.getSecurityName());
} else if (event.getPDU().getType() == PDU.TRAP){
if (event.getSecurityModel() == 2) {
version = "v2";
community = new String(event.getSecurityName());
}else {
version = "v3";
}
}
System.out.println("接收到的trap信息:[发送来源="+event.getPeerAddress()+",snmp版本="+version+",团体名="+community+", 携带的变量="+event.getPDU().getVariableBindings()+"]");
}
public static void main(String[] args) {
SnmpTrapHandler handler = new SnmpTrapHandler();
handler.start();
}
}
测试如下: