Java分享客栈如何用SpringBoot和Stomp实现WebSocket群聊功能?

2026-05-19 19:101阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3972个文字,预计阅读时间需要16分钟。

Java分享客栈如何用SpringBoot和Stomp实现WebSocket群聊功能?

前言:近期,两位大学生小哥们向我透露了他们的私心,询问我是否可以提供毕业设计帮助。我表示暂时没有这个打算,因为工作实在繁忙,目前阶段无法投入到此类领域。其中两位小哥们又问道:我们WE呢?


前言

前两周经常有大学生小伙伴私信给我,问我可否有偿提供毕设帮助,我说暂时没有这个打算,因为工作实在太忙,现阶段无法投入到这样的领域内,其中有两个小伙伴又问到我websocket该怎么使用,想给自己的项目中加入这样的技术。

刚好我所在的公司有做问诊服务,里面就使用了websocket实现聊天通讯,就在闲暇之余专门把部分代码摘取出来,做了一个简单的demo分享给他们了,之后想想这块可以再丰富一下,就花时间又做了一个更完整的小项目出来,且加了详细的注释说明,分享给对websocket感兴趣的小伙伴们。


案例展示

Java分享客栈如何用SpringBoot和Stomp实现WebSocket群聊功能?


技术栈

考虑到不同群体对vue等前端技术的接受程度,本案例采用了HTML+CSS+JQuery来实现,代码直接复制到vue项目中也是一样的,只是赋值和取值的方式改变而已,很多Java程序员其实对于一门简单案例的学习不喜欢牵扯太多前端技术,而是单纯学习想知道的这门技术就好,太多其他的引入反而影响跟踪调试,而原始的HTML+JS方式更有利于我们学习和理解,只需要右键HTML页面在浏览器打开进行F12调试即可。

技术 版本 Java 1.8 SpringBoot 2.3.12.RELEASE WebSocket 2.3.12.RELEASE Hutools 5.8.0.M1 SockJS 1.6.0 StompJS 1.7.1
实现过程 1、引入依赖

<!-- websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Hutools工具类 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.0.M1</version> </dependency>
2、订阅常量类

后面的websocket配置类会用到这几个常量

stomp端点地址: 连接websocket时的后缀地址,比如127.0.0.1:8888/websocket。

websocket前缀:前端调服务端消息接口时的URL都加上了这个前缀,比如默认是/send,变成/app/send。

点对点代理地址:如果websocket配置类中设置了代理路径,一般点对点订阅路径喜欢用/queue。

广播代理地址:如果websocket配置类中设置了代理路径,一般广播订阅路径喜欢用这个/topic。

package com.simple.ws.constants; /** * <p> * websocket常量 * </p> * * @author 福隆苑居士,公众号: * @since 2022-04-02 10:11 */ public class WsConstants { // stomp端点地址 public static final String WEBSOCKET_PATH = "/websocket"; // websocket前缀 public static final String WS_PERFIX = "/app"; // 消息订阅地址常量 public static final class BROKER { // 点对点消息代理地址 public static final String BROKER_QUEUE = "/queue/"; // 广播消息代理地址 public static final String BROKER_TOPIC = "/topic"; } }
3、WebSocket配置类

核心内容讲解:

1)、@EnableWebSocketMessageBroker:用于开启stomp协议,这样就能支持@MessageMapping注解,类似于@requestMapping一样,同时前端可以使用Stomp客户端进行通讯;

2)、registerStompEndpoints实现:主要用来注册端点地址、开启跨域授权、增加拦截器、声明SockJS,这也是前端选择SockJS的原因,因为spring项目本身就支持;

3)、configureMessageBroker实现:主要用来设置客户端订阅消息的路径(可以多个)、点对点订阅路径前缀的设置、访问服务端@MessageMapping接口的前缀路径、心跳设置等;

