Springboot整合WebSocket初探

1. 需求描述

最近正在开发的一个项目,客户端需要实时获取远程硬件设备通过Socket传给系统后台服务器的状态信息,并在客户端实时展示出来。该描述属于传统的拉取消息的实现方式。根据这个需求,查阅相关资料,该类问题更应该归入Web服务器消息推送类问题。换成推送消息的实现方式大意为:客户端需要实时展示系统后台服务器实时推送的状态信息。

2. 消息推送技术主要方式概述

目前,消息推送技术常用实现方式主要有以下几种:

2.1 短连接轮询

客户端采用定时器(setInterval或者setTimeout)的方式,每间隔一定的时间,重复向服务器发送请求来获取服务器的实时数据。该方式是典型的拉取消息的实现方式
在这里插入图片描述

2.2 长轮询

客户端像短连接轮询一样从服务器请求数据,区别在于,服务器不会立马响应,而是阻塞请求,直到有更新的数据或是超时才返回信息给客户端,然后关闭连接。当客户端处理完响应消息后,再给服务器发送新的请求。
在这里插入图片描述

2.3 iframe流

客户端在页面中插入一个隐藏的iframe,利用其src属性在客户端和服务器之间创建一条长连接,服务器向iframe传送数据,来实时更新客户端的页面。

2.4 WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在这里插入图片描述

2.5 Server-sent Events(sse)

sse与长轮询方式类似,区别在于长轮询每个连接不止发送一个请求。而sse客户端只发送一个请求,服务器保持这个连接,直到有新消息发送给客户端,仍然保持着连接,这样连接就可以用于消息的再次发送,后续,服务器通过该连接实时的给客户端推送消息。
在这里插入图片描述

2.6 常用实现对比

消息推送技术常用实现对比如下:
在这里插入图片描述

3 WebSocket客户端

WebSocket 协议本质上是一个基于 TCP 的协议。 为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade:WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

WebSocket为HTML5所支持,创建WebSocket的API为:

let ws = new WebSocket(url, [protocol])

其中,第一个参数url,指定连接的URL地址,第二个protocol是可选的,制定了可接受的子协议。如下为创建一个WebSocket客户端的示例代码。

let ws= null
if ('WebSocket' in window) {
    
    
    ws = new WebSocket('ws://localhost:8080/ws')
} else if ('MozWebSocket' in window) {
    
    
    ws = new MozWebSocket('ws://localhost:8080/ws')
} else {
    
    
    alert('您的浏览器不支持WebSocket,请更换浏览器')
}

3.1 WebSocket属性

完成创建ws(webSocket)对象后,可以通过调用该对象的readyState属性,获取对象的当前状态,以下是ws(webSocket)对象的属性。
在这里插入图片描述

通常以如下代码段的形式,来判断ws对象的当前状态

switch (ws.readyState) {
    
    
  case WebSocket.CONNECTING:
    // do something
    break
  case WebSocket.OPEN:
    // do something
    break
  case WebSocket.CLOSING:
    // do something
    break
  case WebSocket.CLOSED:
    // do something
    break
  default:
    // this never happens
    break
}

3.2 WebSocket事件(回调方法)

完成创建ws(webSocket)对象后,可以通过ws对应的事件(回调方法),做出对应的回调响应。以下是ws(webSocket)对象的相关事件(回调方法)。
在这里插入图片描述
通常以如下代码段的形式,来进行回调方法的处理。

//连接发生错误的回调方法
ws.onerror = function(){
    
    
};
//连接成功建立的回调方法
ws.onopen = function(event){
    
    
};
//接收到消息的回调方法
ws.onmessage = function(event){
    
    
    // ------接收到消息---------
    console.log(event.data);
    // ------发送消息-----------
    ws.send(event.data);
};
//连接关闭的回调方法
ws.onclose = function(){
    
    
    ws.close();
};

3.3 WebSocket方法

完成创建ws(webSocket)对象后,可以通过ws的send方法,发送数据;通过close关闭连接。以下是ws(webSocket)对应的相关方法。
在这里插入图片描述

3.4 WebSocket实例

以下引用一个简单的html页面,向服务器端8080请求连接。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>

<input id="text" type="text"/>
<button onclick="send()">Send</button>
<button onclick="closeWebSocket()">Close</button>
<div id="message"></div>

<script>
    var ws= null;

    // 建立连接
    if ('WebSocket' in window) {
     
     
        ws = new WebSocket("ws://localhost:8080/ws");
    } else if ('MozWebSocket' in window) {
     
     
        ws = new MozWebSocket("ws://localhost:8080/ws");
    } else {
     
     
        alert('您的浏览器不支持WebSocket,请更换浏览器');
    }

    //连接发生错误的回调方法
    ws.onerror = function(){
     
     
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    ws.onopen = function(){
     
     
        setMessageInnerHTML("open");
    };

    //接收到消息的回调方法
    ws.onmessage = function(event){
     
     
        setMessageInnerHTML('---from Server,begin----');
        setMessageInnerHTML(event.data);
		setMessageInnerHTML('---from Server,end----');
    };

    //连接关闭的回调方法
    ws.onclose = function(){
     
     
        setMessageInnerHTML("close");
    };

    //监听窗口关闭事件,当窗口关闭时,主动去关闭webSocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
     
     
        ws.close();
    };

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
     
     
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭连接
    function closeWebSocket(){
     
     
        ws.close();
    }

    //发送消息
    function send(){
     
     
        let message = document.getElementById('text').value;
		// 发送消息成功的时候,把消息显示在下方。
		document.getElementById('message').innerHTML += 'client准备发送:' + message + '<br/>';
        ws.send(message);
		
    }
