SpringBoot 学习笔记_整合 WebSocket —— 消息群发
声明:
本次学习参考 《SpringBoot + Vue 开发实战》 · 王松(著) 一书。
本文的目的是记录我学习的过程和遇到的一些问题以及解决办法,其内容主要来源于原书。
如有侵权,请联系我删除
文章目录
SpringBoot 整合 WebSocket
Http 协议中,所有请求都是由客户端发起,服务端响应的,服务端无法主动向客户端推送消息。
在某些场景下,不可避免的需要服务端向客户端推送消息。 常见解决方案有以下几种:
-
轮询
所谓轮询, 就是客户端在固定的时间间隔下不间断的向服务器发送请求检查是否有最新数据。一般如果有最新数据就返回,如无最新数据则返回一个标识或者空 JSON。
轮询是最简单的解决方案,但其弊端也非常明显,就是客户端每次都要新建 HTTP 请求,服务器要处理大量的无效请求,极大的浪费服务器资源,在高并发场景下严重拖慢服务端运行效率。
-
长轮询
长轮询是轮询的升级版。 与传统轮询不同之处在于,长轮询中,服务端不是每次都会立即响应客户端请求,直到服务端有新数据的时候才会立即响应,否则服务端会持有请求而不返回,直到有新数据时返回。
这种方式可能在一定程度上节省网络资源和服务器资源。但又会存在其他问题:
- 如果浏览器在服务器响应之前有新数据要发送,只能创建并发请求,或者尝试断开当前请求创建新请求;
- TCP 和 HTTP 规范都有连接超时一说,所以长查询并不能一直持续,服务端和客户端的连接需要定期的连接和关闭连接。
-
Applet 和 Flash
使用 Applet 和 Flash 来模拟全双工通信,当服务器有消息发送到客户端的时候,开发者可以在 Applet 和 Flash 中调用 JavaScript 函数将数据显示在页面上,当浏览器有数据要发送给服务器也是一样。但是,这种方式的限制较多,且 Flash 已经即将停止支持。
WebSocket 简介
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。 使用 WebSocket 可以使得客户端和服务器之间的数据交换变得更加简单,它允许服务端主动向客户端推送数据。
在 WebSocket 协议中,浏览器和服务器只需要完成一次握手,两者之间就可以直接创建持久性的连接,并进行双向数据传输。
WebSocket 使用 Http/1.1 的协议升级特性,一个 WebSocket 请求首次使用非正常用的 HTTP 请求以特定的模式访问一个 URL,这个 URL 有两种模式,分别是 ws 和 wss, 对应 HTTP 协议中的 HTTP 和 HTTPS
在请求头中有一个 Connection:Upgrade 字段,表示客户端想要对协议进行升级,另外还有一个 Upgrade:websocket 字段,表示客户端想要将请求协议升级为 WebSocket 协议。
这两个字段共同告诉服务器要将连接升级为 WebSocket 这样一种全双工协议,如果服务端同意协议升级,那么在握手完成之后,文本消息或者其他二进制消息就可以同时在两个方向上进行发送,而不需要关闭和重建连接。
此时客户端和服务端关系是对等的,它们可以互相向对方主动发送消息。
WebSocket 具有以下特点:
- WebSocket 使用时需要先创建连接,这使 WebSocket 成为一种有状态的协议,在之后通信过程中可以省略部分状态信息(身份验证等)
- WebSocket 连接在端口 80(wa) 或者 443(wss)上创建,与 HTTP 使用的端口相同,这样,基本上所有的防火墙都不会阻止 WebSocket 连接
- WebSocket 使用 HTTP 协议进行握手,因此可以自然而然的集成到网络浏览器后台 HTTP 服务器中而不需要额外的成本
- 心跳信息(ping 和 pong)将被反复的发送,进而保持 WebSocket 连接一直处于活跃状态
- 使用该协议,当消息启动或者到达的时候,服务端和客户端都可以知道
- WebSocket 连接关闭时将发送一个特殊的关闭信息
- WebSocket 支持跨域,可以避免 Ajax 限制
- HTTP 规范要求浏览器将并发连接数限制为每个主机名两个连接,如果使用 WebSocket ,当握手完成后,该限制就不存在了,因为此时的连接已经不再是 HTTP 连接了
- WebSocket 协议支持扩展,用户可以扩展协议,实现部分自定义的子协议
- 更好的二进制支持以及更好的压缩效果
WebSocket 常见应用场景:
- 在线股票
- 即时聊天
- 多人在线游戏
- 应用集群通信
- 系统性能实时监控
SpringBoot 整合 WebSocket —— 消息群发
SpringBoot 对 WebSocket 提供了非常友好的支持,可以方便开发者在项目中快速集成 WebSocket 功能,实现单聊或者群聊。
-
创建 SpringBoot 项目,添加依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>
<!-- 添加 WebSocket 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- 添加 webjars 依赖 --> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <!-- 添加 sockjs-client 依赖 --> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <!-- 添加 jquery 依赖 --> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.3.1</version> </dependency>
-
配置 WebSocket
/** * 自定义类 WebSocketConfig 继承自 WebSocketMessageBrokerConfigurer 进行 WebSocket 配置 */ @Configuration // @EnableWebSocketMessageBroker 注解开启 WebSocket 消息代理 @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置消息代理前缀 // 即如果消息前缀是 /topic 就会将消息转发给消息代理(broker),再由消息代理将消息广播给当前连接的客户端 registry.enableSimpleBroker("/topic"); // 配置一个或多个前缀,通过这些前缀过滤出需要被注解方法处理的消息 // 例如,前缀 /app 的 destination 可以通过 @MessageMapping 注解的方法处理 // 而其他 destination(/topic /queue) 将被直接交给 broker 处理 registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 定义一个前缀为 /chat 的 endPoint, 并开启 sockjs 支持,sockjs 可以解决浏览器对 WebSocket 的兼容性问题 // 客户端将通过这里配置的 URL 来建立 WebSocket 连接 registry.addEndpoint("/chat").withSockJS(); } }
-
定义实体类
public class Message { private String name; private String content; }
-
定义 Controller
@Controller public class GreetingController { // 用来接收 /app/hello 路径发送来的消息,在注解方法中对消息进行处理后,再将消息转发到 @SendTo 定义的路径上 @MessageMapping("/hello") // @SendTo 路径是一个前缀为 /topic 的路径,因此该消息将被交给消息代理 broker,再由 broker 进行广播 @SendTo("/topic/greetings") public Message greeting(Message message) throws Exception{ return message; } }
-
构建聊天页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>群聊</title> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/app.js"></script> </head> <body> <div> <label for="name">请输入用户名:</label> <input type="text" id="name" placeholder="用户名"> </div> <div> <button id="connect" type="button">连接</button> <button id="disconnect" type="button" disabled="disabled">断开连接</button> </div> <div id="chat" style="display: none"> </div> <div> <label for="name">请输入聊天内容:</label> <input type="text" id="content" placeholder="聊天内容"> </div> <button id="send" type="button">发送</button> <div id="greetings"> <div id="conversation" style="display: none">群聊进行中...</div> </div> </body> </html>
let stompClient = null; function setConnected(connected) { $('#connect').prop('disabled', connected); $('#disconnected').prop('disabled', !connected); if (connected) { $('#conversation').show(); $('#chat').show(); } else { $('#conversation').hide(); $('#chat').hide(); } $('#greetings').html('') } /** * 建立一个 WebSocket 连接。在建立 WebSocket 连接时,用户必须先输入用户名,然后建立连接 */ function connect() { if (!$('#name').val()) return; // 建立 SockJS 连接,然后创建 STOMP 实例发起连接请求 let socket = new SockJS('/chat'); stompClient = Stomp.over(socket); // 连接成功的回调方法中 stompClient.connect({}, (frame) => { // 首先调用 setConnected 方法进行页面设置 setConnected(true); // 然后调用 subscribe 方法订阅服务器发送回来的消息 stompClient.subscribe('/topic/greetings', (greeting) => { // 并展示服务端发送来的消息 showGreeting(JSON.parse(greeting.body)); }) }); } function disconnect() { if (stompClient !== null) stompClient.disconnect(); setConnected(false); } function sendName() { stompClient.send('/app/hello', {}, JSON.stringify({'name': $('#name').val(), 'content': $('#content').val()})) } function showGreeting(message) { $('#greetings').append(`<div>${message.name}: ${message.content}</div>`) } $(function(){ $('#connect').click(function (){connect()}); $('#disconnect').click(function (){disconnect()}); $('#send').click(function (){sendName()}); })
-
测试
启动项目,访问
http://localhost:8080/chat.html