基于Spring-boot-websocket的聊天应用开发总结
目录
1.概述
1.1 Websocket
1.2 STOMP
1.3 源码
2.Springboot集成WS
2.1 添加依赖
2.2 ws配置
2.2.1 WebSocketMessageBrokerConfigurer
2.2.2 ChatController
2.2.3 ChatInRoomController
2.2.4 ChatToUserController
2.3 前端聊天配置
2.3.1 index.html和main.js
2.3.2 chatInRoom.html和chatInRoom.js
2.3.3 chatToUser.html和chatToUser.js
2.4 测试
2.4.1 基础的发布订阅测试
2.4.2 群聊测试
2.4.3 私聊测试
3 参考总结
最近在研究通过spring-boot-websocket开发简单的聊天应用,以下对这几天做一下总结。
关于WebRTC原理我主要是通过《WebRTC音视频实时互动技术原理、实战与源码分析》这本书了解底层的框架和实现思路,电子版资料可以私聊我。
1.概述
1.1 Websocket
WebSocket 连接允许客户端和服务器进行全双向通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
如果仅使用WebSocket完成群聊、私聊功能时需要自己管理session信息,但通过STOMP协议时,Spring已经封装好,开发者只需要关注自己的主题、订阅关系即可。
1.2 STOMP
STOMP即“面向消息的简单文本协议”,提供了能够协作的报文格式,以至于 STOMP 客户端可以与任何 STOMP 消息代理(Brokers)进行通信,从而为多语言,多平台和 Brokers 集群提供简单且普遍的消息协作。
STOMP 协议可以建立在WebSocket 之上,也可以建立在其他应用层协议之上。通过 Websocket建立 STOMP 连接,也就是说在 Websocket 连接的基础上再建立 STOMP 连接。最终实现如上图所示,这一点可以在代码中有一个良好的体现。
主要包含如下几个协议事务:
- CONNECT:启动与服务器的流或 TCP 连接
- SEND:发送消息
- SUBSCRIBE:订阅主题
- UNSUBSCRIBE:取消订阅
- BEGIN:启动事务
- COMMIT:提交事务
- ABORT:回滚事务
- ACK:确认来自订阅的消息的消费
- NACK:告诉服务器客户端没有消费该消息
- DISCONNECT:断开连接
1.3 源码
git地址:https://github.com/BAStriver/spring-boot-websocket-chat-app
下载路径:https://download.csdn.net/download/BAStriver/88711460
2.Springboot集成WS
2.1 添加依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</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><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-messaging</artifactId><version>6.0.7</version></dependency>
</dependencies>
2.2 ws配置
2.2.1 WebSocketMessageBrokerConfigurer
这里主要是配置STOMP协议端点、消息代理。
并且设置了前端发布消息的前缀为/app,和消息代理的前缀/topic(@SendTo中为/topic/*)。
// register STOMP endpoints
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws") // this is the endpoint which should be set in SockJS client.setAllowedOriginPatterns("*") // allow cross-domain request.withSockJS(); // use SockJS protocol
}// register message broker
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {// while sending messages in front end, the path should add the prefix as /appregistry.setApplicationDestinationPrefixes("/app");// enable and set the prefixes of broker paths, like /topic/public// without this prefix, it will block those sent messagesregistry.enableSimpleBroker("/topic", "/user");// while sending messages to user in front end, the path should add the prefix as /user// default is /userregistry.setUserDestinationPrefix("/user");
}
2.2.2 ChatController
以下是基础的控制器,通过sendMessage()发布消息,通过addUser()把订阅者加入到session管理,并最终返回到订阅路径/topic/public。
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage
) {return chatMessage;
}@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,SimpMessageHeaderAccessor headerAccessor
) {// Add username in web socket sessionheaderAccessor.getSessionAttributes().put("username", chatMessage.getSender());return chatMessage;
}
经过上面的方法可以实现发布订阅模式。
值得注意的是,如果没有配置@SendTo,则消息会默认返回到@MessageMapping的路径给订阅者。
2.2.3 ChatInRoomController
这个主要是实现群聊。
@MessageMapping("/chat/{roomId}")
@SendTo("/topic/chat/{roomId}") // if not add @SendTo, then by default will send to the path /topic/chat/{roomId}
public ChatMessage sendMessage(@DestinationVariable String roomId, ChatMessage message) {log.info("roomId: {}", roomId);return message;
}// if need the {roomId} in @SendTo,
// then should add {roomId} in @MessageMapping and sent roomId from front end.
// otherwise, it could not resolve placeholder 'roomId' in value "/topic/chat/{roomId} of @SendTo
@MessageMapping("/chat.addUserToRoom/{roomId}")
@SendTo("/topic/chat/{roomId}")
public ChatMessage addUser(@Payload ChatMessage chatMessage,SimpMessageHeaderAccessor headerAccessor
) {// Add username in web socket sessionheaderAccessor.getSessionAttributes().put("username", chatMessage.getSender());return chatMessage;
}
值得注意的是,如果@SendTo需要{roomId}这个参数,那么在@MessageMapping()中也需要传入{roomId}。
2.2.4 ChatToUserController
这个主要实现单独发布消息到指定的订阅者。
@MessageMapping("/chatToUser/{userId}")
@SendTo(value = "/topic/chatToUser/{userId}")
public ChatMessage sendMessage(@DestinationVariable String userId, ChatMessage message,SimpMessageHeaderAccessor headerAccessor) {log.info("send to the userId: {}", userId);log.info("message: {}", message);// Set<StompAuthenticatedUser> collect = simpUserRegistry.getUsers().stream()
// .map(simpUser -> StompAuthenticatedUser.class.cast(simpUser.getPrincipal()))
// .collect(Collectors.toSet());
// collect.forEach(user -> {
// if(user.getNickName().equals(userId)) {
// simpMessagingTemplate.convertAndSendToUser(userId, "/chatToUser/"+userId, message);
// }
// });return message;
}@MessageMapping("/chat.helloUser/{userId}")
@SendTo("/user/chat/{userId}")
public ChatMessage helloUser(@DestinationVariable String userId,@Payload ChatMessage chatMessage,SimpMessageHeaderAccessor headerAccessor
) {// Add username in web socket sessionheaderAccessor.getSessionAttributes().put("username", chatMessage.getSender());headerAccessor.getSessionAttributes().put("userid", userId);// use the tool to send the message to public topic directly, without @MessageMapping// simpMessagingTemplate.convertAndSend("/user/chat/" + userId, chatMessage);return chatMessage;
}//@MessageMapping("/chat.sendMessage")
@GetMapping("/testSendMessage")
public void testSendMessage(ChatMessage message) {// use the tool to send the message to public topic directly, without @MessageMappingsimpMessagingTemplate.convertAndSend("/topic/public", message);
}
值得注意的是,这里的@MesssageMapping()不要和前面的重复了。
同样的,也可以通过如下的代码实现发布消息。
simpMessagingTemplate.convertAndSend("/user/chat/" + userId, chatMessage);
其实这个部分和#2.2.3同理,不同的是私聊其实可以用@SendToUser。
2.3 前端聊天配置
SockJS 是一个浏览器的 JavaScript库,它提供了一个类似于网络的对象,SockJS 提供了一个连贯的,跨浏览器的JavaScriptAPI,它在浏览器和 Web 服务器之间创建了一个低延迟、全双工、跨域通信通道。SockJS 的一大好处在于提供了浏览器兼容性。即优先使用原生WebSocket,如果浏览器不支持 WebSocket,会自动降为轮询的方式。如果你使用 Java 做服务端,同时又恰好使用 Spring Framework 作为框架,那么推荐使用SockJS。
2.3.1 index.html和main.js
对应#2.2.2的前端页面和脚本。
这里初始化一个sockjs实例,其中的/ws指定了#2.2.1的STOMP端点。
function connect(event) {username = document.querySelector('#name').value.trim();if(username) {usernamePage.classList.add('hidden');chatPage.classList.remove('hidden');const header = {"User-ID": new Date().getTime().toString(),"User-Name": username};var socket = new SockJS('/ws'); // set the STOMP endpointstompClient = Stomp.over(socket);stompClient.connect(header, onConnected, onError);}event.preventDefault();
}
当客户端和服务Connected之后,开始订阅/topic/public的消息以及设置send()的消息发布路径。
function onConnected() {// Subscribe to the Public TopicstompClient.subscribe('/topic/public', onMessageReceived);// Tell your username to the serverstompClient.send("/app/chat.addUser", // prefix with /app{},JSON.stringify({sender: username, type: 'JOIN'}))connectingElement.classList.add('hidden');
}function sendMessage(event) {var messageContent = messageInput.value.trim();if(messageContent && stompClient) {var chatMessage = {sender: username,content: messageInput.value,type: 'CHAT'};stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));messageInput.value = '';}event.preventDefault();
}
值得注意的是,这里send()的时候要记得加/app作为前缀。
2.3.2 chatInRoom.html和chatInRoom.js
对应#2.2.3的前端页面和脚本。
初始化sockjs实例和上面的一样,但要注意的是Connected之后的订阅和发布路径加上了{room}作为聊天室的id。
function onConnected() {// Subscribe the message of the {room}stompClient.subscribe('/topic/chat/'+room, onMessageReceived);// Tell your username to the serverstompClient.send("/app/chat.addUserToRoom/"+room, // prefix with /app{},JSON.stringify({sender: username, type: 'JOIN'}))connectingElement.classList.add('hidden');
}function sendMessage(event) {var messageContent = messageInput.value.trim();if(messageContent && stompClient) {var chatMessage = {sender: username,content: messageInput.value,type: 'CHAT'};stompClient.send("/app/chat/"+room, {}, JSON.stringify(chatMessage));messageInput.value = '';}event.preventDefault();
}
2.3.3 chatToUser.html和chatToUser.js
对应#2.2.3的前端页面和脚本。
初始化sockjs实例和上面的一样,但要注意的是Connected之后的订阅和发布路径加上了{username}和{userid}作为私聊对象id。
function onConnected() {console.log('username: ', username);console.log('userid: ', userid);// Subscribe the message with {userid}stompClient.subscribe('/user/chat/' + username, onMessageReceived);stompClient.subscribe('/topic/chatToUser/' + username, onMessageReceived);// Tell your username to the serverstompClient.send("/app/chat.helloUser/" + username, // prefix with /app{},JSON.stringify({sender: username, type: 'JOIN'}))connectingElement.classList.add('hidden');
}function sendMessage(event) {var messageContent = messageInput.value.trim();if (messageContent && stompClient) {var chatMessage = {sender: username,content: messageInput.value,type: 'CHAT'};stompClient.send("/app/chatToUser/" + userid, {}, JSON.stringify(chatMessage));messageInput.value = '';}event.preventDefault();
}
2.4 测试
2.4.1 基础的发布订阅测试
这里测试的#2.3.1的部分。
首先是登录界面,进入:http://localhost:8080/index.html
打开两个index页面,然后输入username之后实现聊天。
第一个index.html登入BAS用户,第二个页面登入BAS55。
2.4.2 群聊测试
这里测试的#2.3.2的部分。
首先是登录界面,进入:http://localhost:8080/chatInRoom.html
第一个index.html登入BAS用户(Room: 12345),
第二个页面登入BAS55(Room: 12345),
第三个页面登入BAS10(Room: 123),BAS10单独在一个房间。
2.4.3 私聊测试
这里测试的#2.3.3的部分。
首先是登录界面,进入:http://localhost:8080/chatToUser.html
第一个index.html登入BAS用户(Chat To: BAS5),
第二个页面登入BAS55(Chat To: BAS),
第三个页面登入BAS10(Chat To: BAS9)。
3 参考总结
以下是开发过程中参考并且觉得挺有帮助的资料:
SpringBoot——整合WebSocket(STOMP协议) - 简书
Spring Boot系列 WebSocket集成简单消息代理_websocketmessagebrokerconfigurer-CSDN博客
WebSocket的那些事(4-Spring中的STOMP支持详解)_simpuserregistry 为空-CSDN博客
注:
1.关于@MessageMapping()的使用可以参考:Spring Boot中的@MessageMapping注解:原理及使用-CSDN博客
2.关于AbstractWebSocketHandler的使用可以参考:WebSocket基本概念及在Spring Boot中的使用 - 知乎
3.关于@SendTo()和@SendToUser()的区别和使用可以参考:在Spring WebSocket中使用@SendTo和@SendToUser进行消息路由 - 实时互动网
Spring-messaging (STOMP) @SendTo 与 @SendToUser的区别-CSDN博客
相关文章:

基于Spring-boot-websocket的聊天应用开发总结
目录 1.概述 1.1 Websocket 1.2 STOMP 1.3 源码 2.Springboot集成WS 2.1 添加依赖 2.2 ws配置 2.2.1 WebSocketMessageBrokerConfigurer 2.2.2 ChatController 2.2.3 ChatInRoomController 2.2.4 ChatToUserController 2.3 前端聊天配置 2.3.1 index.html和main.j…...

2023年度总结 - 职业生涯第一个十年
2023年只剩下最后一周,又到了一年一度该做年末总结的时候了。 回想起去年,还有人专门建立了一个关于年度总结文章汇总的仓库。读了很多篇别人写的,给了我很多的触动和感想。这里的每篇文章都是关于某个人这一整年的生活和工作的轨迹啊。即使你…...

setup 语法糖
只有vue3.2以上版本可以使用 优点: 更少的样板内容,更简洁的代码 能够使用纯 Typescript 声明props 和抛出事件 更好的运行时性能 更好的IDE类型推断性能 在sciprt标识上加上setup 顶层绑定都可以使用 不需要return ,可以直接使用 使用组件…...

Javaweb之Mybatis的基础操作的详细解析
1. Mybatis基础操作 学习完mybatis入门后,我们继续学习mybatis基础操作。 1.1 需求 需求说明 通过分析以上的页面原型和需求,我们确定了功能列表: 查询 根据主键ID查询 条件查询 新增 更新 删除 根据主键ID删除 根据主键ID批量删除 …...

知名开发者社区Stack Overflow发布《2023 年开发者调查报告》
Stack Overflow成立于2008年,最知名的是它的公共问答平台,每月有超过 1 亿人访问该平台来提问、学习和分享技术知识。是世界上最受欢迎的开发者社区之一。每年都会发布一份关于开发者的调查报告,来了解不断变化的开发人员现状、正在兴起或衰落…...
vue element plus Form 表单
表单包含 输入框, 单选框, 下拉选择, 多选框 等用户输入的组件。 使用表单,您可以收集、验证和提交数据。 TIP Form 组件已经从 2. x 的 Float 布局升级为 Flex 布局。 典型表单# 最基础的表单包括各种输入表单项,比如input、select、radio、checkbo…...
zmq_connect和zmq_poll
文章内容: 介绍函数zmq_connect和zmq_poll的使用 zmq_connect zmq_connect函数是ZeroMQ库中的一个函数,用于在C语言中创建一个与指定地址的ZeroMQ套接字的连接。该函数的原型如下: int zmq_connect(void *socket, const char *endpoint);其…...

TinyLog iOS v3.0接入文档
1.背景 为在线教育部提供高效、安全、易用的日志组件。 2.功能介绍 2.1 日志格式化 目前输出的日志格式如下: 日志级别/[YYYY-MM-DD HH:MM:SS MS] TinyLog-Tag: |线程| 代码文件名:行数|函数名|日志输出内容触发flush到文件的时机: 每15分钟定时触发…...

react-native 配置@符号绝对路径配置和绝对路径没有提示的问题
这里需要用到vscode的包 yarn add babel-plugin-module-resolver 找到根目录里的babel.config.js 在页面添加plugins配置 直接替换 module.exports {presets: [module:metro-react-native-babel-preset],plugins: [[module-resolver,{root: [./src],alias: {/utils: ./src/…...

element的Table表格组件树形数据与懒加载简单使用
目录 1. 代码实现2. 效果图3. 解决新增、删除、修改之后树节点不刷新问题。([参考文章](https://blog.csdn.net/weixin_41549971/article/details/135504471)) 1. 代码实现 <template><div><!-- lazy 是否懒加载子节点数据 --><!-…...

游戏、设计选什么内存条?光威龙武系列DDR5量大管饱
如果你是一位PC玩家或者创作者,日常工作娱乐中,确实少不了大容量高频内存的支持,这样可以获得更高的工作效率,光威龙武系列DDR5内存条无疑是理想之选。它可以为计算机提供强劲的性能表现和稳定的运行体验,让我们畅玩游…...

linux磁盘清理_docker/overlay2爆满
问题:无意间发现linux服务器登陆有问题,使用df命令发现目录满了。 1. 确定哪里占用了大量内存。 cd / du -sh * | sort -rh经过一段时间后,显示如下: // 474G home // 230G var // 40G usr // 10G snap // --- 根据实际情…...
Redis过期清理策略和内存淘汰机制
目录 Redis过期清理策略Redis内存淘汰机制 Redis过期清理策略 Redis 通过设置键的过期时间来实现自动删除过期键。当键的过期时间到达时,Redis 会自动将该键删除。Redis 过期清理策略主要有以下两种: 惰性删除:Redis 在获取键时会检查键是否…...

2_并发编程同步锁(synchronized)
并发编程带来的安全性同步锁(synchronized) 1.他的背景 当多个线程同时访问,公共共享资源的时候,这时候就会出现线程安全,代码如: public class AtomicDemo {int i0;//排他锁、互斥锁public void incr(){ //synchronizedi; …...
Python 常用模块pickle
Python 常用模块pickle pickle序列化模块 【一】定义 序列化:将数据结构或对象转换为可存储或传输的格式反序列化:将序列化后的数据恢复为开始的数据结构或者对象 【二】目的 数据持久化存储远程通信缓存进程间通信 【三】序列化 将对象转换为字节…...

CentOS 6 制作openssh 9.6 p1 rpm包(含ssh-copy-id、openssl) —— 筑梦之路
openssh 9.6 需要openssl 1.1.1 以上版本,因此需要先安装openssl 1.1.1,可阅读这篇升级更新openssl版本到1.1.1w CentOS 6 制作openssl 1.1.1w rpm包 —— 筑梦之路-CSDN博客 CentOS 6很久都停止更新和支持,关于此版本的写的不多ÿ…...

Tomcat Notes: Deployment File
This is a personal study notes of Apache Tomcat. Below are main reference material. - YouTube Apache Tomcat Full Tutorial,owed by Alpha Brains Courses. https://www.youtube.com/watch?vrElJIPRw5iM&t801s 1、Tomcat deployment1.1、Two modes of …...
某邦通信股份有限公司IP网络对讲广播系统挖矿检测脚本
目录 1.漏洞概述 2.影响版本 3.危害等级 4.挖矿程序检测 5.Nuclei自动化检测...

uniapp点击跳转传对象
目录 传对象传对象传送组件接受组件 最后 传对象 传对象 传送组件 点击传给组件 <view class"dki-tit-edit" click"gotificatedit(item)">编辑 </view>gotificatedit(item){console.log(item,item);let options JSON.stringify(item);uni.…...
简单用PHP实现微信小程序的游戏功能
微信小程序的兴起,越来越多的开发者开始关注如何在小程序中实现游戏功能。PHP作为一种流行的后端语言,可以很好地与小程序进行搭配,实现游戏功能。下面将介绍如何使用PHP来实现微信小程序的游戏功能,并提供具体的代码示例。 建立…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...

华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...

JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...

推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材)
推荐 github 项目:GeminiImageApp(图片生成方向,可以做一定的素材) 这个项目能干嘛? 使用 gemini 2.0 的 api 和 google 其他的 api 来做衍生处理 简化和优化了文生图和图生图的行为(我的最主要) 并且有一些目标检测和切割(我用不到) 视频和 imagefx 因为没 a…...

无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...