package com.simple.ws.config; import com.simple.ws.constants.WsConstants; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.*; /** * <p> * websocket核心配置类 * </p> * * @author 福隆苑居士,公众号: * @since 2022/4/1 22:57 */ @Configuration @EnableWebSocketMessageBroker public class WebsocketConfig implements WebSocketMessageBrokerConfigurer { /** * 注册stomp端点 * * @param registry stomp端点注册对象 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(WsConstants.WEBSOCKET_PATH) .setAllowedOrigins("*") .withSockJS(); } /** * 配置消息代理 * * @param registry 消息代理注册对象 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 配置服务端推送消息给客户端的代理路径 registry.enableSimpleBroker(WsConstants.BROKER.BROKER_QUEUE, WsConstants.BROKER.BROKER_TOPIC); // 定义点对点推送时的前缀为/queue registry.setUserDestinationPrefix(WsConstants.BROKER.BROKER_QUEUE); // 定义客户端访问服务端消息接口时的前缀 registry.setApplicationDestinationPrefixes(WsConstants.WS_PERFIX); } }

特别说明:如果对于配置类中这几个路径的设置看不明白,没关系,后面的前端部分你一看就懂了。


4、消息接口

说明:

1)、消息接口使用@MessageMapping注解,前面讲的配置类@EnableWebSocketMessageBroker注解开启后才能使用这个;

2)、这里稍微提一下,真正线上项目都是把websocket服务做成单独的网关形式,提供rest接口给其他服务调用,达到共用的目的,本项目因为不涉及任何数据库交互,所以直接用@MessageMapping注解,后续完整IM项目接入具体业务后会做一个独立的websocket服务,敬请关注哦!

package com.simple.ws.controller; import com.simple.ws.constants.WsConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.bind.annotation.*; import java.util.Map; /** * <p> * 消息接口 * </p> * * @author 福隆苑居士,公众号: * @since 2022-04-02 12:00 */ @RestController @RequestMapping("/api") @Slf4j public class MsgController { private final SimpMessagingTemplate messagingTemplate; public MsgController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } /** * 发送广播消息 * -- 说明: * 1)、@MessageMapping注解对应客户端的stomp.send('url'); * 2)、用法一:要么配合@SendTo("转发的订阅路径"),去掉messagingTemplate,同时return msg来使用,return msg会去找@SendTo注解的路径; * 3)、用法二:要么设置成void,使用messagingTemplate来控制转发的订阅路径,且不能return msg,个人推荐这种。 * * @param msg 消息 */ @MessageMapping("/send") public void sendAll(@RequestParam String msg) { log.info("[发送消息]>>>> msg: {}", msg); // 发送消息给客户端 messagingTemplate.convertAndSend(WsConstants.BROKER.BROKER_TOPIC, msg); }
5、前端项目结构

很简单,就是HTML+CSS和几个js文件,sockjs和stompjs就是和服务端通信的实现,可以从GitHub官网下载,而websocket.js是我们自己封装的和服务端通信的内容。


6、Stomp客户端使用

页面结构样式这里就省略不讲了,直接开始正文。

stompjs,是对websocket原生使用的一层封装,提供了更简单的调用方法。

这里先看看我们自己封装的websocket.js的实现:

1)、声明URL

也就是服务端配置类的端点地址

var url = "127.0.0.1:8888/websocket"; // 改成自己的服务端地址
2)、建立websocket连接

stompClient = Stomp.over(socket)就是覆盖了sockjs使用自己的客户端来操作ws;

进入stompClient.connect()中就代表连接成功,可以进行自己的业务处理比如广播通知某人上线等等,重要的是连接成功后要声明订阅列表,这样服务端转发的消息才会根据这些订阅地址发送过来,否则收不到;

最后就是有一个回调可以捕获异常情况,在里面可以做一些操作比如重连等等。

/** * 连接 */ function connect() { userId = GetUrlParam("userId"); var socket = new SockJS(url, null, { timeout: 15000}); stompClient = Stomp.over(socket); // 覆盖sockjs使用stomp客户端 stompClient.connect({}, function (frame) { console.log('frame: ' + frame) // 连接成功后广播通知 sendNoticeMsg(userId, "in"); /** * 订阅列表,订阅路径和服务端发消息路径一致就能收到消息。 * -- /topic: 服务端配置的广播订阅路径 * -- /queue/: 服务端配置的点对点订阅路径 */ stompClient.subscribe("/topic", function (response) { showMsg(response.body); }); stompClient.subscribe("/queue/" + userId + "/topic", function (response) { showMsg(response.body); }); // 异常时进行重连 }, function (error) { console.log('connect error: ' + error) if (reConnectCount > 10) { console.log("温馨提示:您的连接已断开,请退出后重新进入。") reConnectCount = 0; } else { wsReconnect && clearTimeout(wsReconnect); wsReconnect = setTimeout(function () { console.log("开始重连..."); connect(); console.log("重连完毕..."); reConnectCount++; }, 1000); } } ) }
3)、断开websocket连接

