简单网络管理协议(SNMP)是TCP/IP协议簇的一个应用层协议。在1988年被制定,并被Internet体系结构委员会(IAB)采纳作为一个短期的网络管理解决方案;由于SNMP的简单性,在Internet时代得到了蓬勃的发展,1992年发布了SNMPv2版本,以增强SNMPv1的安全性和功能。现在,已经有了SNMPv3版本。
SNMP版本
SNMPv1
SNMPv1 是 SNMP 协议的最初版本,提供最小限度的网络管理功能。SNMPv1 的 SMI 和 MIB 都比较简单,且存在较多安全缺陷。SNMPv1 采用团体名认证。团体名的作用类似于密码,用来限制NMS对Agent 的访问。如果 SNMP 报文携带的团体名没有得到 NMS/Agent 的认可,该报文将被丢弃。SNMPV1 是一种简单的请求/响应协议。网络管理系统发出一个请求,管理器则返回一个响应。这一行为的实现是通过使用四种协议操作中的其中任一种完成的。这四种操作分别是 GET、GETNEXT、SET 和 TRAP。NMS 通过 GET 操作,从 SNMP 代理处得到一个或 更多的对象(实例)值。如果代理处不能提供请求列表中所有的对象(实例)值,它也就不提供任何值。 NMS 使用 GETNEXT 操作请求代理从请求列表或对象列表中取出下一 个对象实例值。NMS 通过 SET 操作向 SNMP 代理发送命令,要求对对象值重新配置。SNMP 代理通过 TRAP 操作不定时的通知 NMS 所发生的特定事件 SNMP 是一种应用程序协议。
SNMPv2
SNMPv2c 也采用团体名认证。在兼容 SNMPv1 的同时又扩充了 SNMPv1 的功能:它提 供了更多的操作类型(GetBulk–批量获取操作等);支持更多的数据类型(Counter32等),提供了更丰富的错误代码,能够更细致地区分错误。
SNMPV1 中的 GET、GETNEXT 及 SET 操作同样适用于 SNMPV2,只是 SNMPV2 添加和增强了有关协议操作。例如 SNMPV2 中的 TRAP 操作,不但具备 SNMPV1 中 TRAP 的相同功能,而且它采用了一种不同的消息格式,它用于替换 SNMPV1 中的 TRAP。
SNMPV2 中还定了两种新操作,即 GET BULK 和 INFORM。NMS 通过 GET BULK 操作能有效地获取大块数据,如对象列表中的多行。请求多少数据 GETBULK 返回一个包含尽可能多的请求数据的应答消息。INFORM 操作使一个NMS 能发送 TRAP 给另一个 NMS 并收到回复。SNMPV2中,如果回复 GET BULK 操作的 SNMP 代理不能提供请求表中所有变量值,那么SNMP代理只提供部分结果。
SNMPV3
SNMPv3主要在安全性方面进行了增强,它采用了 USM(基于用户的安全控制模型)和 VACM(基于视图的访问控制模型) 技术。USM 提供了认证和加密功能,VACM 确定用户是否允许访问特定的 MIB 对象以及访问方式。
SNMPV3 中增加了安全管理方式及远程控制。SNMPV3 结构引入了基于用户的安全模型用于保证消息安全及基于视图的访问控制模型用于访问控制(USM)。这种安全管理方式支持不同安全性,访问控制安全性认证和隐私授权和访问控制管理框架人员和政策用户名及密钥管理通知目标文件代理关系实体命名。
SNMPV3 使用 SNMP SET 命令配置MIB对象,使之能动态配置SNMP代理。这种动态配置方式支持本地或远程地配置实体的添加、删除及修改。
一套完整的SNMP系统主要包括管理信息库(MIB)、管理信息结构(SMI)及SNMP报文协议。
管理信息库MIB
任何一个被管理的资源都表示成一个对象,称为被管理的对象。MIB是被管理对象的集合。它定义了被管理对象的一系列属性:对象的名称、对象的访问权限和对象的数据类型等。每个SNMP设备(Agent)都有自己的MIB。MIB也可以看作是NMS(网络管理系统,网络管理系统既可以指一台专门用来进行网络管理的服务器,也可以指某个网络设备中执行管理功能的一个应用程序)和Agent之间的沟通桥梁。它们之间的关系如图1所示。
图1 NMS Agent和MIB的关系
MIB文件中的变量使用的名字取自ISO和ITU管理的对象标识符(object identifier)名字空间。它是一种分级树的结构。如图2所示,第一级有三个节点:ccitt、iso、iso-ccitt。低级的对象ID分别由相关组织分配。一个特定对象的标识符可通过由根到该对象的路径获得。一般网络设备取iso节点下的对象内容。如名字空间ip结点下一个名字为ipInReceives的MIB变量被指派数字值3,因而该变量的名字为:
iso.org.dod.internet.mgmt.mib.ip.ipInReceives
相应的数字表示(对象标识符OID,唯一标识一个MIB对象)为:
1.3.6.1.2.1.4.3
图2 MIB树结构
当网络管理协议在报文中使用MIB变量时,每个变量名后还要加一个后缀,以作为该变量的一个实例。如ipInReceives的实例数字表示为:1.3.6.1.2.1.4.3.0.
需要注意的是,MIB中的管理对象的OID有些需要动态确定,如IP路由表,为了指明地址202.120.86.71的下一站路由(next hop),我们可以引用这样的实例:
iso.org.dod.internet.mgmt.mib.ip. ipRouteTable.ipRouteEntry.ipRouteNextHop.202.120.86.71, 相应的数字表示为:1.3.6.1.2.1.4.21.1.7.202.120.86.71
管理信息结构(SMI)
SMI定义了SNMP框架所用信息的组织、组成和标识,它还为描述MIB对象和描述协议怎样交换信息奠定了基础。
SMI定义的数据类型:
◆ 简单类型(simple)
Integer:整型是-2,147,483,648~2,147,483,647的有符号整数
octet string: 字符串是0~65535个字节的有序序列
OBJECT IDENTIFIER: 来自按照ASN.1规则分配的对象标识符集
◆ 简单结构类型(simple-constructed)
SEQUENCE 用于列表。这一数据类型与大多数程序设计语言中的“structure”类似。一个SEQUENCE包括0个或更多元素,每一个元素又是另一个ASN.1数据类型
SEQUENCE OF type 用于表格。这一数据类型与大多数程序设计语言中的“array”类似。一个表格包括0个或更多元素,每一个元素又是另一个ASN.1数据类型。
◆ 应用类型(application-wide)
IpAddress: 以网络序表示的IP地址。因为它是一个32位的值,所以定义为4个字节;
counter:计数器是一个非负的整数,它递增至最大值,而后回零。在SNMPv1中定义的计数器是32位的,即最大值为4,294,967,295;
Gauge :也是一个非负整数,它可以递增或递减,但达到最大值时保持在最大值,最大值为232-1;
time ticks:是一个时间单位,表示以0.01秒为单位计算的时间;
汇总如下:
SNMP报文
SNMP报文结构如下:(编码之前)
版本号 |
---|
团体名 |
协议数据单元PDU |
SNMP共有5种报文,所以其PDU也有5中。
1 SNMP的5种协议数据单元
SNMP中定义了五种消息类型:Get-Request、Get-Response、Get-Next-Request、Set-Request和Trap 。
(1)Get-Request 、Get-Next-Request与Get-Response
SNMP 管理站用Get-Request消息从拥有SNMP代理的网络设备中检索信息,而SNMP代理则用Get-Response消息响应。Get-Next- Request用于和Get-Request组合起来查询特定的表对象中的列元素。
(2)Set-Request
SNMP管理站用Set-Request 可以对网络设备进行远程配置(包括设备名、设备属性、删除设备或使某一个设备属性有效/无效等)。
(3)Trap
SNMP代理使用Trap向SNMP管理站发送非请求消息,一般用于描述某一事件的发生,如接口UP/DOWN,IP地址更改等。
上面五种消息中Get-Request、Get-Next-Request和Set-Request是由管理站发送到代理侧的161端口的;后面两种Get-Response和Trap 是由代理进程发给管理进程的,其中Trap消息被发送到管理进程的162端口,所有数据都是走UDP封装。SNMP工作流程如图2:
下图是封装成UDP数据报的5种操作的SNMP报文格式。可见一个SNMP报文共有三个部分组成,即公共SNMP首部、get/set首部、trap首部、变量绑定。
(1)公共SNMP首部 共三个字段: 版本 写入版本字段的是版本号减1,对于SNMP(即SNMPV1)则应写入0。 共同体(community) 共同体就是一个字符串,作为管理进程和代理进程之间的明文口令,常用的是6个字符“public”。 PDU类型 根据PDU的类型,填入0~4中的一个数字,其对应关系如表2所示意图。
表2 PDU类型
PDU类型 | 名称 |
---|---|
0 | get-request |
1 | get-next-request |
2 | get-response |
3 | set-request |
4 | trap |
(2)get/set首部 请求标识符(request ID) 这是由管理进程设置的一个整数值。代理进程在发送get-response报文时也要返回此请求标识符。管理进程可同时向许多代理发出get报文,这些报文都使用UDP传送,先发送的有可能后到达。设置了请求标识符可使管理进程能够识别返回的响应报文对于哪一个请求报文 差错状态(error status) 由代理进程回答时填入0~5中的一个数字,见表3的描述
表3 差错状态描述
差错状态 | 名字 | 说明 |
---|---|---|
0 | noError | 一切正常 |
1 | tooBig | 代理无法将回答装入到一个SNMP报文之中 |
2 | noSuchName | 操作指明了一个不存在的变量 |
3 | badValue | 一个set操作指明了一个无效值或无效语法 |
4 | readOnly | 管理进程试图修改一个只读变量 |
5 | genErr | 某些其他的差错 |
差错索引(error index) 当出现noSuchName、badValue或readOnly的差错时,由代理进程在回答时设置的一个整数,它指明有差错的变量在变量列表中的偏移。 (3)trap首部
trap报文格式如下:
企业(enterprise) 填入trap报文的网络设备的对象标识符。此对象标识符肯定是在图3的对象命名树上的enterprise结点{1.3.6.1.4.1}下面的一棵子树上。
代理地址
即代理进程所在系统的地址。
trap类型
此字段正式的名称是generic-trap,共分为表4中的7种。
trap类型 | 名字 | 说明 |
---|---|---|
0 | coldStart | 代理进行了初始化 |
1 | warmStart | 代理进行了重新初始化 |
2 | linkDown | 一个接口从工作状态变为故障状态 |
3 | linkUp | 一个接口从故障状态变为工作状态 |
4 | authenticationFailure | 从SNMP管理进程接收到具有一个无效共同体的报文 |
5 | egpNeighborLoss | 一个EGP相邻路由器变为故障状态 |
6 | enterpriseSpecific | 代理自定义的事件,需要用后面的“特定代码”来指明 |
当使用上述类型2、3、5时,在报文后面变量部分的第一个变量应标识响应的接口。
特定代码
特定代码仅仅在trap类型为6时有效,否则都置为0,他是厂家自定义的事件代码。
时间戳(timestamp) 指明自代理进程初始化到trap报告的事件发生所经历的时间,单位为10ms。例如时间戳为1908表明在代理初始化后1908ms发生了该时间。
(4)变量绑定(variable-bindings) 指明一个或多个变量的名和对应的值。在get或get-next报文中,变量的值应忽略。
管理变量的表示
管理变量表示管理对象类型在某一时刻的值(或称该类型的实例),SNMP以管理变量作为操作对象。
管理变量的表示方法是这样规定的:形如x.y,其中x是管理对象的object identifer。y是能唯一确定对象类型值的一组数字,在非表型变量中为0,在表型变量中是这个表的索引,比如接口表中的接口号,或路由表中的目的网络地址等等 。如:在MIB文件里定义了ipAdEntNetMask这一管理对象,其object identifier为1.3.6.1.1.5.6.1.3它是个路由表中的一项,它的一个实例就是路由表中某一行的子网掩码,如果这行的索引、目的网络地址为129.102.1.0。则这个变量名是:1.3.6.1.1.5.6.1.3.129.102.1.0。在以后的说明中,为了方便,把唯一确定管理变量的一组数字,也就是x.y中的y称作实例。
SNMP的运行过程
驻留在被管设备上的AGENT从UDP端口161接受来自网管站的串行化报文,经解码、团体名验证、分析得到管理变量在MIB树中对应的节点,从相应的模块中得到管理变量的值,再形成响应报文,编码发送回网管站。网管站得到响应报文后,再经同样的处理,最终显示结果。
下面根据RFC1157详细介绍Agent接受到报文后采取的动作:
首先解码生成用内部数据结构表示的报文,解码依据ASN.1的基本编码规则,如果在此过程中出现错误导致解码失败则丢弃该报文,不做进一步处理。
第二步:将报文中的版本号取出,如果与本Agent支持的SNMP版本不一致,则丢弃该报文,不做进一步处理。当前北研的数据通信产品只支持SNMP版本1。
第三步:将报文中的团体名取出,此团体名由发出请求的网管站填写。如与本设备认可的团体名不符,则丢弃该报文,不做进一步处理,同时产生一个陷阱报文。SNMPv1只提供了较弱的安全措施,在版本3中这一功能将大大加强。
第四步:从通过验证的ASN.1对象中提出协议数据单元PDU,如果失败,丢弃报文,不做进一不处理。否则处理PDU,结果将产生一个报文,该报文的发送目的地址应同收到报文的源地址一致。
根据不同的PDU,SNMP协议实体将做不同的处理:
1.1 GetRequest PDU
第一种情况:如果PDU中的变量名在本地维护的MIB树中不存在,则接受到这个PDU的协议实体将向发出者发送一个GetResponse报文,其中的PDU与源PDU只有一点不同:将ERROR-STATUS置为noSuchName,并在ERROR-INDEX中指出产生该变量在变量LIST中的位置。
第二种情况:如果本地协议实体将产生的响应报文的长度大于本地长度限制,将向该PDU的发出者发送一个GetResponse报文,该PDU除了ERROR-STATUS置为tooBig,ERROR-INDEX置为0以外,与源PDU相同。
第三种情况:如果本地协议实体因为其他原因不能产生正确的响应报文,将向该PDU的发出者发送一个GetResponse报文,该PDU除了ERROR-STATUS置为genErr,ERROR-INDEX置为出错变量在变量LIST中的位置,其余与源PDU相同。
第四中情况:如果上面的情况都没有发生,则本地协议实体向该PDU的发出者发送一个GetResponse报文,该PDU中将包含变量名和相应值的对偶表,ERROR-STATUS为noError,ERROR-INDEX为0,request-id域的值应与收到PDU的request-id相同。
1.2 GetNextRequest PDU
GetNextRequest PDU的最重要的功能是表的遍历,这种操作受到了前面所说的管理变量的表示方法的支持,从而可以访问一组相关的变量,就好象他们在一个表内。
下面通过一个例子解释表遍历的过程:
被管设备维护如下路由表:
Destination NextHop Metric
10.0.0.99 89.1.1.42 5
9.1.2.3 99.0.0.3 3
10.0.0.51 89.1.1.42 5
假设网管站欲取得这张路由表的信息,该表的索引是目的网络地址。
网管站向被管设备发送一个GetNextRequest PDU,其中的受管对象的标识如下
GetNextRequest ( ipRouteDest, ipRouteNextHop, ipRouteMetric1 )
SNMP agent响应如下GetResponse PDU:
GetResponse (( ipRouteDest.9.1.2.3 = “9.1.2.3” ),
( ipRouteNextHop.9.1.2.3 = “99.0.0.3” ),
( ipRouteMetric1.9.1.2.3 = 3 ))
网管站继续:
GetNextRequest ( ipRouteDest.9.1.2.3,
ipRouteNextHop.9.1.2.3,
ipRouteMetric1.9.1.2.3 )
agent响应:
GetResponse (( ipRouteDest.10.0.0.51 = “10.0.0.51” ),
( ipRouteNextHop.10.0.0.51 = “89.1.1.42” ),
( ipRouteMetric1.10.0.0.51 = 5 ))
值得注意的是agent必须能够确定下一个管理变量名,以保证所有变量能被取到且只被取到一次。
网管站继续:
GetNextRequest ( ipRouteDest.10.0.0.51,
ipRouteNextHop.10.0.0.51,
ipRouteMetric1.10.0.0.51 )
agent 响应:
GetResponse (( ipRouteDest.10.0.0.99 = “10.0.0.99” ),
( ipRouteNextHop.10.0.0.99 = “89.1.1.42” ),
( ipRouteMetric1.10.0.0.99 = 5 ))
网管站继续
GetNextRequest ( ipRouteDest.10.0.0.99,
ipRouteNextHop.10.0.0.99,
ipRouteMetric1.10.0.0.99 )
这时因为路由表中所有的行都被取遍,agent因返回路由表对象的下一字典后继即该管理对象在MIB树中的后序遍历的直接后继。这里应是nettoMediaIndex,管理对象的OBJECT IDENTIFIER。这个响应通知网管站对表的遍历已经完成。
1.3 GetResponse PDU
GetResponse PDU只有当受到getRequest GetNextRequest SetRequest才由协议实体产生,网管站收到这个PDU后,应显示其结果。
1.4 SetRequest PDU
SetRequest PDU除了PDU类型标识以外,和GetRequest相同,当需要对被管变量进行写操作时,网管站侧的协议实体将生成该PDU。
对SetRequest的响应将根据下面情况分别处理:
如果是关于一个只读变量的设置请求,则收到该PDU的协议实体产生一个GetReponse报文,并置error status为noSuchName, error index的值是错误变量在变量list中的位置。
如果被管设备上的协议实体收到的PDU中的变量对偶中的值,类型、长度不符和要求,则收到该PDU的协议实体产生一个GetReponse报文,并置error status为badValue, error index的值是错误变量在变量list中的位置。
如果需要产生的GetReponse报文长度超过了本地限制,则收到该PDU的协议实体产生一个GetReponse报文,并置error status为tooBig, error index的值是0。
如果是其他原因导致SET失败,则收到该PDU的协议实体产生一个GetReponse报文,并置error status为genErr, error index的值是错误变量在变量list中的位置。
如果不符合上面任何情况,则agent将把管理变量设置收到的PDU中的相应值,这往往可以改变被管设备的运行状态。同时产生一个GetResponse PDU,其中error status置为noError,error index的值为0。
1.5 Trap PDU
Trap PDU的有如下的形式
产生trap的系统的OBJECT IDENTIFIER |
---|
系统的IP地址 |
普通类型 |
特定类型 |
时戳 |
变量对偶表 |
Trap是被管设备遇到紧急情况时主动向网管站发送的消息。网管站收到trap PDU后要将起变量对偶表中的内容显示出来。一些常用的trap类型有冷、热启动,链路状态发生变化等。
通过wireshark抓包工具,捕获一条如下的SNMP报文,接下来对其进行仔细分析。
SNMPv1原始报文内容:
00 23 5a 9e 58 b9 00 4c 41 49 50 55 08 00 45 00 00 48 00 00 40 00 40 11 a5 4e c0 a8 0a 01 c0 a8 0a 05 0c 00 00 a2 00 34 ff e0 30 2a 02 01 00 04 06 70 75 62 6c 69 63 a4 1d 06 0a 2b 06 01 04 01 bf 08 03 02 0a 40 04 c0 a8 0a 01 02 01 00 02 01 00 43 01 0e 30 00
目的MAC:00 23 5a 9e 58 b9
源MAC:00 4c 41 49 50 55
协议类型:08 00 ,为IP数据报
IP头:45 00 00 48 00 00 40 00 40 11 a5 4e c0 a8 0a 01 c0 a8 0a 05 0c
UDP头:0c 00 00 a2 00 34 ff e0
其余部分都为SNMP报文,接下来我们对照前面的报文结构体来逐个分析一下。
n 30 表示SNMP消息是ASN.1的SEQUENCE类型;
n 2a 表示该SNMP报文的总长度是42(0x2a)个字节,该字段所表示的报文长度起始于它后面的第一个字节直到报文结束;
n 02 01 00 表示版本号,可见其确实为BER编码方式。02表示该字段是INTEGER类型;01表示该字段占1个字节;00表示版本号,该值为“版本号-1”;
n 04 06 70 75 62 6c 69 63 表示团体名,04表示该字段为OCTET STRING类型;06表示该字段占6个字节;70 75 62 6c 69 63 表示团体名的ANSII码的十六进制形式,这里是“public”;
n a4 1d 其中a4中的“4”表示这是一个trap报文,a4又叫报文的标签标记;1d表示后面还有29(0x1d)个字节的数据;
n 06 0a 2b 06 01 04 01 bf 08 03 02 0a 企业OID标识。06表示该字段是个对象标识符,OBJECT IDENTIFIER;0a表示该字段占10(0x0a)个字节;关于SNMP的OID的编码方式有些奇特:例如1.3.6.1.2…. 取前两个数字分别记为x和y。编码时40*x y,这里x=1,y=3,因此结果为40*1 3=43,即表示十六进制的2b。因此,这里的企业OID编码即为1.3.6.1.4.1.8072.3.2.10;
n 40 04 c0 a8 0a 01 同样40表示该字段为OCTET STRING 类型;04表示IP地址占4个字节;IP地址为192.168.10.1;
n 02 01 00 其中00表示trap类型为coldStart;
n 02 01 00 其中00表示我们指定的trap即specific-trap也为coldStart类型;
n 43 01 0e 43表示为TimeTicks类型;01表示该字段占1个字节;0e即十进制的14表示时间标签为0.14秒,这里时间计数器以0.01秒递增;
n 30 00 30表示“键-值”值对的编码类型为SEQUENCE;00表示该字段占0个字节,即没有该字段。
SNMPv2 Trap报文 SNMPv2的Trap报文格式如图8所示:
同样的,这里除了trap类型和报文长度是标准网络字节序之外,其余协议字段也均为BER编码方式。可以看到v2版的trap报文正在向统一的报文格式发展,已经非常类似普通的SNMP请求、响应报文了。
SNMPv2原始报文内容:
00 23 5a 9e 58 b9 00 4c 41 49 50 55 08 00 45 00 00 7b 00 00 40 00 40 11 a5 1b c0 a8 0a 01 c0 a8 0a 05 0c 01 00 a2 00 67 04 bb 30 5d02 01 01 04 06 70 75 62 6c 69 63 a7 50 02 04 17 73 2c fb 02 01 00 02 01 00 30 42 30 0d 06 08 2b 06 01 02 01 01 03 00 43 01 0e 30 1706 0a 2b 06 01 06 03 01 01 04 01 00 06 09 2b 06 01 06 03 01 01 05 01 30 18 06 0a 2b 06 01 06 03 01 01 04 03 00 06 0a 2b 06 01 04 01 bf 08 03 02 0a
目的MAC:00 23 5a 9e 58 b9
源MAC:00 4c 41 49 50 55
协议类型:08 00,IP报文
IP头:45 00 00 7b 00 00 40 00 40 11 a5 1b c0 a8 0a 01 c0 a8 0a 05
UDP头:0c 01 00 a2 00 67 04 bb
余下部分全为SNMP报文内容,这里我们做一下简单的约定:
xx 标注类型;xx 标注长度;xx 标注真正的数据。
这样一来上面这串原始数据就好分析多了J
n 30 5d 整个SNMP报文的编码方式为30,即SEQUENCE类型,报文长度93(0x5d)字节;
n 02 01 01 版本号01即v2版本;
n 04 06 70 75 62 6c 69 63 团体名70 75 62 6c 69 63 即英文的“public”;
n a7 50 a7表示trap类型为7,即厂商自定义trap;50表示PDU区段占80(0x50)字节;
n 02 04 17 73 2c fb 请求ID为17 73 2c fb 十进制的393424123;
n 02 01 00 错误状态0;
n 02 01 00 错误索引0;
n 30 42 “变量名-值”对编码类型30 即SEQUENCE类型;“变量名-值”所占总字节0x42,即66字节;
n 30 0d 06 08 2b 06 01 02 01 01 03 00 43 01 0e 第一个“名-值”对区段编码方式30 即SEQUENCE类型;第一个“名-值”对总长度0x0d,13字节;第一个变量名的编码类型0x06,时间标签;第一个变量名占0x08个字节;第一个变量名2b 06 01 02 01 01 03 00,为1.3.6.1.2.1.1.3.0;第一个变量值为0x0e,即14;
n 30 17 06 0a 2b 06 01 06 03 01 01 04 01 00 06 09 2b 06 01 06 03 01 01 05 01 第二个“名-值”对;变量名1.3.6.1.6.3.1.1.4.1.0;变量值1.3.6.1.6.3.1.1.5.1;
n 30 18 06 0a 2b 06 01 06 03 01 01 04 03 00 06 0a 2b 06 01 04 01 bf 08 03 02 0a 第三个“名-值”对;变量名1.3.6.1.6.3.1.1.4.3.0;变量值1.3.6.1.4.1.8072.3.2.10;
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/181001.html原文链接:https://javaforall.cn