【技术派后端篇】整合WebSocket长连接实现消息实时推送

在技术派平台中,实现了文章被点赞或评论后,在右上角实时弹出消息提醒的功能。相较于之前仅有的消息通知红色标识,这种实时通知在交互体验上有显著提升。本文将详细介绍如何借助WebSocket实现消息的实时通知。

1 基础知识点

1.1 相关概念

  1. WebSocket:WebSocket是一种在单个TCP连接上进行全双工通信的协议,能使客户端和服务器实时地双向传输数据,无需频繁建立和关闭连接,可提高数据传输的效率和性能。
  2. STOMPSimple Text Oriented Messaging Protocol,即简单文本定向消息协议,是在HTTP之上实现的简单灵活的消息传递协议,定义了一套简单的命令和帧格式用于客户端和服务器间的消息传递,可实现双向通讯。需注意,STOMP协议属于WebSocket的子协议。

1.2 WebSocket整合STOMP

WebSocket整合STOMP协议实现双向通讯的主要步骤如下:

  1. 建立WebSocket连接:客户端通过JavaScript的WebSocket API与服务器建立连接。
  2. 发送STOMP帧:连接建立后,客户端和服务器通过发送STOMP帧进行通信,STOMP帧是STOMP协议的基本单位,定义了订阅、发布等消息操作。
  3. 处理STOMP帧:服务器收到STOMP帧后,根据帧类型进行相应处理,如收到SUBSCRIBE帧为客户端创建订阅,收到SEND帧将消息发送到指定目的地。
  4. 关闭WebSocket连接:通信完成后,通过调用WebSocket API的close方法关闭连接。
    在这里插入图片描述

1.3 SpringBoot整合STOMP流程

SpringBoot对WebSocket提供了友好封装,便于搭建基于STOMP协议的WebSocket应用工程。其基本工作流程如下:

  1. 步骤1:初始化

    • 服务端:定义接收WebSocket连接的端点EndPoint;配置消息代理Broker,用于前端订阅,后端向Broker写入消息后,订阅该Broker的前端会收到相应消息;配置路由转发规则,将用户信息转发给相应处理器(类似RequestMappingHandlerMapping@RequestMapping注解,WebSocket中使用Destination + @MessageMapping)。
    • 客户端:与EndPoint建立连接;订阅Broker并注册消息回调。
      在这里插入图片描述
  2. 步骤2:通讯

    • 服务端:主动向Broker写入消息,使用simpMessagingTemplate;消息应答使用@SendTo注解。
    • 客户端:发送消息调用send(xxxx)方法;消息应答触发订阅的回调函数。
      在这里插入图片描述

2 WebSocket集成

相关示例demo可在https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/203-websocket获取。

2.1 依赖配置

在Spring Boot应用的pom.xml配置文件中,引入WebSocket的核心依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.2 WebSocket配置

后端通过实现配置类StompConfiguration来定义相关信息:

@Configuration
@EnableWebSocketMessageBroker
public class StompConfiguration implements WebSocketMessageBrokerConfigurer {

    // 配置消息代理
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic"); // 客户端订阅地址前缀
        registry.setApplicationDestinationPrefixes("/app"); // 服务端接收地址前缀
    }

    // 注册STOMP端点
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/hello").withSockJS(); // 客户端连接端点
    }
}
  1. 定义端点:使用registerStompEndpoints()方法。
  2. 定义客户端与服务端通讯信息:使用configureMessageBroker()方法,配置消息代理registry.enableSimpleBroker,配置消息转发registry.setApplicationDestinationPrefixes(转发前缀可为多个)。

配置完成后,客户端的相关路径为:建立连接路径是/ws/hello;订阅消息路径是/topic/xxx(由@SendTo的路径确定);接收前端消息路径是/app/xxx(由@MessageMapping中的路径补齐)。

2.3 消息接收应答

实现简单消息应答,接收客户端向/app/hello发送的消息,将结果写回到/topic/hello对应的Broker,订阅该Broker的客户端会收到应答消息。

@Controller
public class HelloController {
    
    // 处理/app/hello路径的STOMP消息
    @MessageMapping("/hello")
    @SendTo("/topic/hello") // 将返回结果广播到指定主题
    public String greeting(String message) {
        return "[" + LocalDateTime.now() + "]: " + message;
    }
}

