核心实现思路
我们将利用 SNMP (Simple Network Management Protocol) 来获取交换机的信息。SNMP 协议是管理网络设备的标准协议,Java 标准库并没有直接支持,但是有一些轻量级的开源库可以引入。通过引入 snmp4j 库来获取 LLDP 相关的信息。
主要模块设计
graph TD
A[TopologyDiscoveryApp] --> B(TopologyManager)
B -- Uses --> C(Snmp4jClient)
B -- Uses --> D(MibOids)
B -- Registers --> E(IDiscoveryMethod)
E -- Implemented by --> F(LldpDiscoveryMethod)
F -- (Optional/Future) --> G(MacTableDiscoveryMethod)
B -- Manages --> H(NetworkTopology)
H -- Contains --> I(NetworkNode)
H -- Contains --> J(Link)
I -- Has --> K(NetworkNode.ports)
classDef entity fill:#B8DBEA,stroke:#3C8DBD,stroke-width:2px;
classDef interface fill:#AEC6CF,stroke:#6B8E9B,stroke-width:2px;
classDef client fill:#F7E3B5,stroke:#D4A017,stroke-width:2px;
classDef manager fill:#D0E8D7,stroke:#6AB7B1,stroke-width:2px;
class C,F,G client;
class D,I,J,K entity;
class E interface;
class B,H manager;
核心类说明
NetworkNode
:精简与业务预留- 核心标识:
chassisId
(主键)。 - 新增字段:
id
(String): 业务 ID,由您后续填充。type
(String): 业务类型,由您后续填充。
- 精简字段:
ipAddress
->ip
(String)。isReachable
->reachable
(boolean)。
- 其他属性:
sysName
。 - 端口信息:
Map<Integer, String> ports
(key: 端口号, value: 端口描述),不存储 MAC 和 operStatus,因为这些是链路状态的细节,可以在链路中体现或按需查询。
- 核心标识:
Link
:精简与 LLDP 专属- 核心属性:
sourceChassisId
(精简)。targetChassisId
(精简)。sourcePortNum
(本地端口号)。targetPortNum
(远程端口号)。
- 精简字段:
discoveryMethod
:目前仅保留LLDP
,但可以保留枚举类型以供未来扩展。status
:链路状态,可以根据sourcePortNum
的operStatus
推断,但在Link
中只体现为boolean
状态,或enum
。我们先保留一个status
字段。
- 链路的唯一性:
sourceChassisId
,sourcePortNum
,targetChassisId
,targetPortNum
四元组。
- 核心属性:
NetworkTopology
:图的容器- 职责不变,管理
NetworkNode
和Link
。 nodes
Map 的键仍是chassisId
。
- 职责不变,管理
TopologyManager
:发现与构建的协调者- 发现方式:目前仅使用
LldpDiscoveryMethod
。 chassisIdToIpMap
: 继续维护,用于后处理填充NetworkNode
的ip
。visitedChassisIds
: 用于递归去重。
- 发现方式:目前仅使用
总体流程
graph TD
%% 阶段1: 发现与构建核心拓扑
A[TopologyDiscoveryApp main] --> B[TopologyManager discoverNetwork seedIps]
B --> C{遍历 seed Ips}
C --> D[TopologyManager discover Device And Neighbors current Ip]
D --> E{获取当前设备 Chassis ID & 基础信息}
E --> F[NetworkNode: Chassis ID, ID, Type, SysName, IP, Reachable]
F --> G[NetworkTopology add Node]
F --> H[NetworkNode add Port num, port Desc]
E --> I{获取 LLDP 邻居信息}
I --> J{创建 Link 对象}
J --> K[Link: Source Chassis ID, Source Port Num, Target Chassis ID, Target Port Num, Status, LLDP]
K --> L[NetworkTopology add Link]
J --> M{获取 Target Chassis ID}
M -- 如果未访问 --> D
%% 阶段2: 后处理填充IP
B --> N[TopologyManager resolve Remote Ips]
N --> O{遍历 Network Topology 中的所有 Link}
O --> P{根据 Target Chassis ID 查找对应的 IP}
P -- 找到 IP --> Q[更新 Link.targetNode.IP]
%% 核心数据结构
subgraph Data Structures
R[NetworkTopology] --- S[NetworkNode]
R --- T[Link]
S --- U[NetworkNode.ports]
end
主要代码
SnmpConfig
package com.xxx.topology;
import lombok.Data;
import org.snmp4j.mp.SnmpConstants;
/**
* SNMP连接配置。
* 集中管理SNMP连接所需的所有参数
* 如:community字符串、SNMP版本、重试次数和超时时间。
*/
@Data
public class SnmpConfig {
private String community;
private int snmpVersion;
private int retries;
private long timeoutMillis;
public SnmpConfig() {
}
/**
* 构造一个新的SnmpConfig实例。
*
* @param community SNMP community字符串,用于认证。
* @param snmpVersion SNMP版本,如SnmpConstants.version2c。
* @param retries SNMP请求的重试次数。
* @param timeoutMillis SNMP请求的超时时间(毫秒)。
*/
public SnmpConfig(String community, int snmpVersion, int retries, long timeoutMillis) {
this.community = community;
this.snmpVersion = snmpVersion;
this.retries = retries;
this.timeoutMillis = timeoutMillis;
}
/**
* 提供一个默认的SNMP配置实例。
* 默认为community "public", SNMPv2c, 重试2次, 超时1500毫秒。
*
* @return 默认的SnmpConfig实例。
*/
public static SnmpConfig getDefault() {
return new SnmpConfig("public", SnmpConstants.version2c, 2, 1500);
}
}
- Snmp4jClient
package com.xxx.topology;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.smi.*;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.snmp4j.util.DefaultPDUFactory;
import org.snmp4j.util.TreeEvent;
import org.snmp4j.util.TreeUtils;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 提供了基于SNMP4J库的SNMP通信功能。
*
* @version 1.0.0
* Created on 2025/6/12 11:43
*/
public class Snmp4jClient {
private Snmp snmp;
private SnmpConfig config;
/**
* 构造一个Snmp4jClient实例,并初始化SNMP会话。
*
* @param config SNMP配置。
* @throws IOException 如果网络传输层初始化失败。
*/
public Snmp4jClient(SnmpConfig config) throws IOException {
this.config = config;
TransportMapping<UdpAddress> transport = new DefaultUdpTransportMapping();
snmp = new Snmp(transport);
// 开始监听传入的SNMP消息
transport.listen();
}
/**
* 根据IP地址创建SNMP目标对象。
*
* @param ipAddress 目标设备的IP地址。
* @return 配置好的CommunityTarget对象。
*/
private CommunityTarget createTarget(String ipAddress) {
CommunityTarget target = new CommunityTarget();
target.setCommunity(new OctetString(config.getCommunity()));
target.setAddress(GenericAddress.parse("udp:" + ipAddress + "/161")); // SNMP默认端口161
target.setRetries(config.getRetries());
target.setTimeout(config.getTimeoutMillis());
target.setVersion(config.getSnmpVersion());
return target;
}
/**
* 规范化OID字符串,确保其以'.'开头。
* SNMP4J的OID对象通常以'.'开头,但从设备获取的字符串可能不带。
* 统一格式有助于避免匹配问题。
*
* @param oidString 原始OID字符串。
* @return 规范化后的OID字符串。
*/
private String normalizeOid(String oidString) {
if (oidString != null && !oidString.startsWith(".")) {
return "." + oidString;
}
return oidString;
}
/**
* 执行SNMP WALK操作,获取指定OID子树下的所有值。
* 利用SNMP4J的TreeUtils进行可靠的MIB遍历。
*
* @param ipAddress 目标设备的IP地址。
* @param oid 要WALK的基准OID。
* @return 一个Map,键是规范化后的OID字符串,值是对应的MIB变量值。
* @throws IOException 如果SNMP通信发生错误。
*/
public Map<String, String> snmpWalk(String ipAddress, String oid) throws IOException {
Map<String, String> result = new ConcurrentHashMap<>();
CommunityTarget target = createTarget(ipAddress);
TreeUtils treeUtils = new TreeUtils(snmp, new DefaultPDUFactory());
List<TreeEvent> events = treeUtils.getSubtree(target, new OID(normalizeOid(oid)));
if (events == null) {
System.err.println("SNMP TreeUtils.getSubtree returned null for OID: " + oid);
return result;
}
for (TreeEvent event : events) {
if (event.isError()) {
System.err.println("SNMP TreeUtils Error for OID " + oid + ": " + event.getErrorMessage());
continue;
}
if (event.getVariableBindings() != null) {
for (VariableBinding vb : event.getVariableBindings()) {
if (vb != null && vb.getOid() != null) {
String normalizedOid = normalizeOid(vb.getOid().toString());
if (normalizedOid.startsWith(normalizeOid(oid))) {
result.put(normalizedOid, vb.getVariable().toString());
}
}
}
}
}
return result;
}
/**
* 执行SNMP GET操作,获取单个MIB变量的值。
*
* @param ipAddress 目标设备的IP地址。
* @param oid 要GET的MIB变量的OID。
* @return OID对应的值(字符串形式),如果获取失败则返回null。
* @throws IOException 如果SNMP通信发生错误。
*/
public String snmpGet(String ipAddress, String oid) throws IOException {
CommunityTarget target = createTarget(ipAddress);
PDU pdu = new PDU();
pdu.add(new VariableBinding(new OID(normalizeOid(oid))));
pdu.setType(PDU.GET);
ResponseEvent event = snmp.send(pdu, target, null);
if (event != null && event.getResponse() != null) {
if (event.getResponse().getErrorStatus() == PDU.noError) {
VariableBinding vb = event.getResponse().getVariableBindings().get(0);
String normalizedVbOid = normalizeOid(vb.getOid().toString());
if (normalizedVbOid.equals(normalizeOid(oid))) {
return vb.getVariable().toString();
}
} else {
System.err.println("SNMP GET Error for OID " + oid + ": " + event.getResponse().getErrorStatusText());
}
} else {
System.err.println("No response or timed out for SNMP GET OID " + oid);
}
return null;
}
/**
* 关闭SNMP会话,释放网络资源。
*
* @throws IOException 如果关闭过程中发生错误。
*/
public void close() throws IOException {
if (snmp != null) {
snmp.close();
}
}
}
- NetworkNode
package com.xxx.topology;
import lombok.Data;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 表示网络拓扑中的一个设备节点。
* 以Chassis ID作为唯一主键,并包含业务关联字段、设备基本信息和端口信息。
*
* @version 1.0.0
* Created on 2025/6/12 11:44
*/
@Data
public class NetworkNode {
// 设备的全局唯一标识符 (Chassis ID),作为主键。
private String chassisId;
// 业务ID,由用户后续填充,与业务系统关联。
private String id;
// 业务类型,由用户后续填充,用于分类设备。
private String type;
// 设备系统名称。
private String sysName;
// 设备的主IP地址。
private String ip;
// 设备当前是否可达 (通过SNMP通信)。
private boolean reachable;
// 设备本地端口的映射,键为端口号(int),值为端口描述(String)。
private Map<Integer, String> ports;
/**
* 构造一个NetworkNode实例。
*
* @param chassisId 设备的Chassis ID。
* @param sysName 设备系统名称。
* @param ip 设备的主IP地址。
* @param reachable 设备的可达性状态。
*/
public NetworkNode(String chassisId, String sysName, String ip, boolean reachable) {
this.chassisId = chassisId;
this.sysName = sysName;
this.ip = ip;
this.reachable = reachable;
// 端口信息初始化
this.ports = new ConcurrentHashMap<>();
}
/**
* 向节点添加或更新一个本地端口的信息。
*
* @param portNum 端口号。
* @param portDesc 端口描述。
*/
public void addPort(int portNum, String portDesc) {
this.ports.put(portNum, portDesc);
}
}
- Link
package com.xxx.topology;
import lombok.Data;
/**
* 表示网络中两个NetworkNode之间的一条链路(边)。
* 链路的唯一性由源节点、源端口、目标节点和目标端口共同决定。
*
* @version 1.0.0
* Created on 2025/6/12 11:45
*/
@Data
public class Link {
// 链路源节点的Chassis ID。
private String sourceChassisId;
// 源节点上的端口号。
private Integer sourcePortNum;
// 链路目标节点的Chassis ID。
private String targetChassisId;
// 目标节点上的端口号。
private String targetPortNum;
// 链路的UP/DOWN状态(true为UP,false为DOWN),基于源端口。
private Boolean status;
// 链路的发现方式。
private DiscoveryMethod discoveryMethod;
/**
* 定义了链路被发现的方式。
* 保留未来扩展其他发现方法的可能性。
*/
public enum DiscoveryMethod {
LLDP, // 通过Link Layer Discovery Protocol发现。
MAC_TABLE, // 通过MAC地址表(FDB)发现。
ARP, // 通过ARP表发现(未来扩展)。
MANUAL // 手动添加(未来扩展)。
}
/**
* 构造一个Link实例。
*
* @param sourceChassisId 源节点的Chassis ID。
* @param sourcePortNum 源节点上的端口号。
* @param targetChassisId 目标节点的Chassis ID。
* @param targetPortNum 目标节点上的端口号。
* @param status 链路状态。
* @param discoveryMethod 链路发现方式。
*/
public Link(String sourceChassisId, Integer sourcePortNum, String targetChassisId, String targetPortNum, Boolean status, DiscoveryMethod discoveryMethod) {
this.sourceChassisId = sourceChassisId;
this.sourcePortNum = sourcePortNum;
this.targetChassisId = targetChassisId;
this.targetPortNum = targetPortNum;
this.status = status;
this.discoveryMethod = discoveryMethod;
}
}
- IDiscoveryMethod
package com.xxx.topology;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 定义了链路发现方法的通用接口。
* 发现方法负责从本地设备发现直连链路。
*
* @version 1.0.0
* Created on 2025/6/12 11:46
*/
public interface IDiscoveryMethod {
/**
* 发现从本地设备到其邻居的直连链路。
*
* @param snmpClient 用于查询设备的SNMP客户端。
* @param localDeviceIp 正在发现的本地设备的IP地址。
* @param localChassisId 本地设备的Chassis ID。
* @param localPorts 本地设备的端口信息映射(键为端口号,值为端口描述)。
* @return 从该本地设备发现的Link对象列表。
* @throws IOException 如果在发现过程中发生SNMP通信错误。
*/
List<Link> discover(Snmp4jClient snmpClient, String localDeviceIp,
String localChassisId,
Map<Integer, String> localPorts) throws IOException;
}
- LldpDiscoveryMethod
package com.xxx.topology;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* LldpDiscoveryMethod实现了IDiscoveryMethod接口,
* 专门通过LLDP协议(查询标准LLDP-MIB OIDs)来发现网络链路。
*
* @version 1.0.0
* Created on 2025/6/12 11:47
*/
@Slf4j
public class LldpDiscoveryMethod implements IDiscoveryMethod {
@Override
public List<Link> discover(Snmp4jClient snmpClient, String localDeviceIp,
String localChassisId,
Map<Integer, String> localPorts) throws IOException {
log.debug("Attempting LLDP discovery for " + localDeviceIp + " (standard LLDP-MIB)...");
List<Link> discoveredLinks = new ArrayList<>();
// 获取lldpRemLocalPortNumMap,即使它可能为空,也不立即返回,因为它不是唯一判断标准
Map<String, String> remLocalPortNumMap = snmpClient.snmpWalk(localDeviceIp, MibOidManager.LLDP_REM_LOCAL_PORT_NUM_OID_COL);
log.debug(" LLDP: remLocalPortNumMap size: " + remLocalPortNumMap.size() + " (OID: " + MibOidManager.LLDP_REM_LOCAL_PORT_NUM_OID_COL + ")");
// 以remChassisIdMap作为主导循环的驱动,因为它代表了实际发现的远程邻居Chassis ID
Map<String, String> remChassisIdMap = snmpClient.snmpWalk(localDeviceIp, MibOidManager.LLDP_REM_CHASSIS_ID_OID_COL);
log.debug("LLDP: remChassisIdMap size: {} (OID: " + MibOidManager.LLDP_REM_CHASSIS_ID_OID_COL + ")", remChassisIdMap.size());
// 如果没有发现远程Chassis ID,则认为没有LLDP邻居,直接返回
if (remChassisIdMap.isEmpty()) {
log.debug("No LLDP remChassisId data found for {}. Skipping remote LLDP discovery.", localDeviceIp);
return discoveredLinks;
}
// 获取其他LLDP远程信息
Map<String, String> remPortNumMap = snmpClient.snmpWalk(localDeviceIp, MibOidManager.LLDP_REM_PORT_ID_OID_COL);
Map<String, String> remPortDescMap = snmpClient.snmpWalk(localDeviceIp, MibOidManager.LLDP_REM_PORT_DESC_OID_COL);
Map<String, String> remSysNameMap = snmpClient.snmpWalk(localDeviceIp, MibOidManager.LLDP_REM_SYS_NAME_OID_COL);
// 遍历remChassisIdMap中的每个远程邻居条目
remChassisIdMap.forEach((oid, targetChassisId) -> {
// 从当前Chassis ID的OID中提取LLDP索引后缀,这个后缀是这条远程邻居记录的唯一标识
String oidSuffix = MibOidManager.extractLldpRemoteEntryIndexSuffix(oid, MibOidManager.LLDP_REM_CHASSIS_ID_OID_COL);
if (oidSuffix == null) {
log.debug("LLDP: Could not extract OID suffix from {}. Skipping this entry.", oid);
return;
}
// 从索引后缀中解析出本地端口号(localPortNum),这是本设备上发现该邻居的端口
int sourcePortNum = MibOidManager.parseLldpRemLocalPortNumFromIndexSuffix(oidSuffix);
// 检查本地端口是否存在并获取其描述,并据此判断链路状态
String sourcePortDesc = localPorts.get(sourcePortNum);
boolean linkStatus = false; // 默认链路状态为DOWN
if (sourcePortDesc != null) { // 如果本地端口存在,则认为链路是活跃的(简化处理,实际可能需要ifOperStatus)
linkStatus = true;
} else {
log.debug("LLDP: Source port {} (from OID {}) not found in localPorts. Skipping this link.", sourcePortNum, oid);
return; // 如果本地端口信息缺失,则无法有效构建该链路
}
// 使用相同的索引后缀获取远程端口号、远程端口描述和远程系统名称
String targetPortNum = remPortNumMap.get(MibOidManager.LLDP_REM_PORT_ID_OID_COL + oidSuffix);
String targetPortDesc = remPortDescMap.get(MibOidManager.LLDP_REM_PORT_DESC_OID_COL + oidSuffix);
String targetSysName = remSysNameMap.get(MibOidManager.LLDP_REM_SYS_NAME_OID_COL + oidSuffix);
// 创建Link对象,表示从localChassisId到targetChassisId的链路
Link link = new Link(
localChassisId,
sourcePortNum,
targetChassisId,
// 确保targetPortNum不为null,如果没有则使用"N/A"或targetPortDesc
targetPortNum != null ? targetPortNum : (targetPortDesc != null ? targetPortDesc : "N/A"),
linkStatus,
Link.DiscoveryMethod.LLDP // 明确发现方式为LLDP
);
discoveredLinks.add(link);
});
return discoveredLinks;
}
}
- MacTableDiscoveryMethod
package com.xxx.topology;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* MacTableDiscoveryMethod实现了IDiscoveryMethod接口,
* 通过设备的MAC地址表(FDB)发现链路。
* 注意:该方法目前仅为扩展性保留,TopologyManager默认不使用此方法。
*
* @version 1.0.0
* Created on 2025/6/12 11:47
*/
@Slf4j
public class MacTableDiscoveryMethod implements IDiscoveryMethod {
@Override
public List<Link> discover(Snmp4jClient snmpClient, String localDeviceIp,
String localChassisId,
Map<Integer, String> localPorts) throws IOException {
log.debug("Attempting BRIDGE-MIB discovery for {}...", localDeviceIp);
List<Link> discoveredLinks = new ArrayList<>();
// 获取Bridge Port到ifIndex的映射
Map<String, String> dot1dBasePortIfIndex = snmpClient.snmpWalk(localDeviceIp, MibOidManager.DOT1D_BASE_PORT_IF_INDEX_OID);
Map<Integer, Integer> bridgePortToIfIndex = new HashMap<>();
dot1dBasePortIfIndex.forEach((oid, ifIndexStr) -> {
try {
int bridgePort = MibOidManager.extractLastOidIndex(oid);
int ifIndex = Integer.parseInt(ifIndexStr);
bridgePortToIfIndex.put(bridgePort, ifIndex);
} catch (NumberFormatException e) {
log.error("Error parsing bridge port or ifIndex from OID: {}", oid);
}
});
// 获取MAC地址表项的MAC地址和对应的Bridge Port
Map<String, String> dot1dTpFdbAddress = snmpClient.snmpWalk(localDeviceIp, MibOidManager.DOT1D_TP_FDB_ADDRESS_OID);
Map<String, String> dot1dTpFdbPort = snmpClient.snmpWalk(localDeviceIp, MibOidManager.DOT1D_TP_FDB_PORT_OID);
// 遍历MAC地址表项
for (Map.Entry<String, String> entry : dot1dTpFdbAddress.entrySet()) {
String oid = entry.getKey();
// 远程设备的MAC地址,作为其identifier
String macAddress = entry.getValue();
// 提取OID后缀以查找对应的Bridge Port
String portOidSuffix = oid.substring(MibOidManager.DOT1D_TP_FDB_ADDRESS_OID.length());
String bridgePortStr = dot1dTpFdbPort.get(MibOidManager.DOT1D_TP_FDB_PORT_OID + portOidSuffix);
if (bridgePortStr != null) {
try {
int bridgePort = Integer.parseInt(bridgePortStr);
Integer sourcePortNum = bridgePortToIfIndex.get(bridgePort); // 获取本地端口号ifIndex
if (sourcePortNum != null && localPorts.containsKey(sourcePortNum)) {
// 检查本地端口是否存在,并假设链路活跃
Link link = getLink(localChassisId, sourcePortNum, macAddress);
discoveredLinks.add(link);
}
} catch (NumberFormatException e) {
log.error("Error parsing bridge port from FDB OID: {}", bridgePortStr);
}
}
}
return discoveredLinks;
}
private static Link getLink(String localChassisId, Integer sourcePortNum, String macAddress) {
// 简化处理,实际可能需要更复杂的判断
boolean linkStatus = true;
// 创建Link对象
// 目标Chassis ID为发现到的MAC地址
// 目标端口号无法通过MAC表直接获取,设置为"N/A (MAC)"
// 目标Chassis ID为MAC地址
// 目标端口号
// 发现方式为MAC_TABLE
return new Link(
localChassisId,
sourcePortNum,
macAddress, // 目标Chassis ID为MAC地址
"N/A (MAC)", // 目标端口号
linkStatus,
Link.DiscoveryMethod.MAC_TABLE // 发现方式为MAC_TABLE
);
}
}
- MibOidManager
package com.xxx.topology;
import lombok.extern.slf4j.Slf4j;
/**
* 集中管理所有SNMP MIB OID常量及辅助解析函数。
* 所有OID常量都以'.'开头,以保持与Snmp4jClient中normalizeOid逻辑的一致性。
*
* @version 1.0.0
* Created on 2025/6/12 11:44
*/
@Slf4j
public class MibOidManager {
// --- 标准MIB OIDs ---
// System MIB (RFC 1213)
// 设备系统名称
public static final String SYS_NAME_OID = ".1.3.6.1.2.1.1.5.0";
// IF-MIB (RFC 2863) - 接口信息
// 接口索引列 (ifIndex)
public static final String IF_INDEX_OID_COL = ".1.3.6.1.2.1.2.2.1.1";
// 接口描述列 (ifDescr)
public static final String IF_DESCR_OID = ".1.3.6.1.2.1.2.2.1.2";
// 接口操作状态列 (ifOperStatus)
public static final String IF_OPER_STATUS_OID = ".1.3.6.1.2.1.2.2.1.8";
// 接口MAC地址列 (ifPhysAddress)
public static final String IF_PHYS_ADDRESS_OID = ".1.3.6.1.2.1.2.2.1.6";
// LLDP-MIB (IEEE 802.1AB)
// 本地设备Chassis ID (lldpLocChassisId)
public static final String LLDP_LOC_CHASSIS_ID_OID = ".1.0.8802.1.1.2.1.3.2.0";
// LLDP 远程表 (lldpRemTable) OIDs,基于用户设备的实际OID结构进行修正
// 结构: .1.0.8802.1.1.2.1.4.1.1.<ColumnID>.<lldpRemTimeMark>.<lldpRemLocalPortNum>.<lldpRemIndex>
public static final String LLDP_REM_TABLE_BASE_OID = ".1.0.8802.1.1.2.1.4.1.1";
// 远程设备所在的本地接口索引 (lldpRemLocalPortNum)
public static final String LLDP_REM_LOCAL_PORT_NUM_OID_COL = LLDP_REM_TABLE_BASE_OID + ".3";
// 远程设备Chassis ID (lldpRemChassisId)
public static final String LLDP_REM_CHASSIS_ID_OID_COL = LLDP_REM_TABLE_BASE_OID + ".5";
// 远程设备端口ID (lldpRemPortId)
public static final String LLDP_REM_PORT_ID_OID_COL = LLDP_REM_TABLE_BASE_OID + ".8";
// 远程设备端口描述 (lldpRemPortDesc)
public static final String LLDP_REM_PORT_DESC_OID_COL = LLDP_REM_TABLE_BASE_OID + ".9";
// 远程设备系统名称 (lldpRemSysName)
public static final String LLDP_REM_SYS_NAME_OID_COL = LLDP_REM_TABLE_BASE_OID + ".10";
// 远程设备系统描述 (lldpRemSysDesc)
public static final String LLDP_REM_SYS_DESC_OID_COL = LLDP_REM_TABLE_BASE_OID + ".11";
// BRIDGE-MIB (RFC 1493) - 用于MAC地址表发现 (保留扩展性)
// Bridge Port到ifIndex的映射
public static final String DOT1D_BASE_PORT_IF_INDEX_OID = ".1.3.6.1.2.1.17.1.4.1.2";
// MAC地址表项的MAC地址
public static final String DOT1D_TP_FDB_ADDRESS_OID = ".1.3.6.1.2.1.17.4.3.1.1";
// MAC地址表项对应的Bridge Port
public static final String DOT1D_TP_FDB_PORT_OID = ".1.3.6.1.2.1.17.4.3.1.2";
/**
* 从OID字符串中提取最后一个子OID作为整数索引。
* 例如,对于".1.3.6.1.2.1.2.2.1.2.10"这样的OID,将提取并返回10。
*
* @param oid 规范化后的OID字符串。
* @return 最后一个子OID的整数值,如果无法解析则返回-1。
*/
public static int extractLastOidIndex(String oid) {
try {
return Integer.parseInt(oid.substring(oid.lastIndexOf(".") + 1));
} catch (NumberFormatException | StringIndexOutOfBoundsException e) {
return -1;
}
}
/**
* 从LLDP远程表项的完整OID中提取公共索引后缀。
* 索引后缀格式为: .<lldpRemTimeMark>.<lldpRemLocalPortNum>.<lldpRemIndex>。
* 例如,对于OID ".1.0.8802.1.1.2.1.4.1.1.5.12345.6.1",传入colOidPrefix ".1.0.8802.1.1.2.1.4.1.1.5",
* 将返回".12345.6.1"。
*
* @param lldpOid 规范化后的完整LLDP OID字符串。
* @param colOidPrefix 规范化后的列OID前缀。
* @return 提取出的索引后缀字符串,如果OID不以指定前缀开头则返回null。
*/
public static String extractLldpRemoteEntryIndexSuffix(String lldpOid, String colOidPrefix) {
if (lldpOid.startsWith(colOidPrefix)) {
return lldpOid.substring(colOidPrefix.length());
}
return null;
}
/**
* 从LLDP远程表项的索引后缀中解析出lldpRemLocalPortNum。
* 索引后缀格式为: .<timeMark>.<localPortNum>.<remoteIndex> (例如 ".12345.6.1")。
* 将解析并返回<localPortNum>的值,例如6。
*
* @param indexSuffix 索引后缀字符串。
* @return lldpRemLocalPortNum的整数值,如果无法解析则返回-1。
*/
public static int parseLldpRemLocalPortNumFromIndexSuffix(String indexSuffix) {
try {
String[] parts = indexSuffix.split("\\.");
if (parts.length >= 3) {
// parts[0]为空,parts[1]是timeMark,parts[2]是localPortNum
return Integer.parseInt(parts[2]);
}
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
log.error("Error parsing lldpRemLocalPortNum from index suffix: {} - {}", indexSuffix, e.getMessage());
}
return -1;
}
}
- NetworkTopology
package com.xxx.topology;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 表示整个网络的拓扑图。
* 管理NetworkNodes(以Chassis ID为键)和Links。
* 采用单例模式,以确保全局只有一个拓扑实例。
*
* @version 1.0.0
* Created on 2025/6/12 11:46
*/
@Slf4j
@Data
public class NetworkTopology {
// 存储所有NetworkNode,键为Chassis ID
private Map<String, NetworkNode> nodes;
// 存储所有唯一的Link对象
private Set<Link> links;
/**
* 私有构造函数,实现单例模式。
*/
private NetworkTopology() {
this.nodes = new ConcurrentHashMap<>();
// 使用HashSet确保Link的唯一性
this.links = Collections.synchronizedSet(new HashSet<>());
}
// 静态内部类 单例
private static class NetworkTopologyHolder {
private static final NetworkTopology instance = new NetworkTopology();
}
/**
* 获取NetworkTopology的单例实例。
*
* @return NetworkTopology的唯一实例。
*/
public static NetworkTopology getInstance() {
return NetworkTopologyHolder.instance;
}
/**
* 向拓扑中添加或更新一个NetworkNode。
* 如果已存在相同Chassis ID的节点,则会更新其信息。
*
* @param node 要添加的NetworkNode。
*/
public void addNode(NetworkNode node) {
nodes.put(node.getChassisId(), node);
}
/**
* 根据Chassis ID获取NetworkNode。
*
* @param chassisId 节点的Chassis ID。
* @return 对应的NetworkNode,如果不存在则返回null。
*/
public NetworkNode getNode(String chassisId) {
return nodes.get(chassisId);
}
/**
* 获取拓扑中所有NetworkNode的集合。
*
* @return 所有NetworkNode的集合。
*/
public Collection<NetworkNode> getNodes() {
return nodes.values();
}
/**
* 向拓扑中添加一条Link。Link会自动去重。
*
* @param link 要添加的Link。
*/
public void addLink(Link link) {
links.add(link);
}
/**
* 打印网络拓扑的概要信息,包括节点总数和每个节点的基本连接信息。
*/
public void printTopologySummary() {
log.debug("===== Network Topology Summary =====");
log.debug("Total Nodes: {}", nodes.size());
nodes.values().forEach(node -> {
log.debug(" Node: {} (ChassisID='{}', IP={}, Reachable={})", node.getSysName(), node.getChassisId(), node.getIp(), node.isReachable());
// 统计从该节点发出的唯一邻居链路数量
Map<String, Long> neighborLinkCounts = links.stream()
.filter(link -> link.getSourceChassisId().equals(node.getChassisId()))
.collect(Collectors.groupingBy(Link::getTargetChassisId, Collectors.counting()));
if (neighborLinkCounts.isEmpty()) {
log.debug("(No direct neighbors discovered)");
} else {
neighborLinkCounts.forEach((targetChassisId, count) -> {
NetworkNode targetNode = getNode(targetChassisId);
String targetIpStr = (targetNode != null && targetNode.getIp() != null) ? targetNode.getIp() : "N/A";
log.debug(" -> ChassisID '{}' (IP: {}) via {} link(s).", targetChassisId, targetIpStr, count);
});
}
});
}
/**
* 打印拓扑中所有唯一的详细链路信息。
*/
public void printDetailedLinks() {
log.debug("===== Detailed Network Links =====");
if (links.isEmpty()) {
log.debug("No links discovered.");
} else {
links.forEach(System.out::println);
}
System.out.println("Total Unique Links Found: " + links.size());
System.out.println("==================================\n");
}
/**
* 执行基本的环网状态检查。
* 该实现基于DFS算法,在无向图表示中检测环路。
* 注意:这是一个简化的环路检测,对于复杂的网络和链路聚合等可能需要更专业的图库。
*/
public Boolean checkRingStatus() {
log.debug("===== Ring Network Status Check =====");
// 构建基于Chassis ID的邻接列表,用于DFS
Map<String, Set<String>> adjacencyList = new HashMap<>();
for (NetworkNode node : nodes.values()) {
adjacencyList.put(node.getChassisId(), new HashSet<>());
}
// 从唯一的Link中填充邻接列表,为环路检测视为无向图
links.forEach(link -> {
// 只有当源节点和目标节点都存在于拓扑中时,才添加边
if (nodes.containsKey(link.getSourceChassisId()) && nodes.containsKey(link.getTargetChassisId())) {
adjacencyList.get(link.getSourceChassisId()).add(link.getTargetChassisId());
adjacencyList.get(link.getTargetChassisId()).add(link.getSourceChassisId()); // Treat as undirected for cycle check
}
});
System.out.println();
log.debug("Performing basic cycle detection (Chassis ID based DFS)...");
Set<String> visited = new HashSet<>(); // 记录DFS访问过的节点
Set<String> recursionStack = new HashSet<>(); // 记录当前DFS递归路径上的节点
boolean cycleDetected = false;
// 对每个未访问过的节点启动DFS,以确保遍历所有连通分量
for (String chassisId : adjacencyList.keySet()) {
if (!visited.contains(chassisId)) {
if (isCyclicDFS(chassisId, null, visited, recursionStack, adjacencyList)) {
cycleDetected = true;
// 发现一个环路就停止
break;
}
}
}
if (cycleDetected) {
log.warn(" !!! WARNING: Cycles detected in the network topology!");
} else {
log.debug(" No immediate cycles detected (based on basic DFS).");
}
return cycleDetected;
}
/**
* DFS辅助函数,用于在无向图中检测环路。
* 如果在递归栈中发现已访问的邻居节点(且不是直接父节点),则存在环路。
*
* @param currentChassisId 当前正在访问的节点Chassis ID。
* @param parentChassisId 在DFS树中当前节点的父节点Chassis ID,用于避免简单地回溯到父节点被误判为环路。
* @param visited 存储所有已访问过的节点Chassis ID的集合。
* @param recursionStack 存储当前递归路径中节点Chassis ID的集合。
* @param adjList 图的邻接列表表示。
* @return 如果检测到环路则返回true,否则返回false。
*/
private boolean isCyclicDFS(String currentChassisId, String parentChassisId,
Set<String> visited, Set<String> recursionStack,
Map<String, Set<String>> adjList) {
visited.add(currentChassisId);
recursionStack.add(currentChassisId);
// 遍历当前节点的所有邻居
for (String neighborChassisId : adjList.getOrDefault(currentChassisId, Collections.emptySet())) {
// 如果邻居是父节点,则跳过,因为这是无向图中的正常回溯
if (neighborChassisId.equals(parentChassisId)) {
continue;
}
// 如果邻居在当前递归栈中,则发现环路(回边)
if (recursionStack.contains(neighborChassisId)) {
return true;
}
// 如果邻居未被访问过,则对其进行递归DFS
if (!visited.contains(neighborChassisId)) {
if (isCyclicDFS(neighborChassisId, currentChassisId, visited, recursionStack, adjList)) {
return true;
}
}
}
// 当前节点DFS完成,从递归栈中移除
recursionStack.remove(currentChassisId);
return false;
}
}
- TopologyManager
package com.xxx.topology;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @version 1.0.0
* Created on 2025/6/12 11:47
*/
@Slf4j
public class TopologyManager {
// SNMP通信客户端
private final Snmp4jClient snmpClient;
// 发现链路的方法列表
private final List<IDiscoveryMethod> discoveryMethods;
// 记录已访问的Chassis ID,防止重复探测和无限循环
private final Set<String> visitedChassisIds;
// 全局Chassis ID -> IP地址的映射
private final Map<String, String> chassisIdToIpMap;
// 存储构建好的拓扑图的单例实例
private final NetworkTopology networkTopology;
public TopologyManager(Snmp4jClient snmpClient) {
this.snmpClient = snmpClient;
this.discoveryMethods = new ArrayList<>();
this.visitedChassisIds = Collections.synchronizedSet(new HashSet<>());
this.chassisIdToIpMap = new ConcurrentHashMap<>();
this.networkTopology = NetworkTopology.getInstance(); // 获取NetworkTopology的单例实例
}
public void addDiscoveryMethod(IDiscoveryMethod method) {
this.discoveryMethods.add(method);
}
public NetworkTopology discoverNetwork(List<String> seedIps) {
// 第一阶段:递归发现所有设备和其链路。
// 在此阶段,链路中的目标设备IP地址可能尚未完全填充。
for (String seedIp : seedIps) {
discoverDeviceAndNeighbors(seedIp);
}
// 第二阶段:后处理,遍历所有链路,填充远程设备的IP地址。
log.info("-- Post-processing: Resolving Remote IPs in Links ---");
resolveRemoteIpsInLinks();
return networkTopology;
}
private void discoverDeviceAndNeighbors(String ip) {
String chassisId = null;
String sysName = "Unknown";
boolean reachable = true;
try {
// 尝试获取设备的 ChassisId,这是设备在拓扑中的唯一标识
chassisId = snmpClient.snmpGet(ip, MibOidManager.LLDP_LOC_CHASSIS_ID_OID);
if (chassisId == null || chassisId.isEmpty()) {
log.error(" Could not retrieve local LLDP ChassisId for {}. Cannot uniquely identify this device in topology. Skipping.", ip);
// 如果无法获取 ChassisId,则认为设备无法有效被识别
reachable = false;
return;
}
// 检查Chassis ID是否已访问,防止重复探测和无限循环
if (visitedChassisIds.contains(chassisId)) {
log.debug("Device with ChassisId {} (IP: {}) already visited. Skipping.", chassisId, ip);
// 如果已访问,更新其NetworkNode中可能存在的IP地址,因为可能通过不同IP再次访问到
NetworkNode existingNode = networkTopology.getNode(chassisId);
if (existingNode != null) {
existingNode.setIp(ip);
// 既然能再次访问到,就标记为可达
existingNode.setReachable(true);
}
return;
}
// 标记 ChassisId 为已访问
visitedChassisIds.add(chassisId);
log.debug("Starting discovery for device: {} (ChassisId: {})", ip, chassisId);
// 获取设备系统名称
String name = snmpClient.snmpGet(ip, MibOidManager.SYS_NAME_OID);
if (name != null && !name.isEmpty()) {
sysName = name;
}
// 创建或更新当前设备的NetworkNode
NetworkNode currentNode = networkTopology.getNode(chassisId);
if (currentNode == null) {
// 如果节点不存在,则创建新节点
currentNode = new NetworkNode(chassisId, sysName, ip, reachable);
networkTopology.addNode(currentNode);
} else {
// 如果节点已存在,则更新其信息(例如,从其他路径发现的IP、名称或可达性)
currentNode.setReachable(reachable);
currentNode.setSysName(sysName);
currentNode.setIp(ip);
}
// 维护全局Chassis ID -> IP地址的映射
chassisIdToIpMap.put(chassisId, ip);
// 临时存储端口信息
Map<Integer, String> currentDevicePorts = new HashMap<>();
try {
// 首先获取所有接口的ifIndex
Map<String, String> ifIndexMap = snmpClient.snmpWalk(ip, MibOidManager.IF_INDEX_OID_COL);
log.debug("IF-MIB: ifIndexMap size: {} (OID: " + MibOidManager.IF_INDEX_OID_COL + ")", ifIndexMap.size());
if (ifIndexMap.isEmpty()) {
log.error("No interfaces found for {}. Skipping interface and link discovery for this device.", ip);
return;
}
// 一次性获取其他接口属性,这些Map的键都是完整OID
Map<String, String> ifDescrMap = snmpClient.snmpWalk(ip, MibOidManager.IF_DESCR_OID);
Map<String, String> ifOperStatusMap = snmpClient.snmpWalk(ip, MibOidManager.IF_OPER_STATUS_OID); // operStatus用于链路状态判断,此处不存入Node
Map<String, String> ifPhysAddressMap = snmpClient.snmpWalk(ip, MibOidManager.IF_PHYS_ADDRESS_OID); // MAC地址用于链路,此处不存入Node
// 遍历ifIndexMap,以ifIndex作为端口号的主要来源
NetworkNode finalCurrentNode = currentNode;
ifIndexMap.forEach((oid, indexValueStr) -> {
try {
// ifIndex 的值就是端口号
int portNum = Integer.parseInt(indexValueStr);
// 构造完整OID的后缀
String fullOidSuffix = "." + portNum;
String desc = ifDescrMap.get(MibOidManager.IF_DESCR_OID + fullOidSuffix);
// 将端口号和描述添加到当前 NetworkNode 中
finalCurrentNode.addPort(portNum, desc != null ? desc : "N/A");
} catch (NumberFormatException e) {
log.error("Failed to parse ifIndex value from OID: {}, value: {}. Error: {}", oid, indexValueStr, e.getMessage());
}
});
} catch (IOException e) {
log.error("Failed to discover interfaces for {}: {}", ip, e.getMessage());
// 如果接口发现失败,标记节点为不可达
currentNode.setReachable(false);
}
List<Link> currentDeviceDiscoveredLinks = new ArrayList<>();
for (IDiscoveryMethod method : discoveryMethods) {
try {
// 传入本地IP、Chassis ID和已发现的本地端口信息,获取链路
currentDeviceDiscoveredLinks.addAll(method.discover(snmpClient, ip, chassisId, currentNode.getPorts()));
} catch (IOException e) {
log.error("Discovery method failed for {} using {}: {}", ip, method.getClass().getSimpleName(), e.getMessage());
}
}
for (Link link : currentDeviceDiscoveredLinks) {
// 避免添加自环链路 (源Chassis ID与目标Chassis ID相同)
if (!link.getTargetChassisId().equals(link.getSourceChassisId())) {
networkTopology.addLink(link);
} else {
log.debug("Skipping self-loop link detected: " + link);
}
String targetChassisId = link.getTargetChassisId();
// 仅对LLDP发现的链路尝试递归,因为MAC_TABLE链路的目标是MAC地址,不直接对应Chassis ID
if (link.getDiscoveryMethod() == Link.DiscoveryMethod.LLDP && !visitedChassisIds.contains(targetChassisId)) {
// 尝试从全局chassisIdToIpMap中获取目标Chassis ID对应的IP,用于递归
String targetIp = chassisIdToIpMap.get(targetChassisId);
if (targetIp != null && !targetIp.isEmpty()) {
// 如果找到了IP且Chassis ID未被访问过,则递归发现邻居
discoverDeviceAndNeighbors(targetIp);
} else {
log.debug("LLDP: Target ChassisId {} IP not yet known to trigger immediate recursion.", targetChassisId);
// 对于IP未知但Chassis ID已知的LLDP邻居,如果其他路径能发现它,它最终也会被探测到
}
}
}
} catch (IOException e) {
log.error("Failed to discover device {}: {}", ip, e.getMessage());
// 如果设备不可达或SNMP通信失败,尝试更新其NetworkNode状态为不可达
if (chassisId != null) {
NetworkNode failedNode = networkTopology.getNode(chassisId);
if (failedNode == null) {
// 如果因为某种原因节点还未被添加到拓扑,则创建一个不可达的占位节点
failedNode = new NetworkNode(chassisId, sysName, ip, false);
networkTopology.addNode(failedNode);
} else {
failedNode.setReachable(false); // 更新为不可达
failedNode.setIp(ip); // 确保IP被记录
}
} else {
log.error(" Could not even get ChassisId for {}, skipping node creation/update.", ip);
}
}
}
private void resolveRemoteIpsInLinks() {
// 遍历拓扑中所有唯一的链路
for (Link link : networkTopology.getLinks()) {
// 获取链路的目标节点(NetworkNode)实例
NetworkNode targetNode = networkTopology.getNode(link.getTargetChassisId());
// 如果目标节点存在,并且其IP地址已知,则更新链路中的目标IP信息
if (targetNode != null && targetNode.getIp() != null && !targetNode.getIp().isEmpty()) {
log.debug(" Post-process: Target IP for link to {} is {}", link.getTargetChassisId(), targetNode.getIp());
} else {
log.debug(" Post-process: Target IP for link to {} not found in NetworkTopology. (Method: {}).", link.getTargetChassisId(), link.getDiscoveryMethod());
}
}
}
public void close() throws IOException {
snmpClient.close();
}
}
- TopologyDiscoveryApp
package com.xxx.topology;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @version 1.0.0
* Created on 2025/6/12 11:48
*/
public class TopologyDiscoveryApp {
public static void main(String[] args) {
List<String> seedIps = new ArrayList<>();
seedIps.add("100.65.109.21");
seedIps.add("100.65.109.26");
seedIps.add("100.65.109.31");
seedIps.add("100.65.109.36");
seedIps.add("100.65.109.41");
SnmpConfig snmpConfig = SnmpConfig.getDefault();
Snmp4jClient snmpClient = null;
try {
snmpClient = new Snmp4jClient(snmpConfig);
TopologyManager topologyManager = new TopologyManager(snmpClient);
topologyManager.addDiscoveryMethod(new LldpDiscoveryMethod());
NetworkTopology finalTopology = topologyManager.discoverNetwork(seedIps);
finalTopology.printTopologySummary();
finalTopology.printDetailedLinks();
finalTopology.checkRingStatus();
} catch (IOException e) {
} finally {
if (snmpClient != null) {
try {
snmpClient.close();
} catch (IOException e) {
System.err.println("Error closing SNMP client: " + e.getMessage());
}
}
}
}
}