断开很简单,但要注意一点,不要根据关闭窗口或浏览器的事件来控制断开,这是一个误区,首先浏览器兼容性差异较大,传统的js在监听窗口关闭事件的兼容性上是很差的,这个可以自己试验就知道了,有些浏览器可以有些不可以;


其次,可以参考QQ,你自己在退出一个群聊的时候实际上你就单纯是关闭了,并没有离线,而是你退出QQ时才真正离线,所以真正控制这个断开方法的位置应该是点击退出按钮时,这一点不要理解错了。

/** * 断开 */ function disconnect() { if (stompClient != null) { // 断开连接时进行广播通知 sendNoticeMsg(userId, "out"); // 断开连接 stompClient.disconnect(function(){ // 有效断开的回调 console.log(userId + "断开连接....") }); } }
4)、消息滚动到底部

这个没什么说的,在进入页面以及发送消息后渲染页面时使用即可。

// 消息窗口滚动到底部 function scrollBotton(){ var div = document.getElementById("content"); div.scrollTop = div.scrollHeight; }
5)、聊天消息渲染到页面

这里就是单纯的JQuery操作了,注意的一点是这里加了个type判断是系统消息还是聊天消息,在本案例中,系统消息就是某人上下线的提示,聊天消息就是发送出来的内容。

在vue这样的框架中,这部分的操作其实会很简单。

/** * 聊天消息渲染到页面中 */ function showMsg(obj) { obj = JSON.parse(obj); var userId = obj.userId; var sendTime = obj.sendTime; var info = obj.info; var type = obj.type; if (1 === type) { // 聊天消息 console.log("聊天消息...") var msgHtml = "<div class=\"msg\" id=\"msg\">" + " <div class=\"first-line\">" + " <div class=\"userName\" id=\"userName\">" + userId + "</div>" + " <div class=\"sendTime\" id=\"sendTime\">" + sendTime + "</div>" + " </div>" + " <div class=\"second-line\">" + " <div class=\"sendMsg\" id=\"sendMsg\">" + info + "</div>" + " </div>" + "</div>"; // 渲染到页面 $("#content").html($("#content").html() + "\n" + msgHtml); } else if (2=== type) { // 系统消息 console.log("系统消息...") var msgHtml = "<div class=\"notice\">" + "<div class=\"notice-info\">" + info + "</div>" + "</div>"; // 渲染到页面 $("#content").html($("#content").html() + "\n" + msgHtml); } // 消息窗口滚动到底部 scrollBotton(); }
6)、发送群聊消息

这里传递的obj定义了一个消息体,就是一个对象,真正项目中也是这般使用,而不是单纯传递一个文本;


stompClient.send中的url,其中/app是服务端配置类中设置的ApplicationDestinationPrefixes,而/send就是controller接口中@MessageMapping("/send")的路径,两个加在一起就是这里前端发送的路径,少一个或多一个斜杠都会导致服务端收不到消息。

/** * 发送群聊消息 * -- 这里我们传递消息体对象 * { * "userId": userId, // 发送者 * "sendTime": sendTime, // 发送时间 * "info": info, // 发送内容 * "type": 1 // 消息类型,1-聊天消息,2-系统消息 * } */ function sendAll(obj) { stompClient.send("/app/send", {}, JSON.stringify(obj)); }
7)、发送系统消息

就是传递type=2即可,info做了下判断返回不同的消息内容。

/** * 发送系统通知消息 * @param userId 用户id */ function sendNoticeMsg(userId, action) { var obj = { "userId": userId, "sendTime": new Date().Format("HH:mm:ss"), "info": "in" === action ? userId + "进入房间" : userId + "离开房间", "type": 2 } sendAll(obj); }
7、聊天页发消息

index.html就是聊天主页面,直接调用我们前面封装好的websocket.js方法即可。

主要步骤为:进入页面时建立websocket连接 --> 获取登录用户信息 --> 监听按钮点击事件和键盘事件 --> 发送websocket消息 --> 清空文本框内容