同时,编写定时器,由后端定时向/topic/hello这个Broker中写入消息,模拟后台主动下发消息场景。

@Autowired
private SimpMessagingTemplate simpMessagingTemplate;

// 定时推送示例(当前被注释)
@Scheduled(cron = "0/10 * * * * ?")
public void autoPush() {
    simpMessagingTemplate.convertAndSend("/topic/hello", "系统自动消息");
}

2.4 执行流程解析

var stompClient = null;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

function connect() {
    var socket = new SockJS('/ws/hello');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/hello', function (greeting) {
            console.log("resp: ", greeting.body)
            showGreeting(greeting.body);
        });
    });
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $("#connect").click(function () {
        connect();
    });
    $("#disconnect").click(function () {
        disconnect();
    });
    $("#send").click(function () {
        sendName();
    });
});

核心js逻辑实现WebSocket连接建立与通讯:

  1. 建立连接connect():通过new SockJS('/ws/hello')与后端端点建立连接;连接成功后,使用stompClient.subscribe('/topic/hello', 消息应答回调)订阅Broker并接收消息回传。
  2. 发送消息:调用stompClient.send("/app/hello", xxx)方法。
  3. 断开连接:调用stompClient.disconnect()方法。

2.5 效果演示

完成基于Spring Boot整合WebSocket与STOMP协议的示例应用搭建后,启动应用,可看到前端建立连接后向后端发送信息并接收后端广播消息的过程,多个客户端订阅同一Broker时,能收到后端发送的消息。
在这里插入图片描述

3 技术派基于WebSocket的消息通知

3.1 消息推送配置

WsChatConfig 是一个 Spring 配置类,其主要功能是配置基于 STOMP(Simple Text Oriented Messaging Protocol)协议的 WebSocket 通信。下面按照代码结构详细说明其逻辑:

  1. 类定义与注解
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WsChatConfig implements WebSocketMessageBrokerConfigurer {
  • @Slf4j:Lombok 提供的注解,会自动为该类生成一个日志记录器 log,方便日志输出。
  • @Configuration:Spring 注解,表明该类是一个配置类,Spring 容器会扫描并加载其中定义的 Bean。
  • @EnableWebSocketMessageBroker:启用基于消息代理的 WebSocket 支持,允许应用使用 STOMP 协议进行消息传递。
  • implements WebSocketMessageBrokerConfigurer:实现该接口,需要重写其中的方法来定制 WebSocket 消息代理的配置。
  1. 配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/chat");
    config.setApplicationDestinationPrefixes("/app");
}
  • config.enableSimpleBroker("/chat"):启用一个简单的基于内存的消息代理,客户端订阅以 /chat 开头的目的地后,就可以接收从服务端广播的消息。
  • config.setApplicationDestinationPrefixes("/app"):设置应用程序的目标前缀,以 /app 开头的消息会被路由到带有 @MessageMapping 注解的控制器方法进行处理。
  1. 注册 STOMP 端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/gpt/{id}/{aiType}")
            .setHandshakeHandler(new AuthHandshakeHandler())
            .addInterceptors(new AuthHandshakeInterceptor())
            .setAllowedOriginPatterns("*");
}
  • registry.addEndpoint("/gpt/{id}/{aiType}"):注册一个 WebSocket 端点,客户端可以通过这个端点连接到服务端。{id}{aiType} 是路径变量,可用于标识不同的用户和 AI 类型。
  • .setHandshakeHandler(new AuthHandshakeHandler()):设置自定义的握手处理器,用于在 WebSocket 握手阶段进行身份验证等操作。
  • .addInterceptors(new AuthHandshakeInterceptor()):添加自定义的握手拦截器,进一步处理握手过程中的逻辑。
  • .setAllowedOriginPatterns("*"):允许所有来源的跨域请求,确保不同域名的客户端都能连接到该端点。
  1. 配置消息通道拦截器
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.interceptors(channelInInterceptor());
}