</script>
</body>
</html>

4. SpringBoot整合WebSocket

4.1 创建SpringBoot工程(引入WebSocket依赖)

如下图所示,选择Spring Initializr创建Springboot工程。选择对应的WebSocket组件。
在这里插入图片描述

在这里插入图片描述
通过查看pom.xml文件,我们发现,WebSocket的整合,导入了对应的依赖,如下图所示:
在这里插入图片描述
即对于已经创建的springboot工程,在未选择WebSocket组件的时候,整合WebSocket需要引入依赖:

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-websocket</artifactId>
 </dependency>

4.2 创建WebSocket Endpoint

创建一个WebSocketConfig类,里面装配ServerEndPointExporter类。这个类会自动注册并使用@ServerEndpoint注解声明的WebSocket Endpoint,代码如下所示:

package com.supger.websocketdemo2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author supger
 */
@Configuration
public class WebSocketConfig {
    
    
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    
    
        return new ServerEndpointExporter();
    }
}

完成了WebSocketConfig配置类的创建后,就开始编写WebSocket操作处理类。代码如下所示:

package com.supger.websocketdemo2.webSocket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @author supger
 */
@ServerEndpoint("/ws")
@Component
public class WebSocketServer {
    
    
    private Logger log  = LoggerFactory.getLogger(this.getClass());

    /**
     * 用来存放每个客户端对应的MyWebSocket对象
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 连接建立成功
     */
    @OnOpen
    public void onOpen(Session session) {
    
    
        this.session = session;
        webSocketSet.add(this);
        log.info("【WebSocket】客户端:{} 加入连接!当前在线人数为:{}", session.getId(), webSocketSet.size());

        sendMessage("已接受您的连接请求");
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose() {
    
    
        webSocketSet.remove(this);
        log.info("【WebSocket】客户端:{} 关闭连接!当前在线人数为:{}", this.session.getId(), webSocketSet.size());

    }

    /**
     * 收到客户端消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
    
    
        log.info("【WebSocket】收到来自客户端:{} 的消息,消息内容:{}", session.getId(), message);
        sendMessage("收到消息:" + message);
    }

    /**
     * 发生错误
     */
    @OnError
    public void onError(Session session, Throwable error) {
    
    
        log.info("【WebSocket】客户端:{} 发生错误,错误信息:", session.getId(), error);
    }


    /**
     * 对当前客户端发送消息
     */
    public void sendMessage(String message) {
    
    
        this.session.getAsyncRemote().sendText(message);
        // this.session.getBasicRemote().sendText(message);
    }

    @Override
    public boolean equals(Object o) {
    
    
        if (this == o) {
    
    
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
    
    
            return false;
        }
        WebSocketServer that = (WebSocketServer) o;
        return Objects.equals(session, that.session);
    }

    @Override
    public int hashCode() {
    
    
        return Objects.hash(session);
    }
}

通读上述代码,不难发现:
(1)@Component创建一个组件,注入容器中;
(2)@ServerEndpoint指定了WebSocket的路径;
(3)在WebSocketServer类的内部,@OnOpen、@OnClose、@OnMessage、@OnError四个注解,分别对客户端来的请求建立连接成功、关闭连接、收到客户端消息、发生错误这四种情况进行监听处理。注意对比前文就会发现,这四个注解名称和前端ws(WebSocket)对象的四个事件处理程序(回调方法名字一样)。

5. 测试

利用【3.4 WebSocket实例】和【4.2 创建WebSocket Endpoint】的文件和项目。进行测试,启动一个客户端,如下图所示:
在这里插入图片描述
启动两个客户端,并发送消息,如下图所示:
在这里插入图片描述

6. 心跳包检测

由于WebSocket不支持断开重连,在使用WebSocket连接建立一段时间后,如果连接断开,就需要一种机制来检测客户端和服务端是否处于正常连接的状态。这种机制就是心跳包,还有心跳包说明连接正常,没有心跳包则说明连接断开。
实现效果是客户端连接后与服务器通过心跳包检测连接状态。当客户端超过一定时间收不到服务器的心跳包,则客户端认为与服务器连接已经断开,关闭连接,并不停的尝试重连。
修改后台的onMessage()方法,当收到客户端的心跳包时,响应心跳包:

