本文在 Springmvc+Websocket 发送JSON消息实例 的基础上,添加了异步运行 (使用Thread) 的自制 Shell 类封装包和异步运行时用到的 AsyncShellCallback.java (它是Shell包里Callback接口的执行类),主要修改了 Home.jsp 和 WSTextHandler.java。
1. 开发环境
2. 在 IDEA上创建项目
3. 使用 tomcat7-maven-plugin, 将 tomcat 内嵌运行
4. 导入 spring-webmvc, Servlet & JSTL, websocket, fastjson
5. 支持 SpringMVC
6. 支持静态资源 (html/js/css/images)
以上步骤 1 到 6,请参考 Springmvc+Websocket 发送JSON消息实例
7. Shell 类封装包
1) 添加 src/main/java/com/example/shell/Platform.java
package com.example.shell;
public enum Platform {
WINDOWS, LINUX, MACOS
}
2) 添加 src/main/java/com/example/shell/Callback.java
package com.example.shell;
public interface Callback {
public void showMessage(String str) throws Exception;
public void showError(String str) throws Exception;
public void showFinish(String str) throws Exception;
}
3) 添加 src/main/java/com/example/shell/AsyncShellExecute.java
package com.example.shell;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class AsyncShellExecute extends Thread {
volatile int processStatus = 0; // 0 - PIPE ready; 1 - PIPE busying; 2 - PIPE stopping;
volatile int closeStatus = 0;
private String strCommand = "";
private Platform platform = null;
public void setCommand(String strCmd, Platform platform) {
this.strCommand = strCmd;
this.platform = platform;
}
@Override
public void run() {
String errorMessage = "";
try {
String strCmd = "";
if (this.platform == Platform.WINDOWS) {
strCmd = "cmd /c " + this.strCommand;
} else if (platform == Platform.MACOS) {
strCmd = this.strCommand;
} else {
strCmd = this.strCommand;
}
this.processStatus = 1;
Runtime rt = Runtime.getRuntime();
Process p = rt.exec(strCmd);
String line;
BufferedReader dataReader = new BufferedReader(new InputStreamReader(p.getInputStream(), "GBK"));
while ((line = dataReader.readLine()) != null) {
if (this.processStatus == 2) {
//System.out.println("ShellAsyncExecute -> run(1): this.processStatus == 2");
break;
}
this.callback.showMessage(line);
}
if (this.processStatus != 2) {
BufferedReader errorReader = new BufferedReader(new InputStreamReader(p.getErrorStream(), "GBK"));
while ((line = errorReader.readLine()) != null) {
if (this.processStatus == 2) {
//System.out.println("ShellAsyncExecute -> run(2): this.processStatus == 2");
break;
}
this.callback.showMessage(line);
}
}
//System.out.println("AsyncShellExecute -> run(3): this.closeStatus = " + this.closeStatus);
if (this.closeStatus == 0) {
if (this.processStatus != 2) {
int ret = p.waitFor();
if (ret != 0) {
this.callback.showError("Execute '" + this.strCommand + "' failed");
} else {
this.callback.showFinish("Finish after execute '" + this.strCommand + "' successfully");
}
} else {
this.callback.showFinish("Finish after terminate process");
}
}
p.destroy();
} catch (IOException e) {
errorMessage = e.getMessage();
} catch (Exception e2) {
errorMessage = e2.getMessage();
}
if (!errorMessage.isEmpty() && this.closeStatus == 0) {
try {
this.callback.showError(errorMessage);
} catch (Exception e) {
e.printStackTrace();
}
}
this.processStatus = 0;
this.closeStatus = 0;
}
public void terminate(int closeStatus) {
if (this.processStatus == 1)
this.processStatus = 2;
this.closeStatus = closeStatus;
}
public int GetProcessStatus() {
return this.processStatus;
}
public Callback callback;
}
8. 支持 spring-websocket
1) 添加 src/main/java/com/example/ws/AsyncShellCallback.java
package com.example.ws;
import java.util.Map;
import java.util.HashMap;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import com.alibaba.fastjson.JSON;
import com.example.shell.Callback;
public class AsyncShellCallback implements Callback {
private WebSocketSession session = null;
public AsyncShellCallback(WebSocketSession session) {
this.session = session;
}
@Override
public void showMessage(String str) throws Exception {
Map<String, Object> msgMap = new HashMap<>();
msgMap.put("ret", "data");
msgMap.put("message", str);
session.sendMessage(new TextMessage(JSON.toJSON(msgMap).toString()));
}
@Override
public void showError(String str) throws Exception {
Map<String, Object> errMap = new HashMap<>();
errMap.put("ret", "error");
errMap.put("description", str);
session.sendMessage(new TextMessage(JSON.toJSON(errMap).toString()));
}
@Override
public void showFinish(String str) throws Exception {
Map<String, Object> errMap = new HashMap<>();
errMap.put("ret", "finish");
errMap.put("description", str);
session.sendMessage(new TextMessage(JSON.toJSON(errMap).toString()));
}
}
2) 添加 src/main/java/com/example/ws/WSInterceptor.java
package com.example.ws;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import java.util.Map;
public class WSInterceptor extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Exception ex) {
super.afterHandshake(request, response, wsHandler, ex);
}
}
3) 添加 src/main/java/com/example/ws/WSTextHandler.java
package com.example.ws;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.shell.Platform;
import com.example.shell.AsyncShellExecute;
public class WSTextHandler extends TextWebSocketHandler{
private List<WebSocketSession> clientSessions = new CopyOnWriteArrayList<>();
private AsyncShellExecute asyncShellExecute = null;
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
clientSessions.remove(session);
//System.out.println("Client (" + session.getId() + ") closed, status code: " + status.getCode());
if (asyncShellExecute != null) {
asyncShellExecute.terminate(status.getCode());
}
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
clientSessions.add(session);
//System.out.println("Client (" + session.getId() + ") connected ... ");
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
Map<String, Object> retMap = new HashMap<>();
JSONObject recvJson = (JSONObject) JSON.parse(message.getPayload().toString());
String opt = recvJson.getString("operation");
if ("command".equals(opt)) {
JSONObject paramObj = recvJson.getJSONObject("param");
if (paramObj == null) {
retMap.put("ret", "error");
retMap.put("description", "Invalid command format");
session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
return;
}
String osType = paramObj.getString("os_type");
String cmdStr = paramObj.getString("cmd_str");
Platform platform = Platform.LINUX;
if ("windows".equals(osType)) {
platform = Platform.WINDOWS;
}
if (asyncShellExecute == null || asyncShellExecute.GetProcessStatus() == 0) {
asyncShellExecute = new AsyncShellExecute();
asyncShellExecute.callback = new AsyncShellCallback(session);
asyncShellExecute.setCommand(cmdStr, platform);
asyncShellExecute.start();
} else {
retMap.put("ret", "error");
retMap.put("description", "Previous execution process is running, can NOT run two processes at the same time");
session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
}
} else if ("close".equals(opt)) {
if (asyncShellExecute == null || asyncShellExecute.GetProcessStatus() == 0) {
retMap.put("ret", "finish");
retMap.put("description", "Finish directly");
session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
} else {
asyncShellExecute.terminate(0);
}
} else {
retMap.put("ret", "error");
retMap.put("description", "Invalid data format");
session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
}
}
}
4) 添加 src/main/java/com/example/ws/WSConfig.java
package com.example.ws;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
@Configuration
@EnableWebSocket
public class WSConfig implements WebSocketConfigurer{
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new WSTextHandler(), "/websocket")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*"); // Allow cross site
}
}
9. 视图和控制器
1) 添加 src/main/webapp/WEB-INF/jsp/home.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Home Page</title>
<script language="javascript" src="${pageContext.request.contextPath}/static/js/jquery-1.12.2.min.js"></script>
</head>
<body>
<c:if test="${not empty message}">
<p style="color: blue;">${message}</p>
</c:if>
<p> </p>
<form action="" method="post">
<p>
<label><strong>System Environment:</strong></label><br>
<select name="os_type" id="os_type" class="form-control" style="width: 50%; height: 32px;">
<option value="windows">Server run on Windows</option>
<option value="linux">Server run on Linux</option>
</select>
</p>
<p>
<label><strong>Command or shell:</strong></label><br>
<textarea name="cmd_str" id="cmd_str" class="form-control" style="width: 50%; height: 54px;">dir c:\</textarea>
</p>
<p>
<label><strong>Websocket:</strong></label><br>
<input type="text" name="ws_url" id="ws_url" value="ws://${pageContext.request.getServerName()}:${pageContext.request.getServerPort()}${pageContext.request.contextPath}/websocket" class="form-control" style="width: 50%; height: 32px;" />
<p>
<button type="button" id="btn_execute" class="btn btn-default btn-sm" onClick="javascript: connectWebsocket();">
Execute
</button>
<button type="button" id="btn_close" class="btn btn-default btn-sm" onClick="javascript: closeWebsocket();" style="display: none;">
Close Websocket
</button>
</p>
</form>
<p> </p>
<div id="cmd_result" style="padding: 15px; background-color: #e2e2e2; width: 50%; font-size: 12px; min-height: 120px;">
</div>
<p> </p>
<script type="text/javascript">
var globalSocket = null;
$(document).ready(function() {
console.log("Home Page");
changeType();
$("#os_type").change(function(e) {
changeType();
});
});
function changeType() {
var osType = $("#os_type").val();
var cmdStrNode = $("#cmd_str");
if (osType == "windows") {
cmdStrNode.val("dir C:\\");
} else if (osType == "linux") {
cmdStrNode.val("ls /");
} else {
cmdStrNode.val("ls /");
}
}
function connectWebsocket() {
var wsUrl = $("#ws_url").val();
if (wsUrl == '') {
alert("Please enter url");
$("#ws_url").focus();
return;
}
if (globalSocket == null) {
$("#cmd_result").html('');
$("#btn_execute").attr("disabled", "disabled");
createWebsocket(wsUrl);
} else {
/*
var data = {
"operation": "command",
"param": {
"os_type": $("#os_type").val(),
"cmd_str": $("#cmd_str").val(),
}
}
globalSocket.send(JSON.stringify(data));
*/
}
}
function createWebsocket(url) {
if (globalSocket != null || url == '')
return;
//console.log("createWebsocket(): url = ", url);
globalSocket = new WebSocket(url);
globalSocket.onopen = funcWSOpen;
globalSocket.onclose = funcWSClose;
globalSocket.onerror = funcWSError;
globalSocket.onmessage = funcWSMessage;
}
function closeWebsocket() {
if (globalSocket != null) {
//console.log("closeWebsocket(): send close");
globalSocket.send(JSON.stringify({ "operation": "close"}));
$("#btn_close").attr("disabled", "disabled");
}
}
function funcWSOpen(e) {
//console.log("funcWSOpen(): ", e);
$("#cmd_result").html("Executing ... <br><br>");
$("#btn_close").removeAttr("disabled");
$("#btn_close").css("display", "");
var data = {
"operation": "command",
"param": {
"os_type": $("#os_type").val(),
"cmd_str": $("#cmd_str").val(),
}
}
globalSocket.send(JSON.stringify(data));
}
function funcWSClose(e) {
//console.log("funcWSClose(): ", e);
$("#cmd_result").append("<br>Websocket: Close<br>");
$("#btn_execute").removeAttr("disabled");
$("#btn_close").css("display", "none");
globalSocket = null;
}
function funcWSError(e) {
//console.error("funcWSError(): ", e);
$("#cmd_result").append("<br>Websocket: Error<br>");
$("#btn_execute").removeAttr("disabled");
$("#btn_close").css("display", "none");
globalSocket = null;
}
function funcWSMessage(e) {
//console.log("funcWSMessage(): e.data = ", e.data);
var dataObj = JSON.parse(e.data);
if (dataObj['ret'] == "data") {
$("#cmd_result").append(dataObj['message'] + "<br>");
} else if (dataObj['ret'] == "finish") {
console.log("funcWSMessage(): ", dataObj['description'])
$("#cmd_result").append("<br>Websocket: " + dataObj['description'] + "<br>");
globalSocket.close(3009)
} else if (dataObj['ret'] == "error") {
console.log("funcWSMessage(): ", dataObj['description']);
$("#cmd_result").append("<br>Websocket: " + dataObj['description'] + "<br>");
} else {
$("#cmd_result").append("<br>Websocket: Invalid data format<br>");
}
}
</script>
</body>
</html>
2) 添加 src/main/java/com/example/controller/IndexController.java
package com.example.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.ui.ModelMap;
@Controller
@RequestMapping("/")
public class IndexController {
@RequestMapping(method = RequestMethod.GET)
public String home(ModelMap modelMap) {
modelMap.addAttribute("message", "Springmvc Websocket Demo");
return "home";
}
}
3) 删除 src/main/webapp/index.jsp
10. 运行
参考第 3 步