@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
    registration.interceptors(channelOutInterceptor());
}
  • configureClientInboundChannel:配置客户端发送到服务端的消息通道,添加入站拦截器 channelInInterceptor(),用于处理客户端发来的消息。
  • configureClientOutboundChannel:配置服务端发送到客户端的消息通道,添加出站拦截器 channelOutInterceptor(),用于处理服务端返回给客户端的消息。
  1. 定义 Bean
@Bean
public HandshakeHandler handshakeHandler() {
    return new AuthHandshakeHandler();
}

@Bean
public HttpSessionHandshakeInterceptor handshakeInterceptor() {
    return new AuthHandshakeInterceptor();
}

@Bean
public ChannelInterceptor channelInInterceptor() {
    return new AuthInChannelInterceptor();
}

@Bean
public ChannelInterceptor channelOutInterceptor() {
    return new AuthOutChannelInterceptor();
}
  • 使用 @Bean 注解将自定义的握手处理器、握手拦截器、入站拦截器和出站拦截器注册为 Spring Bean,方便 Spring 容器进行管理和使用。

综上所述,WsChatConfig 类通过实现 WebSocketMessageBrokerConfigurer 接口,完成了 WebSocket 端点的注册、消息代理的配置以及消息通道拦截器的设置,从而实现了基于 STOMP 协议的 WebSocket 通信功能,同时加入了身份验证等安全机制。

3.2 身份鉴权拦截器

AuthHandshakeInterceptor 类继承自 HttpSessionHandshakeInterceptor,主要作用是在 WebSocket 握手阶段进行用户身份验证和识别。下面详细解释其代码逻辑:

  1. 类定义和继承
public class AuthHandshakeInterceptor extends HttpSessionHandshakeInterceptor {

这行代码定义了 AuthHandshakeInterceptor 类,并继承自 HttpSessionHandshakeInterceptorHttpSessionHandshakeInterceptor 是 Spring 提供的用于在 WebSocket 握手过程中处理 HTTP 会话的拦截器,继承它可以利用其已有的功能,同时自定义握手逻辑。

  1. 握手前的身份验证 (beforeHandshake 方法)
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
    log.info("准备开始握手了!");
    String session = SessionUtil.findCookieByName(request, LoginService.SESSION_KEY);
    ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo();
    SpringUtil.getBean(GlobalInitService.class).initLoginUser(session, reqInfo);

    if (reqInfo.getUser() == null) {
        log.info("websocket 握手失败,请登录之后再试");
        return false;
    }

    // 将用户信息写入到属性中
    attributes.put(MdcUtil.TRACE_ID_KEY, SelfTraceIdGenerator.generate());
    attributes.put(LoginService.SESSION_KEY, reqInfo);
    attributes.put(WsAnswerHelper.AI_SOURCE_PARAM, initAiSource(request.getURI().getPath()));
    return true;
}
  • 日志记录:使用 log.info 记录开始握手的信息。
  • 获取会话信息:通过 SessionUtil.findCookieByName 方法从请求中获取名为 LoginService.SESSION_KEYCookie 值。
  • 初始化用户信息:创建 ReqInfoContext.ReqInfo 对象,并调用 GlobalInitServiceinitLoginUser 方法,根据会话信息初始化用户信息。
  • 身份验证:检查 reqInfo 中的用户信息是否为空,如果为空则记录握手失败的日志并返回 false,表示握手失败。
  • 设置属性:如果身份验证通过,将生成的跟踪 ID、用户请求信息和 AI 来源信息添加到 attributes 中,这些信息将在后续的 WebSocket 通信中使用。最后返回 true,表示握手成功。
  1. 初始化 AI 来源 (initAiSource 方法)
private String initAiSource(String path) {
    int index = path.lastIndexOf("/");
    return path.substring(index + 1);
}

该方法从请求的 URI 路径中提取 AI 来源信息。通过查找最后一个 / 的位置,然后截取该位置之后的字符串作为 AI 来源。

  1. 握手后的处理 (afterHandshake 方法)
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
    log.info("握手成功了!!!");
    super.afterHandshake(request, response, wsHandler, ex);
}

在握手成功后,使用 log.info 记录握手成功的日志,并调用父类的 afterHandshake 方法完成后续处理。

综上所述,AuthHandshakeInterceptor 类在 WebSocket 握手过程中进行用户身份验证,确保只有登录用户可以建立 WebSocket 连接,并在握手前后记录日志和处理相关信息。

