我现在有多个服务,a是前端,b是java后端,还有很多个小java客户端。我的小客户端是部署在多台电脑上的,使用websocket执行代码,我现在想要的是过去小客户端的所有websocket要用b进行一个中转,即a<=>b<=>小客户端。
这是我的a的代码:
import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';
// npm remove stompjs
// npm install @stomp/stompjs sockjs-client --save
export default {
connect() {
return new Promise((resolve, reject) => {
const socket = new SockJS('http://10.228.73.15:31001/websocket');
const stompClient = new Client({
webSocketFactory: () => socket,
debug: () => {
// 禁用调试输出,或根据需要处理
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
stompClient.onConnect = (frame) => {
console.log('Connected:', frame);
resolve(stompClient);
// 连接成功后立即订阅个人主题
// stompClient.subscribe(`/topic/commandOutput/${userId}`, (message) => {
// console.log(1,message)
// });
};
stompClient.onStompError = (error) => {
console.error('Connection error:', error);
reject(error);
};
stompClient.activate();
});
}
};<template>
<div>
<span style="display: flex; justify-content: center; margin-bottom: 8px">
<img alt="Vue logo" src="../assets/logo.png" style="width: 90px;">
</span>
<span style="display: flex; justify-content: center; margin-bottom: 8px">
当前登录用户:
<input v-model="userId" placeholder="请输入p13" @keyup.enter="connectWebSocket">
<button @click="connectWebSocket" style="margin-left: 10px;">登录</button>
</span>
<span style="display: flex; justify-content: center">
<input
ref="commandInput"
v-model="currentCommand"
style="width: 600px"
placeholder="输入任意命令(如:ipconfig / dir)"
:disabled="isExecuting"
@keyup.enter="sendCommand"
>
<button @click="sendCommand" style="margin-left: 10px;" :disabled="isExecuting">执行命令</button>
<button
@click="abortCommand"
style="margin-left: 10px; background: #ff4d4f"
:disabled="!isExecuting"
>
中止命令
</button>
</span>
<div style="display: flex">
<div style="margin-top: 20px; width: 30%;">
<h3 style="text-align: center">已执行命令</h3>
<div style="height: 482px; overflow-y: auto; background: #f8f8f8;">
<div
v-for="(msg, index) in messages"
:key="index"
style="margin-top: 8px; margin-left: 8px; border-radius: 4px; overflow-y: auto;"
>
{{ msg }}
</div>
</div>
</div>
<div style="padding: 20px; width: 70%">
<h3 style="text-align: center">实时命令输出</h3>
<pre
ref="outputPre"
style="background: #282c34; color: #fff; padding: 15px; border-radius: 4px; height: 450px; overflow-y: auto"
>
{{ currentOutput }}
</pre>
</div>
</div>
</div>
</template>
<script>
import websocket from '@/utils/websocket'
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
currentCommand: '',
messages: [],
currentOutput: '',
stompClient: null,
index: 1,
userId: '',
isExecuting: false // 控制命令是否正在执行
};
},
methods: {
async connectWebSocket() {
alert("登录成功");
try {
this.stompClient = await websocket.connect(this.userId);
this.$nextTick(() => {
this.$refs.commandInput.focus(); // 命令结束后重新聚焦到输入框
});
const subscription = this.stompClient.subscribe(`/topic/commandOutput/${this.userId}`, (message) => {
const output = message.body;
if (output.startsWith("⏹")) {
this.isExecuting = false; // 命令执行结束,允许用户继续输入
this.$nextTick(() => {
this.$refs.commandInput.focus(); // 命令结束后重新聚焦到输入框
});
return;
}
this.currentOutput += output.trim() + '\n';
this.$nextTick(() => {
if (this.$refs.outputPre) {
this.$refs.outputPre.scrollTop = this.$refs.outputPre.scrollHeight;
}
});
});
this.stompClient.onDisconnect = () => {
subscription.unsubscribe();
};
// 检查命令执行状态
await this.checkCommandStatus();
} catch (error) {
console.error('WebSocket连接失败:', error);
}
},
async checkCommandStatus() {
try {
const response = await fetch(`/win/status/${this.userId}`);
if (!response.ok) {
console.error('无法获取命令状态');
return;
}
this.isExecuting = await response.json(); // 更新前端状态
if (this.isExecuting) {
alert("上一个命令尚未完成,请等待或中止命令!");
}
} catch (error) {
console.error('检查命令状态时出错:', error);
}
},
sendCommand() {
if (this.userId === '') {
alert("请输入当前登录用户");
return;
}
this.isExecuting = true;
if (this.currentCommand.trim() && this.stompClient) {
// 删除 currentOutput 的最后一个非空行
if (this.currentOutput) {
let lastNewLineIndex = this.currentOutput.lastIndexOf('\n');
let lastNonEmptyIndex = -1;
// 从后往前遍历,找到最后一个非空行的起始位置
for (let i = this.currentOutput.length - 1; i >= 0; i--) {
if (this.currentOutput[i] === '\n') {
// 如果当前字符是换行符,并且之前的字符不是空白字符,则记录位置
if (lastNewLineIndex !== -1 && i < lastNewLineIndex) {
lastNonEmptyIndex = lastNewLineIndex;
break;
}
lastNewLineIndex = i;
} else if (this.currentOutput[i].trim() !== '') {
// 找到非空字符,更新 lastNewLineIndex
lastNewLineIndex = i;
}
}
// 如果找到了非空行,则删除它
if (lastNonEmptyIndex !== -1) {
const nextNewLineIndex = this.currentOutput.indexOf('\n', lastNonEmptyIndex);
if (nextNewLineIndex === -1) {
this.currentOutput = this.currentOutput.substring(0, lastNonEmptyIndex);
} else {
this.currentOutput = this.currentOutput.substring(0, lastNonEmptyIndex) + this.currentOutput.substring(nextNewLineIndex);
}
}
// 删除最后一个空行
if (this.currentOutput.endsWith('\n')) {
this.currentOutput = this.currentOutput.substring(0, this.currentOutput.length - 1);
}
}
// 发送命令
this.stompClient.publish({
destination: '/app/executeCommand',
body: JSON.stringify({
command: this.currentCommand.trim(),
//todo 之后改为动态
host: '10.228.73.15',
userId: this.userId
}),
});
this.messages.push(this.index + '. ' + this.currentCommand);
this.index += 1;
this.currentCommand = '';
}
},
async abortCommand() {
if (this.stompClient) {
const response = await fetch(`/win/handleAbort/${this.userId}`);
if (response){
this.messages.push("已发送中止命令");
}
}
},
onTabChange(key, type) {
this[type] = key;
},
},
beforeDestroy() {
if (this.stompClient) {
this.stompClient.deactivate();
}
}
}
</script>
<style scoped>
pre {
white-space: pre-line;
background-color: #f4f4f4;
padding: 10px;
border: 1px solid #ddd;
}
</style>
这是我的小客户端的代码:
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}package com.example.demo.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.dao.entity.WExecuteHost;
import com.example.demo.dao.entity.WHostProcess;
import com.example.demo.dao.entity.WPersonalHost;
import com.example.demo.dao.mapper.WExecuteHostMapper;
import com.example.demo.request.ProcessRequest;
import com.example.demo.service.WExecuteHostService;
import com.example.demo.service.WHostProcessService;
import com.example.demo.service.WPersonalHostService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import javax.annotation.PreDestroy;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class WExecuteHostServiceImpl extends ServiceImpl<WExecuteHostMapper, WExecuteHost>
implements WExecuteHostService {
@Autowired
private WPersonalHostService wPersonalHostService;
@Autowired
private WHostProcessService wHostProcessService;
@Data
private static class UserSession {
private Process cmdProcess;
private volatile boolean isProcessRunning;
private String sessionId;
private String logFilePath;
private final Queue<String> logBuffer = new ConcurrentLinkedQueue<>();
private static final int BATCH_SIZE = 50; // 批量写入阈值
// 命令执行状态锁
private final AtomicBoolean isExecuting = new AtomicBoolean(false);
}
private final Map<String, UserSession> userSessions = new ConcurrentHashMap<>();
private final SimpMessagingTemplate messagingTemplate;
// 异步日志写入线程池
private static final ExecutorService LOG_WRITER_POOL = Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setNamePrefix("log-writer-").build());
// 日志刷新调度器
private static final ScheduledExecutorService LOG_FLUSH_SCHEDULER =
Executors.newSingleThreadScheduledExecutor();
public WExecuteHostServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
startLogFlushService();
}
// 初始化日志刷新服务
private void startLogFlushService() {
LOG_FLUSH_SCHEDULER.scheduleAtFixedRate(() -> {
userSessions.values().forEach(session -> {
if (!session.getLogBuffer().isEmpty()) {
List<String> batch = CollUtil.newArrayList(session.getLogBuffer());
session.getLogBuffer().clear();
asyncWriteLog(session.getLogFilePath(), String.join("\n", batch));
}
});
}, 0, 1, TimeUnit.SECONDS);
}
@Override
public void executeCommand(String command, String host, String userId) {
// 0. ABORT命令特殊处理(优先处理终止请求)
if ("ABORT".equalsIgnoreCase(command)) {
handleAbort(userId);
return;
}
// 1. 权限校验
if (!validateUserHost(userId, host)) {
sendError("无权访问该主机", userId);
return;
}
// 2. 检查用户当前会话状态
UserSession session = userSessions.get(userId);
if (session != null && session.isExecuting.get()) {
sendError("已有命令执行中,请等待完成或使用ABORT终止", userId);
return;
}
// 3. 创建新会话(带原子状态检查)
session = userSessions.computeIfAbsent(userId, key -> {
UserSession newSession = createNewSession(userId, host);
if (newSession != null) {
newSession.isExecuting.set(true); // 标记为执行中
}
return newSession;
});
if (session == null) return;
// 4. 写入日志并执行命令
try {
// 确保获得执行锁
if (!session.isExecuting.compareAndSet(true, true)) {
sendError("命令执行冲突,请重试", userId);
return;
}
session.getLogBuffer().offer("——————————————— " + DateUtil.now() + " ———————————————");
// 发送命令到进程
IoUtil.write(session.getCmdProcess().getOutputStream(),
Charset.forName("GBK"), true, command + "\n");
} catch (Exception e) {
session.isExecuting.set(false); // 发生异常时释放锁
sendError("命令发送失败: " + e.getMessage(), userId);
}
}
@Override
public Boolean isCommandExecuting(String userId) {
UserSession session = userSessions.get(userId);
return session != null && session.isExecuting.get();
}
@Override
public void handleAbort(String userId) {
UserSession session = userSessions.get(userId);
if (session == null || session.getCmdProcess() == null) {
sendError("没有活动的命令进程", userId);
return;
}
try {
long pid = session.getCmdProcess().pid();
System.out.println("Attempting to kill process with PID: " + pid);
// 使用 taskkill 命令终止进程
ProcessBuilder taskKill = new ProcessBuilder("taskkill", "/F", "/T", "/PID", String.valueOf(pid));
Process killProcess = taskKill.start();
// 等待命令执行完成
int exitCode = killProcess.waitFor();
System.out.println("taskkill exit code: " + exitCode);
if (exitCode == 0) {
// 进程终止成功
session.isExecuting.set(false);
cleanupSession(userId);
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "✔️" + "进程已通过 taskkill 终止 (PID: " + pid + ")");
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "");
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, System.getProperty("user.dir") + ">");
} else {
// 进程终止失败
sendError("终止进程失败,错误码: " + exitCode, userId);
}
} catch (IOException | InterruptedException e) {
System.err.println("Error killing process: " + e.getMessage());
sendError("终止进程失败: " + e.getMessage(), userId);
}
}
@Override
public String startProcess(ProcessRequest processRequest) {
try {
// 数据库表新增数据
String id = processRequest.getId();
String p13 = processRequest.getP13().trim();
String processName = processRequest.getProcessName().trim();
String productNumber = processRequest.getProductNumber().trim();
String executeHost = processRequest.getExecuteHost().trim();
String department = processRequest.getDepartment().trim();
String version = processRequest.getVersion().trim();
String type = processRequest.getType();
boolean saveOrUpdateResult;
if (type.equals("新增")) {
// 判断产品编号是否唯一
LambdaQueryWrapper<WHostProcess> processWrapper = new LambdaQueryWrapper<>();
processWrapper.eq(WHostProcess::getProductNumber, productNumber);
WHostProcess process = wHostProcessService.getOne(processWrapper);
if (process != null) {
return "该产品编号已被他人使用,请使用其他的产品编号。";
}
if (StrUtil.isEmpty(p13) || StrUtil.isEmpty(processName) || StrUtil.isEmpty(productNumber)
|| StrUtil.isEmpty(executeHost) || StrUtil.isEmpty(department) || StrUtil.isEmpty(version)) {
return "新增进程失败。";
}
WHostProcess wHostProcess = new WHostProcess();
wHostProcess.setP13(p13);
wHostProcess.setProcessName(processName);
wHostProcess.setProductNumber(productNumber);
wHostProcess.setHost(executeHost);
wHostProcess.setDepartment(department);
wHostProcess.setState("离线");
wHostProcess.setVersion(version);
wHostProcess.setBeginTime(new Date());
saveOrUpdateResult = wHostProcessService.save(wHostProcess);
} else {
// 执行更新操作
WHostProcess wHostProcess = wHostProcessService.getById(id);
// 判断产品编号是否唯一
if (!wHostProcess.getProductNumber().equals(productNumber)) {
LambdaQueryWrapper<WHostProcess> processWrapper = new LambdaQueryWrapper<>();
processWrapper.eq(WHostProcess::getProductNumber, productNumber);
WHostProcess process = wHostProcessService.getOne(processWrapper);
if (process != null) {
return "该产品编号已被他人使用,请使用其他的产品编号。";
}
}
wHostProcess.setProcessName(processName);
wHostProcess.setProductNumber(productNumber);
wHostProcess.setHost(executeHost);
wHostProcess.setDepartment(department);
wHostProcess.setState("离线");
wHostProcess.setVersion(version);
wHostProcess.setUpdateTime(new Date());
saveOrUpdateResult = wHostProcessService.updateById(wHostProcess);
}
if (saveOrUpdateResult) {
LambdaQueryWrapper<WPersonalHost> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper
.eq(WPersonalHost::getExecuteHost, processRequest.getExecuteHost())
.eq(WPersonalHost::getSharedHost, processRequest.getSharedHost());
WPersonalHost wPersonalHost = wPersonalHostService.getOne(queryWrapper);
// 执行py启动命令
//todo 后续动态
String pythonEXEPath = "D:\\miniforge\\envs" + File.separator + p13
+ File.separator + p13 + "_python" + wPersonalHost.getPythonEnv() + File.separator + "python.exe -u";
String mainPyPath = System.getProperty("user.dir") + File.separator + "python-package"
+ File.separator + executeHost + File.separator + p13 + File.separator + "test" + File.separator + "main.py";
this.executeCommand(pythonEXEPath + " " + mainPyPath,
processRequest.getExecuteHost(),
processRequest.getP13());
return "正在启动项目...";
}
} catch (Exception e) {
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
return "新增进程失败。";
}
@Override
public String stopProcess(ProcessRequest processRequest) {
try {
//todo 后续动态
String account = "fangpeiyuan";
Integer pid = processRequest.getPid();
LambdaQueryWrapper<WHostProcess> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper
.eq(WHostProcess::getP13, account)
.eq(WHostProcess::getPid, pid);
WHostProcess wHostProcess = wHostProcessService.getOne(queryWrapper);
if (wHostProcess == null) {
return "当前进程终止失败,请联系管理员!";
}
// 执行终止命令并获取返回值
Process process = Runtime.getRuntime().exec(
"taskkill /F /PID \"" + pid + "\"");
int exitCode = process.waitFor();
if (exitCode == 0) {
wHostProcess.setState("离线");
wHostProcess.setPid(null);
wHostProcess.setUpdateTime(DateUtil.date());
wHostProcessService.updateById(wHostProcess);
return "进程终止成功。";
} else {
// 获取错误流信息(可选)
BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()));
String errorLine;
StringBuilder errorMessage = new StringBuilder();
while ((errorLine = errorReader.readLine()) != null) {
errorMessage.append(errorLine).append("\n");
}
return "进程终止失败,错误码: " + exitCode +
(errorMessage.length() > 0 ? ",错误信息: " + errorMessage : "");
}
} catch (Exception e) {
e.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return "当前进程终止失败: " + e.getMessage();
}
}
private boolean validateUserHost(String userId, String executeHost) {
LambdaQueryWrapper<WPersonalHost> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(WPersonalHost::getP13, userId)
.eq(WPersonalHost::getExecuteHost, executeHost)
.eq(WPersonalHost::getState, "在线");
return wPersonalHostService.getOne(queryWrapper) != null;
}
private UserSession createNewSession(String userId, String executeHost) {
try {
UserSession session = new UserSession();
session.setSessionId(UUID.randomUUID().toString());
session.setLogFilePath(initLogFile(userId, executeHost));
// 启动CMD进程(带唯一标题)
ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/k", "title " + generateUniqueTitle(userId));
session.setCmdProcess(pb.redirectErrorStream(true).start());
// 启动输出监听线程
startOutputThread(session, userId);
return session;
} catch (IOException e) {
sendError("进程启动失败: " + e.getMessage(), userId);
return null;
}
}
private String initLogFile(String userId, String executeHost) {
// 1. 构建基础路径(使用File.separator)
String baseDir = FileUtil.normalize(
System.getProperty("user.dir") + File.separator + "command-log");
// 2. 构建安全路径(统一使用File.separator)
String safePath = FileUtil.normalize(
baseDir + File.separator + userId + File.separator + executeHost
+ File.separator + "项目名称");
// 3. 安全校验(现在路径分隔符一致)
if (!safePath.startsWith(baseDir)) {
throw new SecurityException("非法日志路径: " + safePath);
}
// 4. 创建目录(自动处理路径分隔符)
FileUtil.mkdir(safePath);
// 5. 生成日志文件
String logFileName = DateUtil.today() + ".log";
return FileUtil.touch(safePath + File.separator + logFileName).getAbsolutePath();
}
private void startOutputThread(UserSession session, String userId) {
new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(session.getCmdProcess().getInputStream(), Charset.forName("GBK")))) {
String line;
while ((line = reader.readLine()) != null) {
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, line);
session.getLogBuffer().offer(line);
}
} catch (Exception e) {
sendError("输出流异常: " + e.getMessage(), userId);
} finally {
session.isExecuting.set(false); // 命令结束释放锁
cleanupSession(userId);
// 通知前端命令执行结束
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "⏹ 该命令执行已结束");
}
}).start();
}
private void asyncWriteLog(String logFilePath, String content) {
CompletableFuture.runAsync(() -> {
try {
// 替换掉日志文件中没用的信息,如多余的路径信息
String currentDir = System.getProperty("user.dir");
String escapedDir = currentDir.replace("\\", "\\\\");
String regex = "(?m)^\\s*" + escapedDir + ">(?!\\S)\\s*";
// 创建 Pattern 和 Matcher 对象
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content);
// 检查是否存在匹配的模式
if (matcher.find()) {
// 如果存在匹配,则进行替换
String cleaned = content.replaceAll(regex, "");
FileUtil.appendString(cleaned + System.lineSeparator(), logFilePath, Charset.forName("GBK"));
} else {
FileUtil.appendString(content + System.lineSeparator(), logFilePath, Charset.forName("GBK"));
}
} catch (Exception e) {
System.err.println("日志写入失败: " + e.getMessage());
}
}, LOG_WRITER_POOL);
}
private void cleanupSession(String userId) {
UserSession session = userSessions.remove(userId);
if (session != null) {
try {
if (session.getCmdProcess() != null) {
session.getCmdProcess().destroyForcibly();
}
// 强制将剩余日志写入文件(新增代码)
if (!session.getLogBuffer().isEmpty()) {
asyncWriteLog(session.getLogFilePath(), String.join("\n", session.getLogBuffer()));
session.getLogBuffer().clear();
}
} catch (Exception ignored) {
}
}
}
@PreDestroy
public void cleanup() {
LOG_FLUSH_SCHEDULER.shutdown();
LOG_WRITER_POOL.shutdown();
userSessions.forEach((userId, session) -> {
try {
if (session.getCmdProcess() != null) {
session.getCmdProcess().destroyForcibly();
}
} catch (Exception ignored) {
}
});
userSessions.clear();
}
/**
* 发送错误日志
*/
private void sendError(String message, String userId) {
try {
messagingTemplate.convertAndSend("/topic/commandOutput/" + userId, "❌" + message);
} catch (Exception ignored) {
}
}
/**
* 生成cmd窗口唯一id
*/
private String generateUniqueTitle(String userId) {
return "CMD_SESSION_" + userId + "_" + System.currentTimeMillis();
}
}
请参考这两个,帮我实现b作为中转需要的代码,以及a和小客户端需要修改的东西,不要什么心跳检测等复杂的东西,先简单实现,但是代码要给全