这样,一旦发送消息成功,服务端就可以看到接收到的消息体并根据发送路径进行转发,前端websocket.js中订阅列表中的路径一旦和服务端转发的路径匹配上,就会收到消息,我们把消息渲染到页面上即可。

这个过程其实也就是websocket全双工通信的原理

<script> $(function() { // 启动websocket connect(); // 获取用户信息 getUser(); // 消息窗口滚动到底部 scrollBotton(); // 监听键盘Enter键,要用keyup,否则无法清除换行符。 $("#send-info").keyup(function(e) { var eCode = e.keyCode ? e.keyCode : e.which ? e.which : e.charCode; if (eCode == 13){ $("#send-btn").click(); } }); // 监听发送按钮点击事件 $("#send-btn").click(function() { send(); }); // 监听退出按钮点击事件 $("#exit-btn").click(function() { layer.confirm('你确定要退出吗?', { time: 0, // 不自动关闭 btn: ['确定退出', '再玩玩'], yes: function(index){ layer.close(index); disconnect(); window.location.href = 'login.html'; } }); }); }); // 获取用户信息 function getUser() { var userId = GetUrlParam("userId"); $("#userId").text(userId); } // 发送广播消息,这里定义一个type:1-聊天消息,2-系统消息。 function send() { var userId = $("#userId").text().trim(); var sendTime = new Date().Format("HH:mm:ss"); var info = $("#send-info").val().replace("\n", ""); var msg = { "userId": userId, "sendTime": sendTime, "info": info, "type": 1 } // 发送消息 sendAll(msg); // 清空文本域内容 $("#send-info").val(""); } </script>
避坑指南

1)、版本问题,经本人专门花时间测试,SpringBoot2.4.0以下版本才能整合SockJS和StompJS成功,以上的版本都不行,会报 Main site uses: "1.6.0", the iframe: "1.0.0" 这样的错误,将StompJS换成低版本也不行,所以这里整合时用了SpringBoot2.3.12.RELEASE版本,但这个没关系,websocket服务一般都是单独做成一个服务的,如果是微服务,你的其他业务服务使用高版本的SpringBoot就行了;

2)、监听窗口关闭事件不可取,这个在前面已经讲过了,浏览器兼容性差,我试过好几个浏览器监听效果都各不相同甚至完全无效,其次本身这样操作也不合理,我们只要保证退出时触发断开事件即可,无需在这样的事情上浪费时间,可以参考QQ;

3)、服务端编写消息接口时推荐使用SimpMessagingTemplate来控制发送,而不是@SendTo注解,因为前者更符合程序员开发思路,后期独立websocket服务暴露rest接口时也更简单;

4)、配置类中其实还有很多其他配置项,比如心跳配置、拦截器配置等,本案例没有加入进来,因为我自己公司的项目中其实使用过心跳,但后来又去掉了,因为对这块了解不深入的话贸然使用容易出现稀奇古怪的问题。

讲个趣事,我们前端工程师当初就因为心跳这块调试了挺久,上线后依然会出现时好时坏的情况,因为他之前也没做过websocket都是现学的,而且线上环境和测试环境差异难明,包含程序缺陷、网络环境因素等等,后来我们决定去掉心跳检测,之后两年也没出任何问题。

所以有时候保证项目稳定性反而更有用,但处于学习的角度而言,心跳检测是一定需要的,否则所有的socket框架也不会专门提供这样的方案了。


总结

SpringBoot+websocket的实现其实不难,你可以使用原生的实现,也就是websocket本身的OnOpen、OnClosed等等这样的注解来实现,以及对WebSocketHandler的实现,类似于netty的那种使用方式,而且原生的还提供了对websocket的监听,服务端能更好的控制及统计。

但根据我个人的经验而言,真实项目中还是使用Stomp实现的居多,因为独立服务更方便,便于后期搭建集群环境做横向扩展,且内置的方法也很简单,既然如此,我们还是以主流实现方式为准来学习吧。


源码

链接: pan.baidu.com/doc/share/tt1_DVkvVQ9BQoXJiM07hg-793418840542403
提取码: agiu
后续会根据本案例进行优化,设计具体的业务表,实现群聊、单聊、心跳检测,同时前端以vue3来搭建,实现一个完整的IM应用,有兴趣的可以关注下本人以获取最新资讯哦~



本人原创文章纯手打,觉得有一滴滴帮助的话,就请点个赞和推荐吧,鞠躬~