3.3 握手处理器

AuthHandshakeHandler 类继承自 DefaultHandshakeHandler,主要用于在 WebSocket 握手过程中确定连接的用户身份。下面详细解释其代码逻辑:

  1. 类定义和注解
@Slf4j
public class AuthHandshakeHandler extends DefaultHandshakeHandler {
  • @Slf4j:Lombok 注解,用于自动生成日志记录器 log,方便在类中记录日志。
  • extends DefaultHandshakeHandler:继承 DefaultHandshakeHandler 类,该类是 Spring 提供的默认握手处理器,AuthHandshakeHandler 可以重写其方法来实现自定义的握手逻辑。
  1. 重写 determineUser 方法
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
    // case1: 根据cookie来识别用户,即可以实现所有用户连相同的ws地址,然后再 AuthHandshakeChannelInterceptor 中进行destination的转发
    ReqInfoContext.ReqInfo reqInfo = (ReqInfoContext.ReqInfo) attributes.get(LoginService.SESSION_KEY);
    if (reqInfo != null) {
        return reqInfo;
    }

    // case2: 根据路径来区分用户
    // 获取例如 ws://localhost/gpt/id 订阅地址中的最后一个用户 id 参数作为用户的标识, 为实现发送信息给指定用户做准备
    String uri = request.getURI().toString();
    String uid = uri.substring(uri.lastIndexOf("/") + 1);
    log.info("{} -> {}", uri, uid);
    return () -> uid;
}
  • determineUser 方法是 DefaultHandshakeHandler 中的一个受保护方法,用于在握手过程中确定连接的用户身份。重写该方法可以实现自定义的用户身份识别逻辑。
  • 身份识别方式一:通过 Cookie 识别
    • attributes 中获取 LoginService.SESSION_KEY 对应的值,将其转换为 ReqInfoContext.ReqInfo 对象。
    • 如果 reqInfo 不为空,则将其作为 Principal 返回,表示用户身份已通过 Cookie 识别。
  • 身份识别方式二:通过路径识别
    • 如果通过 Cookie 未能识别用户身份,则从请求的 URI 中提取最后一个 / 之后的部分作为用户 ID。
    • 使用 Lambda 表达式创建一个 Principal 对象,其 getName() 方法返回提取的用户 ID。
    • 记录请求 URI 和提取的用户 ID 的日志信息。

综上所述,AuthHandshakeHandler 类在 WebSocket 握手过程中,通过两种方式确定连接的用户身份,确保后续的消息可以准确地发送给指定用户。

3.4 消息发送与处理

WsAnswerHelper 是一个 Spring 组件,主要职责是借助 WebSocket 向用户实时推送聊天相关消息,在技术派项目里实现消息实时推送功能。下面对其代码逻辑展开详细说明:

3.4.1 消息发送方法

  1. sendMsgToUser(String session, String question)
public void sendMsgToUser(String session, String question) {
    ChatRecordsVo res = chatFacade.autoChat(question, vo -> response(session, vo));
    log.info("AI直接返回:{}", res);
}
  • 调用 chatFacade.autoChat 方法发起自动聊天,传入用户提问 question 以及一个回调函数。
  • 回调函数 vo -> response(session, vo) 会在获取到聊天结果后,调用 response 方法把结果推送给指定用户。
  • 记录 AI 返回结果的日志。
  1. sendMsgToUser(AISourceEnum ai, String session, String question)
public void sendMsgToUser(AISourceEnum ai, String session, String question) {
    if (ai == null) {
        // 自动选择AI类型
        sendMsgToUser(session, question);
    } else {
        ChatRecordsVo res = chatFacade.autoChat(ai, question, vo -> response(session, vo));
        log.info("AI直接返回:{}", res);
    }
}
  • 若传入的 ai 为 null,则调用 sendMsgToUser(session, question) 方法自动选择 AI 类型进行聊天。
  • 若 ai 不为 null,则调用 chatFacade.autoChat 方法,指定 AI 类型进行聊天,并将结果推送给用户,同时记录日志。
  1. sendMsgHistoryToUser(String session, AISourceEnum ai)
 public void sendMsgHistoryToUser(String session, AISourceEnum ai) {
     ChatRecordsVo vo = chatFacade.history(ai);
     response(session, vo);
 }
  • 调用 chatFacade.history 方法获取指定 AI 类型的聊天历史记录。
  • 调用 response 方法将聊天历史记录推送给指定用户。

