SpringBoot集成websocket实现消息推送

websocket介绍

为什么需要websocket

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据。

websocket的特点

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

websocket的适用场景

  • 弹幕
  • 媒体聊天
  • 协同编辑
  • 基于位置的应用
  • 体育实况更新
  • 股票基金报价实时更新

SpringBoot集成websocket

基础用法(原生API)

引入相关的依赖

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

开启websocket

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

创建WebSocketSever

@ServerEndpoint("/shell/motorWebsocket")
@Component
public class MotorAlarmWebSocketServer {

    // concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象
    private static final CopyOnWriteArraySet<MotorAlarmWebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();


    public static Set<MotorAlarmWebSocketServer> getWebSocketSet() {
        return webSocketSet;
    }

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

    @OnOpen
    public void onOpen(Session session) throws IOException {
        this.session = session;
        webSocketSet.add(this);
        System.out.println("系统连接成功");

    }

    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message) {
        //群发消息
        for (MotorAlarmWebSocketServer item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                System.out.println("消息发送失败");
            }
        }
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("断开连接");
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        for (MotorAlarmWebSocketServer item : webSocketSet) {
            if (item.session.isOpen()) {
                item.session.getBasicRemote().sendText(message);
            }
        }
    }
}

在前端界面使用websocket

websocket.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>websocket演示</title>
</head>
<body>
<h4>websocket演示</h4>
<p id="content"></p>
</body>
<script type="application/javascript">
    var websocket = null;
    if ('WebSocket' in window) {
        websocket = new WebSocket('ws://127.0.0.1:9999/shell/motorWebsocket');
    } else {
        alert('该浏览器不支持websocket!');
    }

    websocket.onopen = function (event) {
        console.log('建立连接');
        document.getElementById("content").innerText = '建立连接';
    }

    websocket.onclose = function (event) {
        console.log('连接关闭');
        document.getElementById("content").innerText = '连接关闭';
    }

    websocket.onmessage = function (event) {
        console.log('收到消息:' + event.data)
        //所要执行的操作
        document.getElementById("content").innerText = "收到消息:" + event.data;
    }

    websocket.onerror = function () {
        alert('websocket通信发生错误!');
    }

    window.onbeforeunload = function () {
        websocket.close();
    }
</script>
</html>

提供controller触发服务端向前端推送消息

@RestController
public class WebSocketController {

    @Autowired
    private MotorAlarmWebSocketServer webSocketServer;

    @GetMapping("sendMessage")
    public String sendMessage(String message) throws Exception {
        webSocketServer.sendMessage(message);
        return "SEND OK";
    }
}

测试验证

启动项目后,浏览器访问http://localhost:9999/sendMessage?message=hello ,就可以在前端html页面看到收到的消息。

websocket的STOMP支持

STOMP是一个简单的可互操作的协议,通常用于中间服务器与客户端之间进行异步消息传递。

引入相关的依赖

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

WebSocketConfig.java

/**
 * @Description EnableWebSocketMessageBroker-注解开启STOMP协议来传输基于代理的消息,此时控制器支持使用@MessageMapping
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //topic用来广播,user用来实现点对点
        config.enableSimpleBroker("/topic", "/user");
    }

    /**
     * 开放节点
     *
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册两个STOMP的endpoint,分别用于广播和点对点

        //广播
        registry.addEndpoint("/publicServer").setAllowedOrigins("*").withSockJS();

        //点对点
        registry.addEndpoint("/privateServer").setAllowedOrigins("*").withSockJS();
    }
}

推送消息的实体类 Message.java

public class Message {

    /**
     * 消息编码
     */
    private String code;

    /**
     * 来自(保证唯一)
     */
    private String form;

    /**
     * 去自(保证唯一)
     */
    private String to;

    /**
     * 内容
     */
    private String content;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getForm() {
        return form;
    }

    public void setForm(String form) {
        this.form = form;
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

前端广播消息调试 

1)publicExample.html 监听广播消息的测试页面

<html>
<head>
    <meta charset="UTF-8">
    <title>等系统推消息</title>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.2.0.min.js"
            integrity="sha256-JAW99MJVpJBGcbzEuXk4Az05s/XyDdBomFqNlM3ic+I=" crossorigin="anonymous"></script>

    <script type="text/javascript">
        var stompClient = null;

        function setConnected(connected) {
            document.getElementById("connect").disabled = connected;
            document.getElementById("disconnect").disabled = !connected;
            $("#response").html();
        }

        function connect() {

            var socket = new SockJS("http://localhost:9999/publicServer"); //publicServer连接广播节点
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function (frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                ///topic/all 订阅/topic为前缀的主题, /topic/all
                stompClient.subscribe('/topic/all', function (response) {
                    var responseData = document.getElementById('responseData');
                    var p = document.createElement('p');
                    p.style.wordWrap = 'break-word';
                    p.appendChild(document.createTextNode(response.body));
                    responseData.appendChild(p);
                });
            }, {});
        }

        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
            }
            setConnected(false);
            console.log("Disconnected");
        }

        function sendMsg() {
            var content = document.getElementById('content').value;
            stompClient.send("/all", {}, JSON.stringify({'content': content}));
        }
    </script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div>
        <labal>连接广播频道</labal>
        <button id="connect" onclick="connect();">Connect</button>
        <labal>取消连接</labal>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
    </div>
    <div id="conversationDiv">
        <labal>广播消息</labal>
        <input type="text" id="content"/>
        <button id="sendMsg" onclick="sendMsg();">Send</button>

    </div>
    <div>
        <labal>接收到的消息:</labal>
        <p id="responseData"></p>

    </div>