喜欢就点一下推荐吧~~

本文共计3972个文字,预计阅读时间需要16分钟。

Java分享客栈如何用SpringBoot和Stomp实现WebSocket群聊功能?

前言:近期,两位大学生小哥们向我透露了他们的私心,询问我是否可以提供毕业设计帮助。我表示暂时没有这个打算,因为工作实在繁忙,目前阶段无法投入到此类领域。其中两位小哥们又问道:我们WE呢?


前言

前两周经常有大学生小伙伴私信给我,问我可否有偿提供毕设帮助,我说暂时没有这个打算,因为工作实在太忙,现阶段无法投入到这样的领域内,其中有两个小伙伴又问到我websocket该怎么使用,想给自己的项目中加入这样的技术。

刚好我所在的公司有做问诊服务,里面就使用了websocket实现聊天通讯,就在闲暇之余专门把部分代码摘取出来,做了一个简单的demo分享给他们了,之后想想这块可以再丰富一下,就花时间又做了一个更完整的小项目出来,且加了详细的注释说明,分享给对websocket感兴趣的小伙伴们。


案例展示

Java分享客栈如何用SpringBoot和Stomp实现WebSocket群聊功能?


技术栈

考虑到不同群体对vue等前端技术的接受程度,本案例采用了HTML+CSS+JQuery来实现,代码直接复制到vue项目中也是一样的,只是赋值和取值的方式改变而已,很多Java程序员其实对于一门简单案例的学习不喜欢牵扯太多前端技术,而是单纯学习想知道的这门技术就好,太多其他的引入反而影响跟踪调试,而原始的HTML+JS方式更有利于我们学习和理解,只需要右键HTML页面在浏览器打开进行F12调试即可。

技术 版本 Java 1.8 SpringBoot 2.3.12.RELEASE WebSocket 2.3.12.RELEASE Hutools 5.8.0.M1 SockJS 1.6.0 StompJS 1.7.1
实现过程 1、引入依赖

<!-- websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Hutools工具类 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.0.M1</version> </dependency>
2、订阅常量类

后面的websocket配置类会用到这几个常量

stomp端点地址: 连接websocket时的后缀地址,比如127.0.0.1:8888/websocket。

websocket前缀:前端调服务端消息接口时的URL都加上了这个前缀,比如默认是/send,变成/app/send。

点对点代理地址:如果websocket配置类中设置了代理路径,一般点对点订阅路径喜欢用/queue。

广播代理地址:如果websocket配置类中设置了代理路径,一般广播订阅路径喜欢用这个/topic。

package com.simple.ws.constants; /** * <p> * websocket常量 * </p> * * @author 福隆苑居士,公众号: * @since 2022-04-02 10:11 */ public class WsConstants { // stomp端点地址 public static final String WEBSOCKET_PATH = "/websocket"; // websocket前缀 public static final String WS_PERFIX = "/app"; // 消息订阅地址常量 public static final class BROKER { // 点对点消息代理地址 public static final String BROKER_QUEUE = "/queue/"; // 广播消息代理地址 public static final String BROKER_TOPIC = "/topic"; } }
3、WebSocket配置类

核心内容讲解:

1)、@EnableWebSocketMessageBroker:用于开启stomp协议,这样就能支持@MessageMapping注解,类似于@requestMapping一样,同时前端可以使用Stomp客户端进行通讯;

2)、registerStompEndpoints实现:主要用来注册端点地址、开启跨域授权、增加拦截器、声明SockJS,这也是前端选择SockJS的原因,因为spring项目本身就支持;

3)、configureMessageBroker实现:主要用来设置客户端订阅消息的路径(可以多个)、点对点订阅路径前缀的设置、访问服务端@MessageMapping接口的前缀路径、心跳设置等;

