[Java Sharing Inn] SpringBoot integra WebSocket+Stomp para crear un proyecto de chat grupal


prefacio

En las últimas dos semanas, los estudiantes universitarios a menudo me enviaban mensajes privados, preguntándome si podía ayudarlos a completar el proyecto por una tarifa. Dije que no tengo ningún plan para este momento, porque estoy demasiado ocupado con el trabajo y no puedo. invertir en un campo de este tipo en esta etapa. Entre ellos, hay dos pequeños socios. Me preguntó cómo usar websocket y quería agregar esa tecnología a mi proyecto.

Sucede que la empresa para la que trabajo tiene servicios de consulta y utiliza websocket para realizar la comunicación por chat, en mi tiempo libre extraje especialmente algunos códigos e hice una demostración simple para compartir con ellos, después de pensarlo puedo enriquecer de nuevo, me tomé el tiempo para hacer un pequeño proyecto más completo y agregué anotaciones detalladas para compartirlo con amigos que estén interesados ​​en websocket.


demostración de caso

Caso de chat demo.gif


pila de tecnología

Teniendo en cuenta la aceptación de tecnologías front-end como vue por parte de diferentes grupos, en este caso se utiliza HTML+CSS+JQuery. Lo mismo es cierto para copiar el código directamente en el proyecto vue, pero la forma de asignar y recuperar valores es cambiado. Muchos programadores de Java De hecho, no me gusta involucrar demasiadas tecnologías front-end en el estudio de un caso simple, sino simplemente aprender la tecnología que queremos saber. Demasiadas otras introducciones afectarán el seguimiento y la depuración. , y el método HTML+JS original es más beneficioso para nosotros. Para aprender y comprender, simplemente haga clic con el botón derecho en la página HTML y ábrala en el navegador para la depuración F12.

Tecnología Versión
Java 1.8
SpringBoot 2.3.12.LIBERAR
WebSocket 2.3.12.LIBERAR
Hutools 5.8.0.M1
CalcetínJS 1.6.0
TocónJS 1.7.1

Proceso de implementación

1. Introducir dependencias

<!-- 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. Clase constante de suscripción

后面的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是我们自己封装的和服务端通信的内容。

111.png

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框架也不会专门提供这样的方案了。


总结

La implementación de SpringBoot+websocket en realidad no es difícil, puede usar la implementación nativa, es decir, las anotaciones como OnOpen, OnClosed, etc. del propio websocket, así como la implementación de WebSocketHandler, similar a la forma de usar netty, y el nativo También proporciona monitoreo de websockets, y el servidor puede controlar mejor y estadísticas.

Pero según mi experiencia personal, la mayoría de los proyectos reales se implementan con Stomp, porque los servicios independientes son más convenientes, es conveniente construir un entorno de clúster para la expansión horizontal en la etapa posterior, y el método incorporado también es muy simple. . En este caso, todavía usamos la implementación estándar. Aprenda de la manera que desee.


código fuente

Enlace: pan.baidu.com/s/1D34kJ1TO...
Código de extracción: ht71

se optimizará de acuerdo con este caso en el futuro, diseñe una mesa de negocios específica, realice chat grupal, chat único, detección de latidos y la interfaz es construido con vue3 para lograr una aplicación de mensajería instantánea completa, si está interesado, puede seguirme para obtener la información más reciente ~



Mi artículo original está escrito puramente a mano. Si crees que hay un poco de ayuda, por favor dale me gusta y haz una reverencia ~


Supongo que te gusta

Origin juejin.im/post/7083020131412115464
Recomendado
Clasificación