zoukankan      html  css  js  c++  java
  • Springboot整合WebSocket STOMP统计在线人数

    一、项目目录

    首先看一下这个简易的 SpringBoot 项目的目录:

    我首先用 SpringBoot Initializer 创建一个简单的 Demo,然后在 Demo 上进行修改,这样更便捷。

    二、下载js

    这两个js不是我写的,是我从网上下载的:

    2.1 sockjs.min.js

    SockJS是一个浏览器JavaScript库,提供类似WebSocket的对象。SockJS为您提供了一个连贯的、跨浏览器的Javascript API,它在浏览器和web服务器之间创建了一个低延迟、全双工、跨域的通信通道。

    来自 Github 上的开源项目 sockjs-client,也可以通过 http://sockjs.org 跳转。
    进入 dist 文件夹 可以下载到 sockjs.min.js。

    2.2 stomp.min.js

    对于STOMP,许多应用程序已经使用了jmesnil/stomp-websocket库(也称为stomp.js),该库功能齐全,已经在生产中使用多年,但已不再维护。

    他有官方文档 STOMP Over WebSocket

    点击查看 stomp.min.js

    三、Demo介绍

    主要功能是统计网页在线人数。注意,该 Demo 仅支持单Web服务器的统计,不支持集群统计。
    每打开一个标签页 http://localhost:8080/index.html 在线人数就会+1,并且每当人数变化,会通知所有已经打开的网页更新在线人数。

    3.1 pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.5.7</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.example</groupId>
    	<artifactId>websocket-stomp</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>websocket-stomp</name>
    	<description>Demo project for Spring Boot</description>
    	<properties>
    		<java.version>1.8</java.version>
    	</properties>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-websocket</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    
    </project>
    

    为了支持 WebSocket,我们引入了依赖:

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

    3.2 WebSocketConfig

    package com.example.websocketstomp.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.messaging.simp.config.MessageBrokerRegistry;
    import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
    import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
    import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
    
    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/endpointWisely").withSockJS();
        }
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            registry.enableSimpleBroker ("/queue", "/topic");
            registry.setApplicationDestinationPrefixes ("/app");
        }
    }
    

    3.3 WebSocketConnCounter

    package com.example.websocketstomp.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.messaging.simp.SimpMessagingTemplate;
    import org.springframework.stereotype.Component;
    
    import java.util.concurrent.atomic.LongAdder;
    
    @Component
    public class WebSocketConnCounter {
    
        private LongAdder connections = new LongAdder();
    
        @Autowired
        private SimpMessagingTemplate template;
    
        public void increment() {
            connections.increment();
            template.convertAndSend("/topic/getResponse", String.valueOf(connections.sum()));
        }
    
        public void decrement() {
            connections.decrement();
            template.convertAndSend("/topic/getResponse", String.valueOf(connections.sum()));
        }
    
        public long onlineUsers() {
            return connections.sum();
        }
    }
    

    3.4 WebSocketConnectListener

    package com.example.websocketstomp.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationListener;
    import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
    import org.springframework.stereotype.Component;
    import org.springframework.web.socket.messaging.SessionConnectEvent;
    
    @Component
    public class WebSocketConnectListener implements ApplicationListener<SessionConnectEvent> {
    
        private WebSocketConnCounter counter;
    
        @Autowired
        public WebSocketConnectListener(WebSocketConnCounter counter) {
            this.counter = counter;
        }
    
        @Override
        public void onApplicationEvent(SessionConnectEvent event) {
            StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
            String sessionId = accessor.getSessionId();
            System.out.println("sessionId:" + sessionId + "已连接");
            counter.increment();
        }
    }
    

    3.5 WebSocketDisconnectListener

    package com.example.websocketstomp.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationListener;
    import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
    import org.springframework.stereotype.Component;
    import org.springframework.web.socket.messaging.SessionDisconnectEvent;
    
    @Component
    public class WebSocketDisconnectListener implements ApplicationListener<SessionDisconnectEvent> {
    
        private WebSocketConnCounter counter;
    
        @Autowired
        public WebSocketDisconnectListener(WebSocketConnCounter counter) {
            this.counter = counter;
        }
    
        @Override
        public void onApplicationEvent(SessionDisconnectEvent event) {
            StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
            String sessionId = accessor.getSessionId();
            System.out.println("sessionId:" + sessionId + "已断开");
            counter.decrement();
        }
    }
    

    3.6 WebSocketController

    package com.example.websocketstomp.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.messaging.simp.annotation.SubscribeMapping;
    import org.springframework.stereotype.Controller;
    
    @Controller
    public class WebSocketController {
    
        @Autowired
        private WebSocketConnCounter connCounter;
    
        /**
         * 用于初始化数据
         * 初次连接返回数据
         * 只执行一次
         **/
        @SubscribeMapping("welcome")
        public String welcome() {
            return String.valueOf(connCounter.onlineUsers());
        }
    }
    

    3.7 index.html

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
            "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
        <title>Welcome</title>
    </head>
    <body>
        <h1>Welcome to homepage!</h1>
        <p>Online Users: <b id="online-users">0</b></p>
    </body>
    <script type="text/javascript" src="/js/stomp.min.js"></script>
    <script type="text/javascript" src="/js/sockjs.min.js"></script>
    <script type="text/javascript">
        function init(){
            connect(1);
        }
        init();
        function connect(empNo) {
            var socket = new SockJS('/endpointWisely'); //1
            var stompClient = Stomp.over(socket);
            stompClient.connect({empNo: empNo}, function (frame) {
                console.log('Connected: ' + frame);
                stompClient.subscribe('/topic/getResponse', function (response) { //2
                    var elem = document.getElementById("online-users");
                    elem.textContent = response.body;
                });
    
                // 刚连接的时候执行,初始化数据,只执行一次
                stompClient.subscribe('/app/welcome', function (response) {
                    var elem = document.getElementById("online-users");
                    elem.textContent = response.body;
                });
            });
    
            //监听窗口关闭
            window.onbeforeunload = function (event) {
                socket.close()
            }
        }
    </script>
    </html>
    

    四、开发时遇到的问题

    4.1 sockjs /info 404

    当时出现这个问题时的错误代码:

    @SpringBootApplication
    @ComponentScan(value = "com.example.websocketstomp.controller")
    public class WebsocketStompApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(WebsocketStompApplication.class, args);
    	}
    
    }
    

    PS: 当时还没有类去注入 SimpMessagingTemplate : @Autowired private SimpMessagingTemplate template;

    然后可以正常启动,但是访问 http://localhost:8080/index.html 时,在线人数一直是 0。按 F12 查看 Network 时也有 404 报错:

    参考 sockjs 请求/info 应该返回什么?,主要原因是没把 WebSocketConfig 扫描进来!因此也可以这样修改

    @SpringBootApplication
    @ComponentScan(value = {"com.example.websocketstomp.controller", "com.example.websocketstomp.config"})
    public class WebsocketStompApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(WebsocketStompApplication.class, args);
    	}
    
    }
    

    PS: 去掉 @ComponentScan 这一行也是可行的。

    4.2 首次打开首页时,人数为0

    导致这个问题的原因,一个在 html 页面中:

    // 刚连接的时候执行,初始化数据,只执行一次
    stompClient.subscribe('/app/welcome', function (response) {
      var elem = document.getElementById("online-users");
      elem.textContent = response.body;
    });
    

    因为 WebSocket 首次连接时,stompClient.subscribe('/topic/getResponse', function (response) { } 可能发生在服务端的 template.convertAndSend("/topic/getResponse", String.valueOf(connections.sum())); 之后,导致第一次接收不到在线人数的消息。因此需要订阅 /app/welcome,同时在服务端响应它,在 WebSocketController 中用 @SubscribeMapping 注解。

    关于 Java中 SubscribeMapping与MessageMapping的区别

    @SubscribeMapping的主要应用场景是实现请求-回应模式。在请求-回应模式中,客户端订阅某一个目的地,然后预期在这个目的地上获得一个一次性的响应。 这种请求-回应模式与HTTP GET的请求-响应模式的关键区别在于HTTPGET请求是同步的,而订阅的请求-回应模式则是异步的,这样客户端能够在回应可用时再去处理,而不必等待。

    @MessageMapping的主要应用场景是一次订阅,多次获取结果。只要之前有订阅过,后台直接发送结果到对应的路径,则多次获取返回的结果。

    参考文档

    Spring Framework 参考文档(WebSocket STOMP)
    SpringBoot+sockjs client+stompjs实现websocket
    Spring Boot系列十六 WebSocket简介和spring boot集成简单消息代理

  • 相关阅读:
    清除浮动解决父元素高度塌陷问题
    canvas画动图
    vue实现列表的循环滚动
    localStorage读写操作
    angularJS快速入门
    python模块
    python函数式编程
    python高级特性
    Flask 快速入门
    JQuery Ajax
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/15594724.html
Copyright © 2011-2022 走看看