mtr探测优化、临时路由添加异常完善
This commit is contained in:
@@ -9,11 +9,9 @@ import java.util.List;
|
|||||||
public class MtrVO {
|
public class MtrVO {
|
||||||
private String targetIp; // 目标IP
|
private String targetIp; // 目标IP
|
||||||
private String clientId; // 对应的clientId
|
private String clientId; // 对应的clientId
|
||||||
private Long policyId; // 策略ID
|
|
||||||
private double firstLossPercent; // 第一次探测丢包率
|
private double firstLossPercent; // 第一次探测丢包率
|
||||||
private double finalLossPercent; // 最终丢包率
|
private double finalLossPercent; // 最终丢包率
|
||||||
private long timestamp; // 探测时间戳
|
private long timestamp; // 探测时间戳
|
||||||
private boolean hasRetry; // 是否重试
|
|
||||||
private String errorMsg; // 错误信息
|
private String errorMsg; // 错误信息
|
||||||
private List<HopInfoVO> hopInfos; // 每跳路由信息列表
|
private List<HopInfoVO> hopInfos; // 每跳路由信息列表
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.tongran.agent.client.scheduler.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MTR线程池配置(优化版)
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class MtrThreadPoolConfig {
|
||||||
|
|
||||||
|
@Bean("mtrExecutor")
|
||||||
|
public ExecutorService mtrExecutor() {
|
||||||
|
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2; // 根据CPU核心数动态设置
|
||||||
|
int maxPoolSize = corePoolSize * 2;
|
||||||
|
int queueCapacity = 200;
|
||||||
|
long keepAliveTime = 30L; // 缩短空闲线程存活时间
|
||||||
|
|
||||||
|
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||||
|
corePoolSize,
|
||||||
|
maxPoolSize,
|
||||||
|
keepAliveTime,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<>(queueCapacity),
|
||||||
|
new MtrThreadFactory(),
|
||||||
|
new MtrRejectedExecutionHandler() // 自定义拒绝策略
|
||||||
|
);
|
||||||
|
|
||||||
|
// 允许核心线程超时销毁
|
||||||
|
executor.allowCoreThreadTimeOut(true);
|
||||||
|
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MTR线程工厂
|
||||||
|
*/
|
||||||
|
private static class MtrThreadFactory implements ThreadFactory {
|
||||||
|
private final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||||
|
private final String namePrefix = "mtr-worker-";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
Thread thread = new Thread(r, namePrefix + threadNumber.getAndIncrement());
|
||||||
|
thread.setDaemon(false); // 设置为非守护线程
|
||||||
|
thread.setPriority(Thread.NORM_PRIORITY);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MTR拒绝策略
|
||||||
|
*/
|
||||||
|
private static class MtrRejectedExecutionHandler implements RejectedExecutionHandler {
|
||||||
|
@Override
|
||||||
|
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||||
|
// 记录拒绝的线程信息
|
||||||
|
System.err.println("MTR任务被拒绝,线程池状态: " +
|
||||||
|
"活跃线程数: " + executor.getActiveCount() +
|
||||||
|
", 队列大小: " + executor.getQueue().size());
|
||||||
|
|
||||||
|
// 使用调用者线程执行,避免任务丢失
|
||||||
|
if (!executor.isShutdown()) {
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,57 @@
|
|||||||
package com.tongran.agent.client.service.impl;
|
package com.tongran.agent.client.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import com.alibaba.fastjson2.JSON;
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
|
||||||
import com.tongran.agent.client.core.eo.MtrPolicyConfigEO;
|
import com.tongran.agent.client.core.eo.MtrPolicyConfigEO;
|
||||||
|
import com.tongran.agent.client.core.vo.HopInfoVO;
|
||||||
import com.tongran.agent.client.core.vo.MtrVO;
|
import com.tongran.agent.client.core.vo.MtrVO;
|
||||||
import com.tongran.agent.client.core.vo.NetVO;
|
|
||||||
import com.tongran.agent.client.service.MtrService;
|
import com.tongran.agent.client.service.MtrService;
|
||||||
import com.tongran.agent.client.utils.AgentUtil;
|
|
||||||
import com.tongran.agent.client.utils.AssertLog;
|
import com.tongran.agent.client.utils.AssertLog;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import oshi.SystemInfo;
|
|
||||||
import oshi.hardware.HardwareAbstractionLayer;
|
|
||||||
import oshi.hardware.NetworkIF;
|
|
||||||
import oshi.util.FormatUtil;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MTR探测服务实现类
|
||||||
|
* 支持异步批量探测,解析每一跳的路由信息
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class MtrServiceImpl implements MtrService {
|
public class MtrServiceImpl implements MtrService {
|
||||||
|
|
||||||
|
private static final int MTR_TIMEOUT_MINUTES = 3;
|
||||||
|
private static final int RETRY_DELAY_MS = 30000;
|
||||||
|
private static final double LOSS_PERCENT_THRESHOLD = 5.0;
|
||||||
|
private static final int MTR_PACKET_COUNT = 10;
|
||||||
|
|
||||||
|
// 内网IP地址段正则表达式
|
||||||
|
private static final Pattern PRIVATE_IP_PATTERN = Pattern.compile(
|
||||||
|
"^(10\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.|192\\.168\\.|127\\.|169\\.254\\.|0\\.|255\\.)"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final OnlineIPQueryService onlineIPQueryService;
|
||||||
|
private final ExecutorService mtrExecutor;
|
||||||
|
|
||||||
|
// 通过构造器注入线程池
|
||||||
|
public MtrServiceImpl(OnlineIPQueryService onlineIPQueryService,
|
||||||
|
@Qualifier("mtrExecutor") ExecutorService mtrExecutor) {
|
||||||
|
this.onlineIPQueryService = onlineIPQueryService;
|
||||||
|
this.mtrExecutor = mtrExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行MTR探测列表 - 异步版本
|
* 执行MTR探测列表 - 异步版本
|
||||||
|
* @param timestamp 探测时间戳
|
||||||
|
* @param mtrPolicyConfigEOList 探测策略配置列表
|
||||||
|
* @return MTR探测结果列表
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public List<MtrVO> mtrList(long timestamp, List<MtrPolicyConfigEO> mtrPolicyConfigEOList) {
|
public List<MtrVO> mtrList(long timestamp, List<MtrPolicyConfigEO> mtrPolicyConfigEOList) {
|
||||||
List<CompletableFuture<MtrVO>> futures = new ArrayList<>();
|
List<CompletableFuture<MtrVO>> futures = new ArrayList<>();
|
||||||
List<MtrVO> mtrResults = new ArrayList<>();
|
List<MtrVO> mtrResults = new ArrayList<>();
|
||||||
@@ -37,37 +61,23 @@ public class MtrServiceImpl implements MtrService {
|
|||||||
return mtrResults;
|
return mtrResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为每个IP创建异步任务
|
// 为每个IP创建异步探测任务
|
||||||
for (MtrPolicyConfigEO policy : mtrPolicyConfigEOList) {
|
for (MtrPolicyConfigEO policy : mtrPolicyConfigEOList) {
|
||||||
if (!isPolicyTimeValid(policy, timestamp)) {
|
if (!isPolicyTimeValid(policy, timestamp)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (Map.Entry<String, List<String>> entry : policy.getClientIdToIpsMap().entrySet()) {
|
createMtrTasksForPolicy(policy, timestamp, futures);
|
||||||
String clientId = entry.getKey();
|
|
||||||
List<String> ips = entry.getValue();
|
|
||||||
if (ips != null) {
|
|
||||||
for (String ip : ips) {
|
|
||||||
String targetIp = ip.trim();
|
|
||||||
if (!targetIp.isEmpty()) {
|
|
||||||
// 异步执行每个IP的探测
|
|
||||||
CompletableFuture<MtrVO> future = CompletableFuture.supplyAsync(() ->
|
|
||||||
executeSingleMtrProbe(targetIp, clientId, timestamp)
|
|
||||||
);
|
|
||||||
futures.add(future);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待所有任务完成
|
// 等待所有异步任务完成
|
||||||
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
|
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
|
||||||
futures.toArray(new CompletableFuture[0])
|
futures.toArray(new CompletableFuture[0])
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取所有结果(设置超时时间,比如2分钟)
|
// 设置总超时时间
|
||||||
allFutures.get(2, TimeUnit.MINUTES);
|
allFutures.get(MTR_TIMEOUT_MINUTES, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
// 收集所有结果
|
||||||
for (CompletableFuture<MtrVO> future : futures) {
|
for (CompletableFuture<MtrVO> future : futures) {
|
||||||
MtrVO result = future.get();
|
MtrVO result = future.get();
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
@@ -82,20 +92,59 @@ public class MtrServiceImpl implements MtrService {
|
|||||||
return mtrResults;
|
return mtrResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为策略创建MTR探测任务
|
||||||
|
*/
|
||||||
|
private void createMtrTasksForPolicy(MtrPolicyConfigEO policy, long timestamp,
|
||||||
|
List<CompletableFuture<MtrVO>> futures) {
|
||||||
|
for (Map.Entry<String, List<String>> entry : policy.getClientIdToIpsMap().entrySet()) {
|
||||||
|
String clientId = entry.getKey();
|
||||||
|
List<String> ips = entry.getValue();
|
||||||
|
|
||||||
|
if (ips != null) {
|
||||||
|
for (String ip : ips) {
|
||||||
|
String targetIp = ip.trim();
|
||||||
|
if (!targetIp.isEmpty() && !isPrivateIp(targetIp)) {
|
||||||
|
CompletableFuture<MtrVO> future = CompletableFuture.supplyAsync(() ->
|
||||||
|
executeSingleMtrProbe(targetIp, clientId, timestamp), mtrExecutor
|
||||||
|
);
|
||||||
|
futures.add(future);
|
||||||
|
} else if (isPrivateIp(targetIp)) {
|
||||||
|
AssertLog.info("跳过内网IP探测: {}", targetIp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为内网IP
|
||||||
|
*/
|
||||||
|
private boolean isPrivateIp(String ip) {
|
||||||
|
if (ip == null || ip.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查常见的非IP地址情况
|
||||||
|
if (ip.equals("???") || ip.equals("*") || ip.equals("localhost")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正则表达式匹配内网IP段
|
||||||
|
return PRIVATE_IP_PATTERN.matcher(ip).find();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查策略时间是否有效
|
* 检查策略时间是否有效
|
||||||
*/
|
*/
|
||||||
private boolean isPolicyTimeValid(MtrPolicyConfigEO policy, long currentTimestamp) {
|
private boolean isPolicyTimeValid(MtrPolicyConfigEO policy, long currentTimestamp) {
|
||||||
try {
|
try {
|
||||||
// 将时间戳转换为Date对象进行比较
|
|
||||||
Date currentTime = new Date(currentTimestamp * 1000);
|
Date currentTime = new Date(currentTimestamp * 1000);
|
||||||
Date startTime = policy.getStartTime();
|
Date startTime = policy.getStartTime();
|
||||||
Date endTime = policy.getEndTime();
|
Date endTime = policy.getEndTime();
|
||||||
|
|
||||||
// 开始时间为空表示立即开始
|
// 开始时间为空表示立即开始,结束时间为空表示永不过期
|
||||||
boolean startValid = startTime == null || !currentTime.before(startTime);
|
boolean startValid = startTime == null || !currentTime.before(startTime);
|
||||||
// 结束时间为空表示永不过期
|
|
||||||
boolean endValid = endTime == null || !currentTime.after(endTime);
|
boolean endValid = endTime == null || !currentTime.after(endTime);
|
||||||
|
|
||||||
return startValid && endValid;
|
return startValid && endValid;
|
||||||
@@ -109,83 +158,73 @@ public class MtrServiceImpl implements MtrService {
|
|||||||
* 执行单个IP的MTR探测
|
* 执行单个IP的MTR探测
|
||||||
*/
|
*/
|
||||||
private MtrVO executeSingleMtrProbe(String targetIp, String clientId, long timestamp) {
|
private MtrVO executeSingleMtrProbe(String targetIp, String clientId, long timestamp) {
|
||||||
// 构建IP到clientId的反向映射
|
MtrVO mtrVO = createMtrVO(targetIp, clientId, timestamp);
|
||||||
MtrVO mtrVO = new MtrVO();
|
|
||||||
mtrVO.setClientId(clientId);
|
|
||||||
mtrVO.setTargetIp(targetIp);
|
|
||||||
mtrVO.setTimestamp(timestamp);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 第一次MTR探测
|
// 第一次MTR探测
|
||||||
double firstLossPercent = executeMtrCommand(targetIp);
|
List<HopInfoVO> firstHops = executeMtrCommand(targetIp);
|
||||||
|
double firstLossPercent = calculateFinalLossPercent(firstHops);
|
||||||
mtrVO.setFirstLossPercent(firstLossPercent);
|
mtrVO.setFirstLossPercent(firstLossPercent);
|
||||||
|
mtrVO.setHopInfos(firstHops);
|
||||||
|
|
||||||
// 如果丢包率超过5%,30秒后重试
|
// 如果丢包率超过阈值,进行重试探测
|
||||||
if (firstLossPercent > 5.0) {
|
if (firstLossPercent > LOSS_PERCENT_THRESHOLD) {
|
||||||
AssertLog.info("目标IP {} 丢包率 {}% > 5%,30秒后重试探测", targetIp, firstLossPercent);
|
AssertLog.info("目标IP {} 丢包率 {}% > {}%,{}秒后重试探测",
|
||||||
|
targetIp, firstLossPercent, LOSS_PERCENT_THRESHOLD, RETRY_DELAY_MS/1000);
|
||||||
|
|
||||||
// 等待30秒
|
Thread.sleep(RETRY_DELAY_MS);
|
||||||
Thread.sleep(30000);
|
|
||||||
|
|
||||||
// 第二次MTR探测
|
// 第二次MTR探测
|
||||||
double secondLossPercent = executeMtrCommand(targetIp);
|
List<HopInfoVO> finalHops = executeMtrCommand(targetIp);
|
||||||
mtrVO.setFinalLossPercent(secondLossPercent);
|
double finalLossPercent = calculateFinalLossPercent(finalHops);
|
||||||
mtrVO.setHasRetry(true);
|
mtrVO.setFinalLossPercent(finalLossPercent);
|
||||||
|
mtrVO.setHopInfos(finalHops);
|
||||||
|
|
||||||
AssertLog.info("目标IP {} 重试探测完成,最终丢包率: {}%", targetIp, secondLossPercent);
|
AssertLog.info("目标IP {} 重试探测完成,最终丢包率: {}%", targetIp, finalLossPercent);
|
||||||
} else {
|
} else {
|
||||||
mtrVO.setFinalLossPercent(firstLossPercent);
|
mtrVO.setFinalLossPercent(firstLossPercent);
|
||||||
mtrVO.setHasRetry(false);
|
|
||||||
AssertLog.info("目标IP {} 丢包率 {}%,无需重试", targetIp, firstLossPercent);
|
AssertLog.info("目标IP {} 丢包率 {}%,无需重试", targetIp, firstLossPercent);
|
||||||
}
|
}
|
||||||
return mtrVO;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
AssertLog.error("MTR探测异常 - IP: {}", targetIp, e);
|
handleMtrProbeError(mtrVO, targetIp, e);
|
||||||
mtrVO.setFinalLossPercent(-1.0); // -1表示探测失败
|
|
||||||
mtrVO.setErrorMsg(e.getMessage());
|
|
||||||
return mtrVO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mtrVO;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行mtr -r命令并解析结果
|
* 创建MTR结果对象
|
||||||
*/
|
*/
|
||||||
private double executeMtrCommand(String targetIp) {
|
private MtrVO createMtrVO(String targetIp, String clientId, long timestamp) {
|
||||||
|
MtrVO mtrVO = new MtrVO();
|
||||||
|
mtrVO.setTargetIp(targetIp);
|
||||||
|
mtrVO.setClientId(clientId);
|
||||||
|
mtrVO.setTimestamp(timestamp);
|
||||||
|
return mtrVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理MTR探测异常
|
||||||
|
*/
|
||||||
|
private void handleMtrProbeError(MtrVO mtrVO, String targetIp, Exception e) {
|
||||||
|
AssertLog.error("MTR探测异常 - IP: {}", targetIp, e);
|
||||||
|
mtrVO.setFinalLossPercent(-1.0);
|
||||||
|
mtrVO.setErrorMsg(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行mtr命令并解析结果
|
||||||
|
* @return 跳数信息列表
|
||||||
|
*/
|
||||||
|
private List<HopInfoVO> executeMtrCommand(String targetIp) {
|
||||||
Process process = null;
|
Process process = null;
|
||||||
try {
|
try {
|
||||||
// 执行mtr -r -c 10命令
|
ProcessBuilder processBuilder = new ProcessBuilder("mtr", "-r", "-c",
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder("mtr", "-r", "-c", "10", targetIp);
|
String.valueOf(MTR_PACKET_COUNT), targetIp);
|
||||||
process = processBuilder.start();
|
process = processBuilder.start();
|
||||||
|
|
||||||
// 读取命令输出
|
return parseMtrOutput(process);
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
String line;
|
|
||||||
String lastValidLine = null;
|
|
||||||
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
// 跳过标题行,记录最后一条有效数据行
|
|
||||||
if (!line.trim().isEmpty() &&
|
|
||||||
!line.startsWith("Start:") &&
|
|
||||||
!line.startsWith("HOST:") &&
|
|
||||||
Character.isDigit(line.trim().charAt(0))) {
|
|
||||||
lastValidLine = line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待命令执行完成
|
|
||||||
boolean finished = process.waitFor(2, TimeUnit.MINUTES);
|
|
||||||
if (!finished) {
|
|
||||||
process.destroy();
|
|
||||||
throw new RuntimeException("MTR命令执行超时");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastValidLine == null) {
|
|
||||||
throw new RuntimeException("MTR命令无有效输出");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析丢包率
|
|
||||||
return parseLossPercentFromMtrOutput(lastValidLine);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("MTR命令执行异常: " + e.getMessage());
|
throw new RuntimeException("MTR命令执行异常: " + e.getMessage());
|
||||||
@@ -197,21 +236,167 @@ public class MtrServiceImpl implements MtrService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从MTR输出中解析丢包率
|
* 解析MTR命令输出
|
||||||
*/
|
*/
|
||||||
private double parseLossPercentFromMtrOutput(String mtrOutputLine) {
|
private List<HopInfoVO> parseMtrOutput(Process process) throws Exception {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
String line;
|
||||||
|
List<HopInfoVO> hops = new ArrayList<>();
|
||||||
|
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (isValidHopLine(line)) {
|
||||||
|
HopInfoVO hop = parseHopLine(line);
|
||||||
|
if (hop != null) {
|
||||||
|
hops.add(hop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待命令执行完成
|
||||||
|
boolean finished = process.waitFor(MTR_TIMEOUT_MINUTES, TimeUnit.MINUTES);
|
||||||
|
if (!finished) {
|
||||||
|
process.destroy();
|
||||||
|
throw new RuntimeException("MTR命令执行超时");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hops.isEmpty()) {
|
||||||
|
throw new RuntimeException("MTR命令无有效输出");
|
||||||
|
}
|
||||||
|
|
||||||
|
return hops;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为有效的跳数行
|
||||||
|
*/
|
||||||
|
private boolean isValidHopLine(String line) {
|
||||||
|
return !line.trim().isEmpty() &&
|
||||||
|
!line.startsWith("Start:") &&
|
||||||
|
!line.startsWith("HOST:") &&
|
||||||
|
Character.isDigit(line.trim().charAt(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单跳数据行
|
||||||
|
*/
|
||||||
|
private HopInfoVO parseHopLine(String line) {
|
||||||
try {
|
try {
|
||||||
// 使用正则表达式匹配Loss%列
|
// 匹配格式: "1.|-- 26.10.194.38 20.0% 10 1.6 1.5 1.3 1.8 0.0"
|
||||||
Pattern pattern = Pattern.compile("(\\d+\\.?\\d*)%");
|
Pattern pattern = Pattern.compile(
|
||||||
Matcher matcher = pattern.matcher(mtrOutputLine);
|
"^\\s*(\\d+)\\.\\|--\\s+(\\S+)\\s+(\\d+\\.?\\d*)%\\s+(\\d+)\\s+(\\d+\\.\\d+)\\s+(\\d+\\.\\d+).*"
|
||||||
|
);
|
||||||
|
Matcher matcher = pattern.matcher(line);
|
||||||
|
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
return Double.parseDouble(matcher.group(1));
|
HopInfoVO hop = new HopInfoVO();
|
||||||
|
hop.setHopNumber(Integer.parseInt(matcher.group(1)));
|
||||||
|
|
||||||
|
String host = matcher.group(2);
|
||||||
|
setHostInfo(hop, host);
|
||||||
|
|
||||||
|
hop.setLossPercent(Double.parseDouble(matcher.group(3)));
|
||||||
|
hop.setAvgLatency(Double.parseDouble(matcher.group(6))); // 平均延迟
|
||||||
|
|
||||||
|
return hop;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("未找到Loss%数据: " + mtrOutputLine);
|
// 处理未知主机的情况(如???)
|
||||||
|
return parseUnknownHostLine(line);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("解析Loss%失败: " + e.getMessage());
|
throw new RuntimeException("解析MTR跳数数据失败: " + e.getMessage() + ", 行: " + line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 设置主机信息(IP地址解析和地理位置查询)
|
||||||
|
*/
|
||||||
|
private void setHostInfo(HopInfoVO hop, String host) {
|
||||||
|
if (isValidIpAddress(host)) {
|
||||||
|
hop.setIpAddress(host);
|
||||||
|
// 如果是内网IP,不进行地理位置查询
|
||||||
|
if (!isPrivateIp(host)) {
|
||||||
|
queryIpLocationInfo(hop, host);
|
||||||
|
} else {
|
||||||
|
setPrivateIpLocationInfo(hop, host);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hop.setHostname(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为有效的IP地址
|
||||||
|
*/
|
||||||
|
private boolean isValidIpAddress(String host) {
|
||||||
|
return host.matches("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}") && !host.equals("???");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为内网IP设置特殊的地理位置信息
|
||||||
|
*/
|
||||||
|
private void setPrivateIpLocationInfo(HopInfoVO hop, String ipAddress) {
|
||||||
|
hop.setCountry("内网");
|
||||||
|
hop.setProvince("内网");
|
||||||
|
hop.setCity("内网");
|
||||||
|
hop.setIsp("内网");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用在线服务查询IP地理位置信息
|
||||||
|
*/
|
||||||
|
private void queryIpLocationInfo(HopInfoVO hop, String ipAddress) {
|
||||||
|
try {
|
||||||
|
Map<String, String> locationInfo = onlineIPQueryService.queryIPLocation(ipAddress);
|
||||||
|
if (locationInfo != null) {
|
||||||
|
hop.setCountry(locationInfo.get("country"));
|
||||||
|
hop.setProvince(locationInfo.get("province"));
|
||||||
|
hop.setCity(locationInfo.get("city"));
|
||||||
|
hop.setIsp(locationInfo.get("isp"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
AssertLog.error("查询IP地理位置信息失败: {}", ipAddress, e);
|
||||||
|
// 设置默认值
|
||||||
|
setDefaultLocationInfo(hop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认地理位置信息
|
||||||
|
*/
|
||||||
|
private void setDefaultLocationInfo(HopInfoVO hop) {
|
||||||
|
hop.setCountry("未知");
|
||||||
|
hop.setProvince("未知");
|
||||||
|
hop.setCity("未知");
|
||||||
|
hop.setIsp("未知");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析未知主机行
|
||||||
|
*/
|
||||||
|
private HopInfoVO parseUnknownHostLine(String line) {
|
||||||
|
Pattern pattern = Pattern.compile("^\\s*(\\d+)\\.\\|--\\s+(\\S+).*");
|
||||||
|
Matcher matcher = pattern.matcher(line);
|
||||||
|
|
||||||
|
if (matcher.find()) {
|
||||||
|
HopInfoVO hop = new HopInfoVO();
|
||||||
|
hop.setHopNumber(Integer.parseInt(matcher.group(1)));
|
||||||
|
hop.setHostname(matcher.group(2));
|
||||||
|
hop.setLossPercent(0.0);
|
||||||
|
hop.setAvgLatency(0.0);
|
||||||
|
return hop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算最终丢包率(取最后一跳的丢包率)
|
||||||
|
*/
|
||||||
|
private double calculateFinalLossPercent(List<HopInfoVO> hops) {
|
||||||
|
if (CollectionUtil.isEmpty(hops)) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return hops.get(hops.size() - 1).getLossPercent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package com.tongran.agent.client.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线IP查询服务
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class OnlineIPQueryService {
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
// 内网IP地址段正则表达式
|
||||||
|
private static final Pattern PRIVATE_IP_PATTERN = Pattern.compile(
|
||||||
|
"^(10\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.|192\\.168\\.|127\\.|169\\.254\\.|0\\.|255\\.)"
|
||||||
|
);
|
||||||
|
|
||||||
|
public OnlineIPQueryService() {
|
||||||
|
this.restTemplate = new RestTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询IP地理位置信息 - 使用ip-api.com(免费无需key)
|
||||||
|
*/
|
||||||
|
public Map<String, String> queryIPLocation(String ip) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
|
||||||
|
// 如果是内网IP,直接返回内网标识
|
||||||
|
if (isPrivateIp(ip)) {
|
||||||
|
return createPrivateIpResult(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用ip-api.com免费服务
|
||||||
|
String url = "http://ip-api.com/json/" + ip + "?lang=zh-CN";
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("User-Agent", "Mozilla/5.0");
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(
|
||||||
|
url, HttpMethod.GET, entity, String.class);
|
||||||
|
|
||||||
|
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||||
|
return parseIpApiResponse(response.getBody(), ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 如果ip-api.com失败,尝试备用服务
|
||||||
|
return queryBackupService(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createUnknownResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为内网IP
|
||||||
|
*/
|
||||||
|
private boolean isPrivateIp(String ip) {
|
||||||
|
if (ip == null || ip.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查常见的非IP地址情况
|
||||||
|
if (ip.equals("???") || ip.equals("*") || ip.equals("localhost")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正则表达式匹配内网IP段
|
||||||
|
return PRIVATE_IP_PATTERN.matcher(ip).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析ip-api.com响应
|
||||||
|
*/
|
||||||
|
private Map<String, String> parseIpApiResponse(String response, String ip) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject json = JSON.parseObject(response);
|
||||||
|
|
||||||
|
if (!"success".equals(json.getString("status"))) {
|
||||||
|
return createUnknownResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.put("country", json.getString("country"));
|
||||||
|
result.put("province", json.getString("regionName"));
|
||||||
|
result.put("city", json.getString("city"));
|
||||||
|
result.put("isp", parseISP(json.getString("isp")));
|
||||||
|
result.put("query", ip);
|
||||||
|
result.put("source", "ip-api.com");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return createUnknownResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备用查询服务
|
||||||
|
*/
|
||||||
|
private Map<String, String> queryBackupService(String ip) {
|
||||||
|
try {
|
||||||
|
// 使用IP.SB作为备用服务
|
||||||
|
String url = "https://api.ip.sb/geoip/" + ip;
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("User-Agent", "Mozilla/5.0");
|
||||||
|
headers.set("Accept", "application/json");
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
ResponseEntity<String> response = restTemplate.exchange(
|
||||||
|
url, HttpMethod.GET, entity, String.class);
|
||||||
|
|
||||||
|
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||||
|
return parseIpSbResponse(response.getBody(), ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 备用服务也失败
|
||||||
|
}
|
||||||
|
|
||||||
|
return createUnknownResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析IP.SB响应
|
||||||
|
*/
|
||||||
|
private Map<String, String> parseIpSbResponse(String response, String ip) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject json = JSON.parseObject(response);
|
||||||
|
|
||||||
|
result.put("country", json.getString("country"));
|
||||||
|
result.put("province", json.getString("region"));
|
||||||
|
result.put("city", json.getString("city"));
|
||||||
|
result.put("isp", json.getString("isp"));
|
||||||
|
result.put("query", ip);
|
||||||
|
result.put("source", "ip.sb");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return createUnknownResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建内网IP结果
|
||||||
|
*/
|
||||||
|
private Map<String, String> createPrivateIpResult(String ip) {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
result.put("country", "内网");
|
||||||
|
result.put("province", "内网");
|
||||||
|
result.put("city", "内网");
|
||||||
|
result.put("isp", "内网");
|
||||||
|
result.put("query", ip);
|
||||||
|
result.put("source", "private");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化运营商名称
|
||||||
|
*/
|
||||||
|
private String parseISP(String isp) {
|
||||||
|
if (isp == null) return "未知";
|
||||||
|
|
||||||
|
if (isp.contains("电信") || isp.contains("China Telecom")) return "中国电信";
|
||||||
|
if (isp.contains("联通") || isp.contains("China Unicom")) return "中国联通";
|
||||||
|
if (isp.contains("移动") || isp.contains("China Mobile")) return "中国移动";
|
||||||
|
if (isp.contains("铁通")) return "中国铁通";
|
||||||
|
if (isp.contains("教育网")) return "教育网";
|
||||||
|
|
||||||
|
return isp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> createUnknownResult() {
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
result.put("country", "未知");
|
||||||
|
result.put("province", "未知");
|
||||||
|
result.put("city", "未知");
|
||||||
|
result.put("isp", "未知");
|
||||||
|
result.put("source", "unknown");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -557,11 +557,51 @@ public class AgentUtil {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dmidecode -s system-serial-number命令获取SN序列号
|
||||||
|
* @return SN序列号,如果获取失败则返回空字符串
|
||||||
|
*/
|
||||||
|
public static String getDeviceSN() {
|
||||||
|
BufferedReader reader = null;
|
||||||
|
try {
|
||||||
|
// 执行dmidecode命令获取系统序列号
|
||||||
|
Process process = Runtime.getRuntime().exec("dmidecode -s system-serial-number");
|
||||||
|
|
||||||
|
// 读取命令输出
|
||||||
|
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
String sn = reader.readLine();
|
||||||
|
|
||||||
|
// 等待命令执行完成
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
|
||||||
|
// 检查命令是否执行成功且输出不为空
|
||||||
|
if (exitCode == 0 && sn != null && !sn.trim().isEmpty()) {
|
||||||
|
return sn.trim();
|
||||||
|
} else {
|
||||||
|
AssertLog.error("Failed to get SN: Command exited with code " + exitCode);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 处理其他未知异常
|
||||||
|
AssertLog.error("Unexpected error while getting device SN: " + e.getMessage());
|
||||||
|
return "";
|
||||||
|
} finally {
|
||||||
|
// 确保关闭资源
|
||||||
|
if (reader != null) {
|
||||||
|
try {
|
||||||
|
reader.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
AssertLog.error("Error closing reader: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 获取 /etc/issue 文件的第二行内容
|
* 获取 /etc/issue 文件的第二行内容
|
||||||
* @return 第二行字符串,如果不存在则返回 null
|
* @return 第二行字符串,如果不存在则返回 null
|
||||||
*/
|
*/
|
||||||
public static String getDeviceSN() {
|
public static String getDeviceSNBak() {
|
||||||
Path issuePath = Paths.get("/etc/issue");
|
Path issuePath = Paths.get("/etc/issue");
|
||||||
if (!Files.exists(issuePath)) {
|
if (!Files.exists(issuePath)) {
|
||||||
System.err.println("文件不存在: /etc/issue");
|
System.err.println("文件不存在: /etc/issue");
|
||||||
|
|||||||
@@ -1,19 +1,46 @@
|
|||||||
package com.tongran.agent.client.utils;
|
package com.tongran.agent.client.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class NetworkUtil {
|
public class NetworkUtil {
|
||||||
public static boolean addRoute(String ip, String prefix, String gateway, String dev) {
|
public static boolean addRoute(String ip, String prefix, String gateway, String dev) {
|
||||||
try {
|
try {
|
||||||
ProcessBuilder pb = new ProcessBuilder("ip", "route", "add",
|
ProcessBuilder pb = new ProcessBuilder("ip", "route", "add",
|
||||||
ip + "/" + prefix, "via", gateway, "dev", dev);
|
ip + "/" + prefix, "via", gateway, "dev", dev);
|
||||||
pb.redirectErrorStream(true);
|
pb.redirectErrorStream(true);
|
||||||
Process p = pb.start();
|
Process p = pb.start();
|
||||||
return p.waitFor() == 0;
|
|
||||||
|
String output = readProcessOutput(p);
|
||||||
|
int exitCode = p.waitFor();
|
||||||
|
|
||||||
|
if (exitCode == 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// 检查是否是路由已存在的错误
|
||||||
|
if (output != null && (output.contains("File exists") || output.contains("RTNETLINK answers: File exists"))) {
|
||||||
|
AssertLog.info("Route already exists");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
AssertLog.error("Failed to add route: " + output);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
AssertLog.error("Exception while adding route: " + e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String readProcessOutput(Process p) {
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||||
|
return reader.lines().collect(Collectors.joining("\n"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return "Error reading output: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean deleteRoute(String ip, String prefix, String gateway, String dev) {
|
public static boolean deleteRoute(String ip, String prefix, String gateway, String dev) {
|
||||||
try {
|
try {
|
||||||
ProcessBuilder pb = new ProcessBuilder("ip", "route", "del",
|
ProcessBuilder pb = new ProcessBuilder("ip", "route", "del",
|
||||||
|
|||||||
Reference in New Issue
Block a user