一、项目目录
首先看一下这个简易的 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实现websocketSpring Boot系列十六 WebSocket简介和spring boot集成简单消息代理