package com.simple.ws.config; import com.simple.ws.constants.WsConstants; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.*; /** * <p> * websocket核心配置类 * </p> * * @author 福隆苑居士,公众号: * @since 2022/4/1 22:57 */ @Configuration @EnableWebSocketMessageBroker public class WebsocketConfig implements WebSocketMessageBrokerConfigurer { /** * 注册stomp端点 * * @param registry stomp端点注册对象 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(WsConstants.WEBSOCKET_PATH) .setAllowedOrigins("*") .withSockJS(); } /** * 配置消息代理 * * @param registry 消息代理注册对象 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 配置服务端推送消息给客户端的代理路径 registry.enableSimpleBroker(WsConstants.BROKER.BROKER_QUEUE, WsConstants.BROKER.BROKER_TOPIC); // 定义点对点推送时的前缀为/queue registry.setUserDestinationPrefix(WsConstants.BROKER.BROKER_QUEUE); // 定义客户端访问服务端消息接口时的前缀 registry.setApplicationDestinationPrefixes(WsConstants.WS_PERFIX); } }

特别说明:如果对于配置类中这几个路径的设置看不明白,没关系,后面的前端部分你一看就懂了。


4、消息接口

说明:

1)、消息接口使用@MessageMapping注解,前面讲的配置类@EnableWebSocketMessageBroker注解开启后才能使用这个;

2)、这里稍微提一下,真正线上项目都是把websocket服务做成单独的网关形式,提供rest接口给其他服务调用,达到共用的目的,本项目因为不涉及任何数据库交互,所以直接用@MessageMapping注解,后续完整IM项目接入具体业务后会做一个独立的websocket服务,敬请关注哦!

package com.simple.ws.controller; import com.simple.ws.constants.WsConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.bind.annotation.*; import java.util.Map; /** * <p> * 消息接口 * </p> * * @author 福隆苑居士,公众号: * @since 2022-04-02 12:00 */ @RestController @RequestMapping("/api") @Slf4j public class MsgController { private final SimpMessagingTemplate messagingTemplate; public MsgController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } /** * 发送广播消息 * -- 说明: * 1)、@MessageMapping注解对应客户端的stomp.send('url'); * 2)、用法一:要么配合@SendTo("转发的订阅路径"),去掉messagingTemplate,同时return msg来使用,return msg会去找@SendTo注解的路径; * 3)、用法二:要么设置成void,使用messagingTemplate来控制转发的订阅路径,且不能return msg,个人推荐这种。 * * @param msg 消息 */ @MessageMapping("/send") public void sendAll(@RequestParam String msg) { log.info("[发送消息]>>>> msg: {}", msg); // 发送消息给客户端 messagingTemplate.convertAndSend(WsConstants.BROKER.BROKER_TOPIC, msg); }
5、前端项目结构

很简单,就是HTML+CSS和几个js文件,sockjs和stompjs就是和服务端通信的实现,可以从GitHub官网下载,而websocket.js是我们自己封装的和服务端通信的内容。


6、Stomp客户端使用

页面结构样式这里就省略不讲了,直接开始正文。

stompjs,是对websocket原生使用的一层封装,提供了更简单的调用方法。

这里先看看我们自己封装的websocket.js的实现:

1)、声明URL

也就是服务端配置类的端点地址

var url = "127.0.0.1:8888/websocket"; // 改成自己的服务端地址
2)、建立websocket连接

stompClient = Stomp.over(socket)就是覆盖了sockjs使用自己的客户端来操作ws;

进入stompClient.connect()中就代表连接成功,可以进行自己的业务处理比如广播通知某人上线等等,重要的是连接成功后要声明订阅列表,这样服务端转发的消息才会根据这些订阅地址发送过来,否则收不到;

最后就是有一个回调可以捕获异常情况,在里面可以做一些操作比如重连等等。

/** * 连接 */ function connect() { userId = GetUrlParam("userId"); var socket = new SockJS(url, null, { timeout: 15000}); stompClient = Stomp.over(socket); // 覆盖sockjs使用stomp客户端 stompClient.connect({}, function (frame) { console.log('frame: ' + frame) // 连接成功后广播通知 sendNoticeMsg(userId, "in"); /** * 订阅列表,订阅路径和服务端发消息路径一致就能收到消息。 * -- /topic: 服务端配置的广播订阅路径 * -- /queue/: 服务端配置的点对点订阅路径 */ stompClient.subscribe("/topic", function (response) { showMsg(response.body); }); stompClient.subscribe("/queue/" + userId + "/topic", function (response) { showMsg(response.body); }); // 异常时进行重连 }, function (error) { console.log('connect error: ' + error) if (reConnectCount > 10) { console.log("温馨提示:您的连接已断开,请退出后重新进入。") reConnectCount = 0; } else { wsReconnect && clearTimeout(wsReconnect); wsReconnect = setTimeout(function () { console.log("开始重连..."); connect(); console.log("重连完毕..."); reConnectCount++; }, 1000); } } ) }
3)、断开websocket连接

