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集成簡單消息代理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM