前言

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

刚好我所在的公司有做问诊服务,里面就使用了websocket实现聊天通讯,就在闲暇之余专门把部分代码摘取出来,做了一个简单的demo分享给他们了,之后想想这块可以再丰富一下,就花时间又做了一个更完整的小项目出来,且加了详细的注释说明,分享给对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 福隆苑居士,公众号:【Java分享客栈】
* @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 福隆苑居士,公众号:【Java分享客栈】
* @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 福隆苑居士,公众号:【Java分享客栈】
* @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 = "http://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实现的居多,因为独立服务更方便,便于后期搭建集群环境做横向扩展,且内置的方法也很简单,既然如此,我们还是以主流实现方式为准来学习吧。

源码

链接: https://pan.baidu.com/s/1D34kJ1TO4evQlvUwHiN7eg?pwd=ht71

提取码: ht71



后续会根据本案例进行优化,设计具体的业务表,实现群聊、单聊、心跳检测,同时前端以vue3来搭建,实现一个完整的IM应用,有兴趣的可以关注下本人以获取最新资讯哦~


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

【Java分享客栈】SpringBoot整合WebSocket+Stomp搭建群聊项目的更多相关文章

  1. 【Java分享客栈】超简洁SpringBoot使用AOP统一日志管理-纯干货干到便秘

    前言 请问今天您便秘了吗?程序员坐久了真的会便秘哦,如果偶然点进了这篇小干货,就麻烦您喝杯水然后去趟厕所一边用左手托起对准嘘嘘,一边用右手滑动手机看完本篇吧. 实现 本篇AOP统一日志管理写法来源于国 ...

  2. 【Java分享客栈】SpringBoot线程池参数搜一堆资料还是不会配,我花一天测试换你此生明白。

    一.前言   首先说一句,如果比较忙顺路点进来的,可以先收藏,有时间或用到了再看也行:   我相信很多人会有一个困惑,这个困惑和我之前一样,就是线程池这个玩意儿,感觉很高大上,用起来很fashion, ...

  3. SpringBoot 整合 WebSocket

    SpringBoot 整合 WebSocket(topic广播) 1.什么是WebSocket WebSocket为游览器和服务器提供了双工异步通信的功能,即游览器可以向服务器发送消息,服务器也可以向 ...

  4. 【Java分享客栈】我为什么极力推荐XXL-JOB作为中小厂的分布式任务调度平台

    前言   大家好,我是福隆苑居士,今天给大家聊聊XXL-JOB的使用.   XXL-JOB是本人呆过的三家公司都使用到的分布式任务调度平台,前两家都是服务于传统行业(某大型移动基地和某大型电网),现在 ...

  5. SpringBoot整合websocket简单示例

    依赖 <!-- springboot整合websocket --> <dependency> <groupId>org.springframework.boot&l ...

  6. 【Java分享客栈】一文搞定京东零售开源的AsyncTool,彻底解决异步编排问题。

    一.前言 本章主要是承接上一篇讲CompletableFuture的文章,想了解的可以先去看看案例: https://juejin.cn/post/7091132240574283813 Comple ...

  7. Springboot整合Websocket遇到的坑

    Springboot整合Websocket遇到的坑 一.使用Springboot内嵌的tomcat启动websocket 1.添加ServerEndpointExporter配置bean @Confi ...

  8. springboot整合websocket原生版

    目录 HTTP缺点 HTTP websocket区别 websocket原理 使用场景 springboot整合websocket 环境准备 客户端连接 加入战队 微信公众号 主题 HTTP请求用于我 ...

  9. 【Java分享客栈】一文搞定CompletableFuture并行处理,成倍缩短查询时间。

    前言   工作中你可能会遇到很多这样的场景,一个接口,要从其他几个service调用查询方法,分别获取到需要的值之后再封装数据返回.   还可能在微服务中遇到类似的情况,某个服务的接口,要使用好几次f ...

随机推荐

  1. ln -s 软链接知识总结

    ln -s 软链接知识总结 1.软连建立:ln  -s  源文件 软链接文件 2.误区:软链接是创建的,就意味着软链接文件不可以在创建之前存在 3.类比:win快捷方式 4.删除:rm就可以,但源文件 ...

  2. NTFS权限详解

    NTFS权限是作为一个Windows管理员必备的知识,许多经验丰富的管理员都能够很熟悉地对文件.文件夹.注册表项等进行安全性的权限设置,包括完全控制.修改.只读等.而谈论NTFS权限这个话题也算是老生 ...

  3. ArcMap操作随记(5)

    1.[栅格转面]等工具的使用 若栅格数据为浮点型,需使用[转为整型]工具,将栅格转为整型,再进行操作. 2.人口密度分布趋势图 使用[核密度分析]工具,也可尝试插值 3.点要素做面 [点集转线][要素 ...

  4. 【dubbo3.x trace组件分享】

    目录 背景 一.trace-dubbo组件介绍 二.设计原理 2.1 原理图 2.2 实现方案 2.2.1 consumer端实现 2.2.2 provider端实现 2.2.3 traceId和sp ...

  5. Kafka的优秀设计学习

    一.Kafka基础 消息系统的作用 应该大部份小伙伴都清楚,用机油装箱举个例子 所以消息系统就是如上图我们所说的仓库,能在中间过程作为缓存,并且实现解耦合的作用. 引入一个场景,我们知道中国移动,中国 ...

  6. asp.net MVC 事务

    使用事务的目的是什么? 保证事务范围内的代码,要么全部执行,要么全不执行,也就是出错回滚. 写在数据库脚本里很好理解,但是用在应用程序层面,没有看到catcha error rallback的代码,心 ...

  7. eclipse启动指定jvm的版本

    参阅:https://www.eclipse.org/forums/index.php/t/1105435/ https://wiki.eclipse.org/Eclipse.ini#-vm_valu ...

  8. ssl免密登录(centos6)

    1.首先执行ll -a查看是否有隐藏文件.ssh,如果没有,需要执行ssh localhost登录以下即可 cd ~/.ssh 2.生成秘钥: 可查看https://hadoop.apache.org ...

  9. 说说 Redis 哈希槽的概念?

    Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽, 集群的每个节点 ...

  10. 哪些浏览器支持HTML 5?

    几乎所有的浏览器都支持HTML 5,例如Safari,Chrome,火狐,Opera,IE等.