断开很简单,但要注意一点,不要根据关闭窗口或浏览器的事件来控制断开,这是一个误区,首先浏览器兼容性差异较大,传统的js在监听窗口关闭事件的兼容性上是很差的,这个可以自己试验就知道了,有些浏览器可以有些不可以;


其次,可以参考QQ,你自己在退出一个群聊的时候实际上你就单纯是关闭了,并没有离线,而是你退出QQ时才真正离线,所以真正控制这个断开方法的位置应该是点击退出按钮时,这一点不要理解错了。

/** * 断开 */ function disconnect() { if (stompClient != null) { // 断开连接时进行广播通知 sendNoticeMsg(userId, "out"); // 断开连接 stompClient.disconnect(function(){ // 有效断开的回调 console.log(userId + "断开连接....") }); } }
4)、消息滚动到底部

这个没什么说的,在进入页面以及发送消息后渲染页面时使用即可。

// 消息窗口滚动到底部 function scrollBotton(){ var div = document.getElementById("content"); div.scrollTop = div.scrollHeight; }
5)、聊天消息渲染到页面

这里就是单纯的JQuery操作了,注意的一点是这里加了个type判断是系统消息还是聊天消息,在本案例中,系统消息就是某人上下线的提示,聊天消息就是发送出来的内容。

在vue这样的框架中,这部分的操作其实会很简单。

/** * 聊天消息渲染到页面中 */ function showMsg(obj) { obj = JSON.parse(obj); var userId = obj.userId; var sendTime = obj.sendTime; var info = obj.info; var type = obj.type; if (1 === type) { // 聊天消息 console.log("聊天消息...") var msgHtml = "<div class=\"msg\" id=\"msg\">" + " <div class=\"first-line\">" + " <div class=\"userName\" id=\"userName\">" + userId + "</div>" + " <div class=\"sendTime\" id=\"sendTime\">" + sendTime + "</div>" + " </div>" + " <div class=\"second-line\">" + " <div class=\"sendMsg\" id=\"sendMsg\">" + info + "</div>" + " </div>" + "</div>"; // 渲染到页面 $("#content").html($("#content").html() + "\n" + msgHtml); } else if (2=== type) { // 系统消息 console.log("系统消息...") var msgHtml = "<div class=\"notice\">" + "<div class=\"notice-info\">" + info + "</div>" + "</div>"; // 渲染到页面 $("#content").html($("#content").html() + "\n" + msgHtml); } // 消息窗口滚动到底部 scrollBotton(); }
6)、发送群聊消息

这里传递的obj定义了一个消息体,就是一个对象,真正项目中也是这般使用,而不是单纯传递一个文本;


stompClient.send中的url,其中/app是服务端配置类中设置的ApplicationDestinationPrefixes,而/send就是controller接口中@MessageMapping("/send")的路径,两个加在一起就是这里前端发送的路径,少一个或多一个斜杠都会导致服务端收不到消息。

/** * 发送群聊消息 * -- 这里我们传递消息体对象 * { * "userId": userId, // 发送者 * "sendTime": sendTime, // 发送时间 * "info": info, // 发送内容 * "type": 1 // 消息类型,1-聊天消息,2-系统消息 * } */ function sendAll(obj) { stompClient.send("/app/send", {}, JSON.stringify(obj)); }
7)、发送系统消息

就是传递type=2即可,info做了下判断返回不同的消息内容。

/** * 发送系统通知消息 * @param userId 用户id */ function sendNoticeMsg(userId, action) { var obj = { "userId": userId, "sendTime": new Date().Format("HH:mm:ss"), "info": "in" === action ? userId + "进入房间" : userId + "离开房间", "type": 2 } sendAll(obj); }
7、聊天页发消息

index.html就是聊天主页面,直接调用我们前面封装好的websocket.js方法即可。

主要步骤为:进入页面时建立websocket连接 --> 获取登录用户信息 --> 监听按钮点击事件和键盘事件 --> 发送websocket消息 --> 清空文本框内容

这样,一旦发送消息成功,服务端就可以看到接收到的消息体并根据发送路径进行转发,前端websocket.js中订阅列表中的路径一旦和服务端转发的路径匹配上,就会收到消息,我们把消息渲染到页面上即可。