    /**
     * 收到客户端消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
    
    
        log.info("【WebSocket】收到来自客户端:{} 的消息,消息内容:{}", session.getId(), message);
        // sendMessage("收到消息:" + message);

        // 如果客户端发送心跳包,返回心跳包
        if("ping".equals(message)) {
    
    
            sendMessage("pong");
        } else {
    
    
            sendMessage("收到消息:" + message);
        }
        
    }

参考的心跳检测的前端代码里面涉及到几条主线:
(1)主动断开连接后,会执行连接关闭的回调方法,在该方法内部,调用重连WebSocket的方法reconnect(url)。
(2)在重连方法的内部,判断重连是否成功,不成功的话,间隔2秒一直尝试重连。
(3)在连接成功建立后,会执行连接成功建立的回调方法,在该方法内部,会重置心跳检测(清空计时器),并向后台发送心跳包。
(4)在收到服务器消息的时候,会执行收到消息的回调方法,在该方法内部,会重置心跳检测(清空计时器),并向后台发送心跳包。
(5)在心跳检测对象内部,重置心跳包,给计时器对象清空。启动心跳包,会设置一个计时器;如果计时器超过一定时间还没有被重置,会执行计时器里面的关闭连接的回调方法。连接关闭后,就会执行连接关闭的回调方法,在该方法内部,调用重连WebSocket的方法reconnect(url)。

至此,完成了前端的心跳检测,前端代码如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket Heart</title>
</head>
<body>

<button onclick="closeWebSocket()">主动断开连接</button>

<script>
    var ws = null, wsUrl = "ws://localhost:8080/ws1";
    var lockReconnect = false;  //避免ws重复连接

    createWebSocket(wsUrl);

    // 建立连接
    function createWebSocket(url) {
     
     
        if ('WebSocket' in window) {
     
     
            ws = new WebSocket(url);
        } else if ('MozWebSocket' in window) {
     
     
            ws = new MozWebSocket(url);
        } else {
     
     
            alert('您的浏览器不支持WebSocket,请更换浏览器');
        }
        initEventHandle();
    }

    // 初始化相关回调函数
    function initEventHandle() {
     
     
        //连接成功建立的回调方法
        ws.onopen = function(){
     
     
            console.log("客户端连接建立");
            //心跳检测重置
            heartCheck.reset().start();
        };

        //接收到消息的回调方法
        ws.onmessage = function(event){
     
     
            console.log("客户端收到消息啦:" +event.data);
            //拿到任何消息都说明当前连接是正常的,重置心跳
            heartCheck.reset().start();
        };

        //连接关闭的回调方法
        ws.onclose = function(){
     
     
            console.log("客户端连接关闭");
            // 重连WebSocket
            reconnect(wsUrl);
        };

        //连接发生错误的回调方法
        ws.onerror = function(){
     
     
            console.log("客户端连接错误");
            // 重连WebSocket
            reconnect(wsUrl);
        };
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭webSocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
     
     
        ws.close();
    };

    // 重连WebSocket
    function reconnect(url) {
     
     
        if (lockReconnect)
            return;
        lockReconnect = true;

        //没连接上会一直重连,设置延迟避免请求过多
        setTimeout(function () {
     
     
            createWebSocket(url);
            lockReconnect = false;
        }, 2000);
    }


    //心跳检测
    var heartCheck = {
     
     
        timeout: 10000,        // 10s发一次心跳
        timeoutObj: null,
        serverTimeoutObj: null,
        reset: function () {
     
      //心跳包重置
            clearTimeout(this.timeoutObj);
            clearTimeout(this.serverTimeoutObj);
            return this;
        },
        start: function () {
     
     
            var self = this;
            this.timeoutObj = setTimeout(function () {
     
     
                // 向后台发送心跳
                ws.send("ping");
                self.serverTimeoutObj = setTimeout(function () {
     
      //如果超过一定时间还没重置,说明后端主动断开了
                    // 执行ws.close()会回调onclose,然后执行其中的reconnet。如果直接执行reconnect 会触发onclose导致重连两次
                    ws.close();
                }, self.timeout)
            }, this.timeout)
        }
    };

    //关闭连接
    function closeWebSocket(){
     
     
        ws.close();
    }
</script>
</body>
</html>

再次,测试连接,如下图所示,当主动断开连接后,在前端Console会报异常。进而重新连接。未断开连接的情况下,每间隔10秒钟发送一个心跳包,同时,收到后台的心跳响应消息。
在这里插入图片描述

7. 结语

通过以上的理论探索加上Demo实战,完成了Springboot整合WebSocket初步目标:对Springboot整合WebSocket的应用场景、实现流程、原理、异常重连、心跳检测,等有了初步的认识。
从相关资料发现,SockJS+Stomp实现WebSocket可以解决H5原生WebSocket存在的兼容性问题,并能简化开发。 在此不再展开,待探索实战之后,新开一篇进行详说。

8. 参考资源

(1)长连接/websocket/SSE等主流服务器推送技术比较
(2)Web 实时推送技术的总结
(3)实时消息推送汇总
(4)百度百科·WebSocket
(5)菜鸟教程·HTML5 WebSocket
(6)WebSoket初探并于SpringBoot整合

猜你喜欢

转载自blog.csdn.net/fanjianglin/article/details/112726259