3.4.2 消息推送方法

    /**
     * 将返回结果推送给用户
     *
     * @param session
     * @param response
     */
    public void response(String session, ChatRecordsVo response) {
        // convertAndSendToUser 方法可以发送信给给指定用户,
        // 底层会自动将第二个参数目的地址 /chat/rsp 拼接为
        // /user/username/chat/rsp,其中第二个参数 username 即为这里的第一个参数 session
        // username 也是AuthHandshakeHandler中配置的 Principal 用户识别标志
        simpMessagingTemplate.convertAndSendToUser(session, "/chat/rsp", response);
    }
  • 借助 SimpMessagingTemplate 的 convertAndSendToUser 方法,把聊天结果 response 推送给指定用户 session。
  • 目标地址 /chat/rsp 会自动拼接成 /user/username/chat/rsp,这里的 username 就是 session。

3.4.3 业务逻辑执行方法

    public void execute(Map<String, Object> attributes, Runnable func) {
        try {
            ReqInfoContext.ReqInfo reqInfo = (ReqInfoContext.ReqInfo) attributes.get(LoginService.SESSION_KEY);
            ReqInfoContext.addReqInfo(reqInfo);
            String traceId = (String) attributes.get(MdcUtil.TRACE_ID_KEY);
            MdcUtil.add(MdcUtil.TRACE_ID_KEY, traceId);


            // 执行具体的业务逻辑
            func.run();

        } finally {
            ReqInfoContext.clear();
            MdcUtil.clear();
        }
    }
  • attributes 里获取用户请求信息 reqInfo 和跟踪 ID traceId,并分别添加到 ReqInfoContextMdcUtil 中。
  • 执行传入的业务逻辑 func.run()
  • 无论业务逻辑是否执行成功,最后都要清除 ReqInfoContextMdcUtil 中的信息。

综上所述,WsAnswerHelper 类整合了聊天服务和 WebSocket 消息推送功能,实现了消息的实时发送和推送,同时对请求信息和跟踪 ID 进行管理。

3.5 前端页面集成

在前端页面 paicoding-ui/src/main/resources/templates/views/chat-home/index.html中,可以使用 JavaScript 代码来连接 WebSocket 并接收消息。

3.5.1 连接 WebSocket

<script th:inline="javascript">
    // ... 其他代码 ...
    const isLogin = [[${global.isLogin}]], user = [[${global.user}]];
    let stompClient = null;

    $(function () {
        if (isLogin) {
            initWs();
        } else {
            console.log("请先登录");
        }
    });

    function initWs() {
        let protocol = window.location.protocol.replace("http", "ws");
        let host = window.location.host;
        let aiType = $('#chat-type').val();
        const session = getCookie("f-session");
        let socket = new WebSocket(`${protocol}//${host}/gpt/${session}/${aiType}`);
        stompClient = Stomp.over(socket);

        stompClient.connect({}, function (frame) {
            console.log('ws连接成功: ' + frame);
            // 其他操作...
        });

        // 关闭链接
        socket.onclose = disconnect;
    }
    // ... 其他代码 ...
</script>
  • 登录状态判断:借助 Thymeleaf 模板引擎获取后端传递的登录状态 isLogin 和用户信息 user。页面加载完成后,若用户已登录,则调用 initWs方法来初始化 WebSocket 连接;反之,在控制台输出提示信息。
  • 构建 WebSocket 地址initWs 方法先把当前页面的协议从 http 替换成 ws,接着获取当前页面的主机名,再获取下拉框选中的 AI 类型 aiType 以及用户的会话 session,最后构建出完整的 WebSocket 连接地址。
  • 创建 WebSocket 连接:使用 new WebSocket 创建 WebSocket 连接,再用 Stomp.over 方法将其包装成基于 STOMP 协议的客户端。
  • 连接服务器:调用 stompClient.connect 方法连接服务器,连接成功后会执行回调函数,在控制台输出连接成功信息。
  • 处理关闭事件:为 WebSocket 的 onclose 事件绑定 disconnect 方法,当连接关闭时会触发该方法。