</div>

</body>
</html>

2)TestController.java

@RestController
public class TestSocketController {

    @Autowired
    public SimpMessagingTemplate template;

    /**
     * 广播
     *
     * @param msg
     */
    @PostMapping("pushToAll")
    public String sendMessage(@RequestBody Message msg) throws Exception {
        template.convertAndSend("/topic/all", msg.getContent());
        return "SEND OK";
    }
}

说明:我们推送消息,直接用 SimpMessagingTemplate ,用的是convertAndSend 广播方式推送到对于的主题目的地 destination 。(其实还有convertAndSendToUser)

3)直接把项目跑起来,打开页面开始测试

我们调用测试接口,推送广播消息(前端页面在浏览器打开多个tab,并点击Connect进行连接):

点对点消息测试

1)前端页面:privateExample.html

<html>
<head>
    <meta charset="UTF-8">
    <title>聊起来</title>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.2.0.min.js"
            integrity="sha256-JAW99MJVpJBGcbzEuXk4Az05s/XyDdBomFqNlM3ic+I=" crossorigin="anonymous"></script>

    <script type="text/javascript">
        var stompClient = null;

        function setConnected(connected) {
            document.getElementById("connect").disabled = connected;
            document.getElementById("disconnect").disabled = !connected;
            $("#response").html();
        }

        function connect() {
            var socket = new SockJS("http://localhost:9999/privateServer"); //连接点对点节点
            stompClient = Stomp.over(socket);
            stompClient.heartbeat.outgoing = 20000;
            // client will send heartbeats every 20000ms
            stompClient.heartbeat.incoming = 0;
            stompClient.connect({}, function (frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                //订阅/user开头,格式:/user + 连接的用户唯一标识 + /message
                stompClient.subscribe('/user/'+document.getElementById('user').value+'/message', function (response) {
                    var responseData = document.getElementById('responseData');
                    var p = document.createElement('p');
                    p.style.wordWrap = 'break-word';
                    p.appendChild(document.createTextNode(response.body));
                    responseData.appendChild(p);
                });


            });
        }

        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
            }
            setConnected(false);
            console.log("Disconnected");

        }

        function sendMsg() {
            var headers = {
                login: 'mylogin',
                passcode: 'mypasscode',
                // additional header
                'accessToken': 'HWPO325J9814GBHJF933'
            };
            var content = document.getElementById('content').value;
            var to = document.getElementById('to').value;
            stompClient.send("/alone", {'accessToken': 'HWPO325J9814GBHJF933'}, JSON.stringify({
                'content': content,
                'to': to
            }));
        }
    </script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div>
        <labal>连接用户</labal>
        <input type="text" id="user"/>
        <button id="connect" onclick="connect();">Connect</button>

    </div>

    <div>
        <labal>取消连接</labal>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
    </div>

    <div id="conversationDiv">
        <labal>发送消息</labal>
        <div>
            <labal>内容</labal>
            <input type="text" id="content"/>
        </div>
        <div>
            <labal>发给谁</labal>
            <input type="text" id="to"/>
        </div>
        <button id="sendMsg" onclick="sendMsg();">Send</button>

    </div>
    <div>
        <labal>接收到的消息:</labal>
        <p id="responseData"></p>

    </div>
</div>

</body>
</html>

2)TestController.java

@PostMapping("/pushToOne")
public void queue(@RequestBody Message msg) {
    System.out.println("进入方法");
    /*使用convertAndSendToUser方法,第一个参数为用户id,此时js中的订阅地址为
        "/user/" + 用户Id + "/message",其中"/user"是固定的*/
    template.convertAndSendToUser(msg.getTo(), "/message", msg.getContent());
}

用的是convertAndSendToUser 推送到指定的用户 ,对于的主题目的地 destination(/message)。

也许看到这里,你会觉得很奇怪,为什么我们推的主题是 /message,但是前端订阅的却是:

"/user/" + 用户Id + "/message"

我们直接看源码:

应该不用多说,代码帮我们自己拼接起来了,跟前端订阅规则保持一致。

启动项目,模拟下。

① 模拟我们连接的用户标识 19901 ,连接成功

② 使用postman调用我们的测试接口,模拟系统指定推送消息到 19901 这个人 :

③ 我们重新打开一个html页面,然后也给20011发送一下消息:

对点推送,广播推送,也已经完毕了 。

这种情况就是相当于使用http接口方式,去撮合后端服务做消息推送。

其实spring还提供了一些好玩的注解:@MessageMapping  这个注解是对称于  @EnableWebSocketMessageBroker

也就是说,如果我们使用@EnableWebSocketMessageBroker ,那么我们在接口上面其实就能直接使用  @MessageMapping。

然后前端代码里面的使用:

个人认为其实没有必要使用这个注解,直接通过前端调用后端服务代码,我们服务端来根据Message里面 的 发送方、接收方、消息类型(点对点、广播)就可以直接完成相关也业务场景了。

 

参考:

 

posted @ 2022-04-25 21:47  残城碎梦  阅读(1202)  评论(0编辑  收藏  举报