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"?>

	4.0.0
	
		org.springframework.boot
		spring-boot-starter-parent
		2.5.7
		 
	
	com.example
	websocket-stomp
	0.0.1-SNAPSHOT
	websocket-stomp
	Demo project for Spring Boot
	
		1.8
	
	
		
			org.springframework.boot
			spring-boot-starter
		
		
			org.springframework.boot
			spring-boot-starter-websocket
		

		
			org.springframework.boot
			spring-boot-starter-test
			test
		
	

	
		
			
				org.springframework.boot
				spring-boot-maven-plugin
			
		
	


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


    org.springframework.boot
    spring-boot-starter-websocket

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 {

    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 {

    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




    Welcome


    

Welcome to homepage!

Online Users: 0

四、开发时遇到的问题

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集成简单消息代理