一、項目目錄
首先看一下這個簡易的 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集成簡單消息代理