业务场景
- 我们某市的客户,一直使用CS版本的信控平台,直接安装客户Windows server服务器上,主要对信号机设备进行在线管理、方案配时、管控等
- 其中有一项功能,在网络波动情况,对信号机管控失败,判断信号机是否在线。大致方法是直接调用Windows的dos窗口,发送 ping ip的命令,显示网络情况
- 我们新的信控平台使用Spring Cloud微服务架构,使用Spring Boot构建Java服务,使用google的jib插件打成docker镜像包
- 我们使用docker虚拟化部署,使用docker-compose统一管理所有服务,部署在Linux服务器里
- 客户很喜欢之前的功能,需要我们在新平台里实现这个功能,调用dos窗口,ping网络
- 而我们新平台是B/S架构,浏览器是很难调用Windows组件去弹出窗口实现ping功能,而且我们也没有限制一定使用的是Windows电脑访问,有网有浏览器就行
- ping功能无论Windows还是Linux,都是有的,至于界面展现,只能自己实现了
处理解决
实现ping功能并实时返回输出
- 代码实现,有两个核心功能点
- 一是根据不同的操作系统,执行对应的系统命令,进行结果接收与解析
- 对于第一个问题,Java有现成的类库,使用
Runtime.getRuntime().exec(ping命令)
即可 - 对于Windows服务器,需要使用
GB2312
解析命令执行结果 - 对于Linux 服务器,需要使用
UTF_8
解析命令执行结果 - 对于
ServletOutputStream.println
输出中文字符串报错Not an ISO 8859-1 character
问题,可以使用PrintWriter.println
输出代替 - 也可以在
ServletOutputStream.println
输出时输出字符数组(string.getBytes()
)
- 二是流式输出到请求端,模拟再现一秒一次的逐步展示的效果
- 对于第二个问题,核心是命令执行的结果输出流,要实时的返回给请求端,请求端能接收到
- 主要是获取流,然后按行读取,按行
flush()
即可返回给请求端 - 对于请求端实时渲染,需要在代码的
response
里指定ContentType
为text/event-stream
,这样flush刷新的返回流,才能实时被前端浏览器接收到(ChatGPT流式输出也是使用的这种content-type
) - 一开始是考虑使用
multipart
,完全不行,流flush后,浏览器无法获取,只能在流输出完成后,浏览器才能获取到
- 具体代码如下:
@GetMapping("/ping/start")
public void ping(String ip, Integer count, HttpServletResponse response) {
logger.info("ping 信号机【{}】 开始 ......", ip);
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
String line = null;
Process pro = null;
BufferedReader buf = null;
try {
if (null == count) {
count = 4;
}
String osName = System.getProperty("os.name");
if (osName.toLowerCase().contains("windows")){
pro = Runtime.getRuntime().exec("ping -n " + count + " " + ip);
buf = new BufferedReader(new InputStreamReader(pro.getInputStream(), "GB2312"));
} else if (osName.toLowerCase().contains("linux")){
pro = Runtime.getRuntime().exec("ping -c " + count + " " + ip);
buf = new BufferedReader(new InputStreamReader(pro.getInputStream(), StandardCharsets.UTF_8));
}
PrintWriter out = response.getWriter();
while (null != buf && (line = buf.readLine()) != null){
out.println(line);
out.flush();
}
logger.info("执行ping请求结束!");
out.close();
} catch (Exception e){
logger.error("执行ping命令出现异常");
e.printStackTrace();
}finally {
if (null != pro){
pro.destroy();
}
}
}
实现长ping和中断请求
- 主要是在请求时传输一个唯一命令id,缓存到内存里
- 当命令执行完成,或者接收到打断请求时,调用
destroy()
打断循环,结束请求 - 当然,可以尝试使用
kill -2
去模拟CTRL + C
的打断,可以使用Runtime.getRuntime().exec(中断命令)
打断试下,我的代码已经满足自己的需求了,就没再尝试,有兴趣的小伙伴可以试一下 - 具体代码如下:
package com.newatc.api.rest;
import com.newatc.api.signalcontrol.dto.PingRequestVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping(value = "/api/syscmd")
public class SysCmdController {
private static final Logger logger = LoggerFactory.getLogger(SysCmdController.class);
public static final Map<String, Boolean> COMMAND_REQUEST_MAP = new HashMap<>();
@PostMapping("/ping/start")
public void ping(@RequestBody PingRequestVO pingRequest, HttpServletResponse response) {
String ip = pingRequest.getIp();
String cmdId = pingRequest.getCmdId();
Integer count = pingRequest.getCount();
logger.info("ping 信号机【{}】 开始 ......", ip);
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/event-stream");
response.setCharacterEncoding("UTF-8");
String line = null;
Process pro = null;
BufferedReader buf = null;
try {
if (null == count) {
count = 4;
}
if (count > 50) {
count = 50;
}
String osName = System.getProperty("os.name");
if (osName.toLowerCase().contains("windows")){
pro = Runtime.getRuntime().exec("ping -n " + count + " " + ip);
buf = new BufferedReader(new InputStreamReader(pro.getInputStream(), "GB2312"));
} else if (osName.toLowerCase().contains("linux")){
pro = Runtime.getRuntime().exec("ping -c " + count + " " + ip);
buf = new BufferedReader(new InputStreamReader(pro.getInputStream(), StandardCharsets.UTF_8));
}
COMMAND_REQUEST_MAP.put(cmdId, true);
PrintWriter out = response.getWriter();
while (null != buf && (line = buf.readLine()) != null){
out.println(line);
out.flush();
if (!COMMAND_REQUEST_MAP.get(cmdId)) {
pro.destroy();
}
}
logger.info("执行ping请求结束!");
out.close();
} catch (Exception e){
logger.error("执行ping命令出现异常");
e.printStackTrace();
}finally {
if (null != pro){
pro.destroy();
}
COMMAND_REQUEST_MAP.remove(cmdId);
}
}
@PostMapping("/ping/stop")
public void ping(@RequestBody PingRequestVO requestVO) {
COMMAND_REQUEST_MAP.put(requestVO.getCmdId(), false);
}
}
docker容器找不到ping命令处理
- 我们打包导出的docker镜像,无法使用ping命令,报错,找不到这个命令
bash: ping:command not found
- 我们使用的是极简镜像
eclipse-temurin:11-jre-focal
,这个版本里的ubuntu
没有安装不需要的命令 - 具体可以参考我的这篇博文:《自制Java镜像发布到dockerhub公网使用》
- 也可以直接使用我发布到公网的包含ping命令的jre11镜像文件
1363241277/jre11:11-jre-focal
- 主要思路,就是打包使用的原始Java镜像里,要已经安装
ping
等需要的命令