3.5.2 订阅消息

<script th:inline="javascript">
    // ... 其他代码 ...
    function initWs() {
        // ... 其他代码 ...
        stompClient.connect({}, function (frame) {
            console.log('ws连接成功: ' + frame);
            // 订阅消息
            stompClient.subscribe(`/user/chat/rsp`, function (message) {
                // 解析 JSON 字符串
                console.log("rsp:", message);
                let res = JSON.parse(message.body);
                console.log("res:", res);

                // 记录聊天次数
                $("#chatCnt").html(` ${res.usedCnt}/${res.maxCnt} `);

                const data = res.records;

                // 移除 loading 元素
                $(".home_chat-message-actions__loading").remove();

                if (data.length > 1) {
                    // 返回历史全部信息
                    chatContent.html('');
                    for (let i = data.length - 1; i >= 0; i--) {
                        if (data[i].question) {
                            addClientMsg(data[i].question, false);
                        }
                        if (i == 0) {
                            addSplit();
                        }
                        appendServerMessage(data[i]);
                    }
                } else {
                    appendServerMessage(data[0]);
                }

                // 除流式持续返回场景,恢复按钮状态
                if(data[data.length - 1].answerType != 'STREAM') {
                    sendBtn.removeAttr("disabled");
                }
            });
        });
        // ... 其他代码 ...
    }
    // ... 其他代码 ...
</script>
  • 订阅目的地:在 stompClient.connect 的回调函数里,调用 stompClient.subscribe 方法订阅 /user/chat/rsp 目的地,该目的地是服务器推送消息的目标地址。
  • 处理接收到的消息:当服务器向该目的地推送消息时,会触发回调函数。在回调函数中,先将接收到的消息体解析成 JSON 对象,然后更新页面上的聊天次数,移除加载中的提示元素,根据消息数据的长度不同,采用不同的方式将消息添加到聊天界面,最后根据消息类型恢复发送按钮的状态。

3.5.3 发送消息

<script th:inline="javascript">
    // ... 其他代码 ...
    function doSend() {
        const qa = inputField.val();
        if (qa.length > 512) {
            toastr.info("提问长度请不要超过512字符哦~");
            return;
        }
        stompClient.send("/app/chat/" + session, {'s-uid': session}, qa);
        inputField.val("");
        addClientMsg(qa.replace(/\n/g, "<br/>"), true);
        sendBtn.attr("disabled", true);
    }

    sendBtn.click(function () {
        if (stompClient == null) {
            initWs();
        } else {
            if (inputField.val() === "") {
                inputField.focus();
            } else {
                doSend();
            }
        }
    });

    inputField.keydown(function (e) {
        if (e.keyCode === 13) {
            sendBtn.click();
            e.preventDefault();
        }
    });
    // ... 其他代码 ...
</script>
  • doSend 方法:获取输入框中的内容,若内容长度超过 512 字符,则给出提示并返回;否则,调用 stompClient.send 方法将消息发送到 /app/chat/ 加上会话 ID 的地址,同时传递自定义头信息 {'s-uid': session}。发送消息后,清空输入框,在聊天界面添加用户消息,并禁用发送按钮。
  • 按钮点击事件:为发送按钮绑定点击事件,若 stompClient 为空,则调用 initWs 方法初始化连接;否则,若输入框内容为空,则将焦点聚焦到输入框,反之调用 doSend 方法发送消息。
  • 回车键事件:为输入框绑定按键事件,当按下回车键时,触发发送按钮的点击事件,并阻止回车键的默认行为。

综上所述,前端代码通过上述步骤实现了 WebSocket 的连接、消息的订阅和发送功能,从而与服务器进行实时通信。

4 小结

本文聚焦WebSocket的应用,涵盖其基础概念、Spring整合STOMP的流程图、Spring Boot应用集成WebSocket实现双向通信,以及技术派中基于WebSocket实现消息推送的全过程。

5 参考链接

  1. 技术派整合WebSocket实现消息实时推送
  2. 项目仓库(GitHub)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值