这个过程其实也就是websocket全双工通信的原理

<script> $(function() { // 启动websocket connect(); // 获取用户信息 getUser(); // 消息窗口滚动到底部 scrollBotton(); // 监听键盘Enter键,要用keyup,否则无法清除换行符。 $("#send-info").keyup(function(e) { var eCode = e.keyCode ? e.keyCode : e.which ? e.which : e.charCode; if (eCode == 13){ $("#send-btn").click(); } }); // 监听发送按钮点击事件 $("#send-btn").click(function() { send(); }); // 监听退出按钮点击事件 $("#exit-btn").click(function() { layer.confirm('你确定要退出吗?', { time: 0, // 不自动关闭 btn: ['确定退出', '再玩玩'], yes: function(index){ layer.close(index); disconnect(); window.location.href = 'login.html'; } }); }); }); // 获取用户信息 function getUser() { var userId = GetUrlParam("userId"); $("#userId").text(userId); } // 发送广播消息,这里定义一个type:1-聊天消息,2-系统消息。 function send() { var userId = $("#userId").text().trim(); var sendTime = new Date().Format("HH:mm:ss"); var info = $("#send-info").val().replace("\n", ""); var msg = { "userId": userId, "sendTime": sendTime, "info": info, "type": 1 } // 发送消息 sendAll(msg); // 清空文本域内容 $("#send-info").val(""); } </script>
避坑指南

1)、版本问题,经本人专门花时间测试,SpringBoot2.4.0以下版本才能整合SockJS和StompJS成功,以上的版本都不行,会报 Main site uses: "1.6.0", the iframe: "1.0.0" 这样的错误,将StompJS换成低版本也不行,所以这里整合时用了SpringBoot2.3.12.RELEASE版本,但这个没关系,websocket服务一般都是单独做成一个服务的,如果是微服务,你的其他业务服务使用高版本的SpringBoot就行了;

2)、监听窗口关闭事件不可取,这个在前面已经讲过了,浏览器兼容性差,我试过好几个浏览器监听效果都各不相同甚至完全无效,其次本身这样操作也不合理,我们只要保证退出时触发断开事件即可,无需在这样的事情上浪费时间,可以参考QQ;

3)、服务端编写消息接口时推荐使用SimpMessagingTemplate来控制发送,而不是@SendTo注解,因为前者更符合程序员开发思路,后期独立websocket服务暴露rest接口时也更简单;

4)、配置类中其实还有很多其他配置项,比如心跳配置、拦截器配置等,本案例没有加入进来,因为我自己公司的项目中其实使用过心跳,但后来又去掉了,因为对这块了解不深入的话贸然使用容易出现稀奇古怪的问题。

讲个趣事,我们前端工程师当初就因为心跳这块调试了挺久,上线后依然会出现时好时坏的情况,因为他之前也没做过websocket都是现学的,而且线上环境和测试环境差异难明,包含程序缺陷、网络环境因素等等,后来我们决定去掉心跳检测,之后两年也没出任何问题。

所以有时候保证项目稳定性反而更有用,但处于学习的角度而言,心跳检测是一定需要的,否则所有的socket框架也不会专门提供这样的方案了。


总结

SpringBoot+websocket的实现其实不难,你可以使用原生的实现,也就是websocket本身的OnOpen、OnClosed等等这样的注解来实现,以及对WebSocketHandler的实现,类似于netty的那种使用方式,而且原生的还提供了对websocket的监听,服务端能更好的控制及统计。

但根据我个人的经验而言,真实项目中还是使用Stomp实现的居多,因为独立服务更方便,便于后期搭建集群环境做横向扩展,且内置的方法也很简单,既然如此,我们还是以主流实现方式为准来学习吧。


源码

链接: pan.baidu.com/doc/share/tt1_DVkvVQ9BQoXJiM07hg-793418840542403
提取码: agiu
后续会根据本案例进行优化,设计具体的业务表,实现群聊、单聊、心跳检测,同时前端以vue3来搭建,实现一个完整的IM应用,有兴趣的可以关注下本人以获取最新资讯哦~



本人原创文章纯手打,觉得有一滴滴帮助的话,就请点个赞和推荐吧,鞠躬~


喜欢就点一下推荐吧~~