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 {
    @Override
    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!"));
    }
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("websocket connected");
    }
    @Override
    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 连接;
Spring MVC(11):通过 WebSocket 实现前后端全双工通信
Spring MVC(11):通过 WebSocket 实现前后端全双工通信



一个示例的具体使用场景

应用 Websocket 这样的特性,我们可以假象这么一个实际场景,有一个实时刷新某个城市PM2.5值的业务,前端输入某个城市,后端获取该程序后,不断地向前端发送该城市PM2.5的最新实时数据,并由前端实时渲染,这样的场景可以用AJAX来实现,但是使用 WebSocket 实现的效率会更高;

Spring MVC(11):通过 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();
    @Autowired
    private PMService pmService;
    private MessageSendThread thread ;  //信息发送线程
    @Override
    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);
    }
    @Override
    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;
        }
        @Override
        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” 进行实验;