WebSocket 是HTML5一种新的协议,实现了浏览器与服务器全双工通信。其本质是先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接,服务端与客户端通过此TCP连接进行实时通信。
1. 开发环境
系统:macOS High Sierra 10.13.6
开发工具:IntelliJ IDEA 2020.1.4 (Community Edition)
Java, Maven 和 IDEA 的安装配置过程,见 IDEA创建Maven Quickstart项目
2. 在 IDEA上创建项目
Name: SpringmvcWebsocket
GroupId: com.example
ArtifactId: SpringmvcWebsocket
项目目录结构,参考 IDEA创建Maven Webapp项目
3. 使用 tomcat7-maven-plugin, 将 tomcat 内嵌运行
1) 修改 pom.xml:
<project ... >
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<port>9090</port>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
*注: path 项目访问路径, 本例:localhost:9090, 如果配置的aa,则访问路径为localhost:9090/aa;uriEncoding 非必选项。
2) 运行
Run -> Edit configurations -> Click "+" -> Select "Maven"
Command line: clean tomcat7:run
Name: SpringmvcWebsocket [clean,tomcat7:run]
Before Launch:
Click "+", select "Launch Web Browser"
Browser: default
Url: http://localhost:9090
-> OK
Run -> Run "SpringmvcWebsocket [clean,tomcat7:run]"
* tomcat7 除了支持 run, 还可以支持如下 Goal:
help, deploy, deploy-only, redeploy, redeploy-only, undeploy,
exec-war, exec-war-only, run-war, run-war-only,
standalone-war, standalone-war-only, shutdown
3) 打包 War
Run -> Edit configurations -> Click "+" -> Select "Maven"
Command line: clean tomcat7:run-war
Name: SpringmvcWebsocket [clean,tomcat7:run-war]
Before Launch:
Click "+", select "Launch Web Browser"
Browser: default
Url: http://localhost:9090
-> OK
Run -> Run "SpringmvcWebsocket [clean,tomcat7:run-war]"
可见到 target/SpringmvcWebsocket.war
4. 导入 spring-webmvc, Servlet & JSTL, websocket, fastjson
访问 http://www.mvnrepository.com/,查询 ...
修改 pom.xml
<project ... >
...
<dependencies>
...
<!-- Servlet & JSTL -->
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/taglibs/standard -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<!-- Spring web/mvc -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- Spring Websocket -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-messaging -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- JSON -->
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
...
</dependencies>
...
</project>
在IDE中项目列表 -> 点击鼠标右键 -> Maven -> Reload Project
5. 支持 SpringMVC
1) 修改 src/main/webapp/WEB-INF/web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
...
<!-- Spring mvc 适配器 -->
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-beans.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
...
</web-app>
2) 添加 src/main/resources/springmvc-beans.xml (中间目录如果不存在,新建目录,下同)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd"
default-init-method="init"
default-destroy-method="destroy">
<!-- Scan package -->
<context:component-scan base-package="com.example"/>
<mvc:annotation-driven />
<!-- MVC viewResolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
6. 支持静态资源 (html/js/css/images)
1) 修改 src/main/resources/springmvc-beans.xml
<beans ...>
...
<!-- html,css,js,images -->
<mvc:resources location="/static/" mapping="/static/**" />
...
</beans>
新建目录 src/main/webapp/static
2) 添加 jQuery
从 https://jquery.com/ 下载 JQuery 包,添加到:
src/main/webapp/static/js/jquery-1.12.2.min.js
* jQuery版本根据项目需要,这里用1.12.2, CSS和图片也是放到static目录下
3) 添加 src/main/webapp/static/test.html
<!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>Static HTML</title>
<script language="javascript" src="js/jquery-1.12.2.min.js"></script>
</head>
<body>
<h3>Static HTML Page</h3>
<p> </p>
<p id="message"></p>
<script type="text/javascript">
$(document).ready(function() {
console.log("Static HTML Page");
$("#message").html("JQuery is ready");
});
</script>
</body>
</html>
http://localhost:9090/static/test.html
7. 支持 spring-websocket
1) 添加 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);
}
}
2) 添加 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;
public class WSTextHandler extends TextWebSocketHandler{
private List<WebSocketSession> clientSessions = new CopyOnWriteArrayList<>();
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
clientSessions.remove(session);
System.out.println("Client (" + session.getId() + ") closed, status: " + status);
}
@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)) {
retMap.put("ret", "data");
retMap.put("message", "Get command '" + recvJson.getString("param") + "' from client");
session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
} else if ("close".equals(opt)) {
retMap.put("ret", "finish");
retMap.put("description", "Finish directly");
session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
} else {
retMap.put("ret", "error");
retMap.put("description", "Invalid data format");
session.sendMessage(new TextMessage(JSON.toJSON(retMap).toString()));
}
}
}
3) 添加 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
}
}
8. 视图和控制器
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>Websocket:</label><br/>
<input type="text" name="ws_url" id="ws_url" value="ws://${pageContext.request.getServerName()}:${pageContext.request.getServerPort()}${pageContext.request.contextPath}/websocket" style="height: 32px; width:50%" />
</p>
<p>
<button type="button" id="btn_connect" class="btn btn-default btn-sm" onClick="javascript: connectWebsocket();">
Connect
</button>
<button type="button" id="btn_close" class="btn btn-default btn-sm" onClick="javascript: closeWebsocket();" style="display: none;">
Close
</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");
});
function connectWebsocket() {
var wsUrl = $("#ws_url").val();
if (wsUrl == '') {
alert("Please enter url");
$("#ws_url").focus();
return;
}
if (globalSocket == null) {
$("#cmd_result").html('');
$("#btn_connect").attr("disabled", "disabled");
createWebsocket(wsUrl);
}
}
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": "test",
}
globalSocket.send(JSON.stringify(data));
}
function funcWSClose(e) {
console.log("funcWSClose(): ", e);
$("#cmd_result").append("<br>Websocket: close<br>");
$("#btn_connect").removeAttr("disabled");
$("#btn_close").css("display", "none");
globalSocket = null;
}
function funcWSError(e) {
console.error("funcWSError(): ", e);
$("#cmd_result").append("<br>Websocket: error<br>");
$("#btn_connect").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 java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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");
return "home";
}
}
3) 删除 src/main/webapp/index.jsp
9. 运行
参考第 3 步