Spring MVC(11):通过 WebSocket 实现前后端全双工通信
Spring WebSocket 支持
通常应用程序之间交换信息会使用如 JMS、AMQP 等技术,但是它们都无法实现浏览器客户端和服务器之间的全双工通信,基于 HTML5 的 WebSocket 协议可以用于实现浏览器和服务器之间的全双工通信;
相对于传统的服务器轮询方式,WebSocket
可以为长连接的建立一个低延迟、全双工、跨域的通信通道,这样的方式更加高效便捷,很适合需要实时通信的场景;
目前浏览器对于 WebSocket 的支持情况如下:
- IE:10.0(完整支持);
- FireFox:4.0(部分支持),6.0(完整支持);
- Chrome:4.0(部分支持),13.0(完整支持);
- Opera:11.0(部分支持),12.10(完整支持);
- Safari:5.0(部分支持),12.10(完整支持);
- IOS Safari:4.2(部分支持),6.0(完整支持);
- Android Browser:4.4(完整支持);
目前部分服务端对于 WebSocket 的支持情况如下:
- JavaEE 7.0+
- Tomcat 8.0+
- Jetty 7.0+
WebSocket 通信的 URL 是以 “ws://” 或 “wss://” 开头的,分别对应 “http://”,“https://”;
Spring 从 4.0 开始全面支持 WebSocket ,该支持包括以下几个方面:
-
发送和接收信息的 API;
-
用于发送信息的模板;
-
支持 SockJS(用于解决浏览器、服务器及代理不支持 WebSocket 的问题);
在 Spring 中使用 WebSocket 需要导入以下依赖包:
org.springframework:spring-web
org.springframework:spring-webmvc
org.springframework:spring-websocket
以下完整示例代码地址:
Spring MVC WebSocket 程序的实现
Spring MVC 提供了 WebSocketHandler 用于支持 WebSocket 的收发信息API,同时提供一个实现类AbstractWebSocketHandler
,可以继承该类方便地实现一个 WebSocket 服务端控制器,该接口常用地方法如下:
handleTextMessage | 处理接收的文本类型消息 |
handleBinaryMessage | 处理接收的二进制类型消息 |
handlePongMessage | 处理接收的Pong类型的消息 |
handleTransportError | 处理接收Exception信息 |
afterConnectionEstablished | 在建立 Websocket 连接后的执行方法 |
afterConnectionClosed | 在关闭 Websocket 连接后的执行方法 |
示例代码模块(代码地址见上面):
site.assad.web.HelloWebScoketHandler(websocket服务端控制器)
resources/applicationContext.xml(配置文件)
webapp/hello.jsp(websocket客户端)
一个示例的服务端控制器 HelloWebSocketHandler 如下:
package site.assad.web;
public class HelloWebSocketHandler extends AbstractWebSocketHandler {
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//处理文本消息
System.out.println("get message: " + message.getPayload());
//模拟延时
Thread.sleep(2000);
//发送信息
System.out.println("send message: Hello world!");
session.sendMessage(new TextMessage("from server: Hello world!"));
}
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("websocket connected");
}
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("websocket connection close");
}
}
在 spring 上线文配置文件 applicationContext.xml 中装载该 WebSocketHandler:
<!--装载 WebSocketHandler bean -->
<bean id="helloHandler" class="site.assad.web.HelloWebSocketHandler" />
<!--将 WebSocketHandler 映射到对应路由-->
<webscket:handlers>
<webscket:mapping handler="helloHandler" path="/hello"/>>
</webscket:handlers>
客户端 hello.jsp 如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>websocket 测试</title>
<script type="text/javascript">
//创建 websocket
var url = 'ws://'+ window.location.host + '<%= request.getContextPath() %>/hello';
var sock = new WebSocket(url);
//websocket 连接行为
sock.onopen = function(){
console.log('开启 websocket 连接');
sayHello();
};
//websocket 接受到信息行为
sock.onmessage = function(e){
console.log('接受信息',e.data);
setTimeout(function(){sayHello()},2000);
};
//websocket 关闭行为
sock.onclose = function(){
console.log('关闭 websocket 连接');
};
function sayHello() {
console.log('发送信息:hello world!');
sock.send('form client: Hello world!');
}
</script>
</head>
<body>
Hello world!
</body>
</html>
演示程序运行的过程如下:
启动后端程序,此时服务端建立一个websocket服务端,通过浏览器访问 hello.jsp ,此时客户端建立一websocket客户端,并向服务端发送文本信息 “form client: Hello world!”,服务端接收该信息,再向客户端发送文本信息 “from server: Hello world!” ,客户端接收信息后再次向服务端发送文本信息 “form
client: Hello world!”,以此循环,直到任意端断开
websocket 连接;
一个示例的具体使用场景
应用 Websocket 这样的特性,我们可以假象这么一个实际场景,有一个实时刷新某个城市PM2.5值的业务,前端输入某个城市,后端获取该程序后,不断地向前端发送该城市PM2.5的最新实时数据,并由前端实时渲染,这样的场景可以用AJAX来实现,但是使用 WebSocket 实现的效率会更高;
示例代码模块(代码地址见上面):
site.assad.service.PMService(模拟PM2.5值数据获取的业务层)
site.assad.web.PMWebScoketHandler(websocket服务端控制器)
webapp/pm25.jsp(websocket客户端)
编写服务端WebSocke控制器:PMWebScoketHandler
package site.assad.web;
public class PMWebSocketHandler extends AbstractWebSocketHandler{
private static final Logger log = LogManager.getLogger();
private PMService pmService;
private MessageSendThread thread ; //信息发送线程
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String city = message.getPayload().trim();
log.debug("get: " + city);
if(thread == null){
thread = new MessageSendThread(session,city);
thread.start();
}
thread.setCity(city);
}
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
if(thread != null)
thread.kill();
}
private class MessageSendThread extends Thread{
private volatile boolean isRunning = true;
private WebSocketSession session ;
private volatile String city;
public MessageSendThread(WebSocketSession session, String city) {
this.session = session;
this.city = city;
}
public void setCity(String city) {
this.city = city;
}
public void run() {
while(isRunning){
try {
String sendMsg = pmService.getData(city);
log.debug("send:" + sendMsg);
session.sendMessage(new TextMessage(sendMsg));
Thread.sleep(1000);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
}
}
}
public void kill(){
isRunning = false;
}
}
}
spring上下文装载wesocket控制器:applicationContext.xml
<!--装载业务层 bean -->
<context:component-scan base-package="site.assad.service" />
<!--装载 WebSocketHandler bean -->
<bean id="pmHandler" class="site.assad.web.PMWebSocketHandler" />
<!--将 WebSocketHandler 映射到对应路由-->
<webscket:handlers>
<webscket:mapping handler="pmHandler" path="/pm25"/>
</webscket:handlers>
编写 websocket 客户端,pm25.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>WebSocket 测试</title>
<script type="text/javascript">
var url = 'ws://' + window.location.host + '<%= request.getContextPath() %>/pm25'; //本机测试,真实常见下更换为服务主机 url
var sock = new WebSocket(url);
sock.onopen = function(){
changeCity("bj");
};
sock.onmessage = function(e){
console.log("get:"+ e.data);
document.getElementById("showData").innerHTML = e.data;
};
function changeCity(city){
console.log("send: "+city);
sock.send(city);
}
</script>
</head>
<body>
<h1>PM 2.5 AQI 实时刷新数据</h1>
<button onclick="changeCity('bj')">北京</button>
<button onclick="changeCity('gz')">广州</button>
<br/>
<p id="showData"></p>
</body>
</html>
启动服务端后,在浏览器通过 “http://127.0.0.1/<projectName>/pm25.jsp” 进行实验;