(十一)从“轮询卡顿”到“实时推送”——WebSocket实战进阶指南

引言:实时场景的"痛点",被WebSocket彻底解决

搞定接口文档标准化后,前后端联调效率大幅提升,但在开发实时交互场景时,我又遇到了新的瓶颈------传统的HTTP请求无法实现"服务器主动向客户端推送数据",只能靠客户端反复轮询(每隔几秒请求一次接口),踩了无数坑:

  • 轮询效率极低:客户端每隔1秒请求一次接口,大部分请求都是"无效请求"(服务器数据未更新),浪费服务器资源和带宽,高并发场景下甚至会导致服务器卡顿;

  • 实时性差:轮询间隔再短,也会有延迟(比如间隔1秒,最大延迟就是1秒),无法满足聊天、实时通知、订单状态推送等高频实时场景;

  • 客户端负担重:频繁的请求会消耗客户端资源,移动端场景下会导致耗电快、流量消耗大,用户体验极差;

  • 代码冗余复杂:需要单独编写轮询逻辑、处理请求频率、控制重试机制,后期维护成本高,新手容易写出bug。

印象最深的一次,我开发一个"在线聊天"功能,一开始用HTTP轮询实现:前端每隔1秒请求接口获取新消息,测试时发现,两个人聊天时会有明显延迟,消息发送后要等1秒才能看到,而且服务器压力极大,10个用户同时聊天,服务器每秒就要处理10次请求,再加上其他接口,直接导致服务器响应变慢。

后来,部门李哥告诉我:"实时交互场景,别用轮询,WebSocket才是最优解------它能建立客户端和服务器之间的长连接,实现双向实时通信,服务器能主动向客户端推送数据,无需客户端反复请求,实时性、效率都能提升一个档次。"

从那以后,我开始系统学习WebSocket的核心原理、实战用法,从最基础的概念、协议细节,到Spring Boot整合WebSocket、实现聊天/通知等场景,再到生产环境的高并发优化、故障排查,一步步摆脱了"轮询卡顿"的困境,慢慢学会了用WebSocket赋能实时交互场景。

今天,就把这段从"被轮询折磨"到"用WebSocket掌控实时交互"的成长历程,分享给和曾经的我一样,被实时场景困扰、想提升系统实时性的Java新手开发者。

注:本文聚焦Java后端WebSocket实战,从入门到精通,结合实际开发场景(在线聊天、实时通知、订单推送),全程用真实案例串联知识点,不冗余讲解理论,重点覆盖"基础原理+Spring Boot实战+多场景适配+生产优化+故障排查",所有代码和示例可直接复制使用,让你看完就能上手,真正用WebSocket解决实时交互痛点。


第一章:入门破局------吃透WebSocket基础,告别轮询卡顿

李哥告诉我:"WebSocket的核心,是'长连接+双向通信'------它打破了HTTP协议'请求-响应'的单向限制,客户端和服务器建立一次连接后,就能持续通信,服务器能主动向客户端推送数据,客户端也能随时向服务器发送请求,这也是它比轮询高效的核心原因。"

对于Java新手来说,WebSocket的进阶第一步,就是吃透以下基础知识点,快速摆脱轮询卡顿的困扰。

一、先搞懂:WebSocket到底是什么?(一看就懂)

很多新手觉得"WebSocket是一种新的HTTP协议",这是一个很大的误区。一句话讲明白WebSocket的定位:WebSocket是一种独立的应用层协议,基于TCP协议实现,专门用于解决"实时双向通信"场景,它能在客户端和服务器之间建立持久化的长连接,实现服务器主动向客户端推送数据,无需客户端反复发起请求。

举个通俗的例子:HTTP请求就像"打电话",客户端打给服务器(请求),服务器接电话(响应),通话结束(连接断开),下次再联系需要重新打电话;而WebSocket就像"视频通话",客户端和服务器建立一次连接(拨通视频),之后双方可以随时说话(双向通信),连接一直保持,直到主动挂断(关闭连接)。

补充:WebSocket的3个核心价值------

  1. 实时性强:服务器数据更新后,能立即推送给客户端,无轮询延迟,适合聊天、实时通知等场景;

  2. 效率高:建立一次长连接,无需反复发起请求,减少无效请求,节省服务器资源和带宽;

  3. 双向通信:客户端可向服务器发送请求,服务器也可主动向客户端推送数据,适配复杂实时交互场景。

现在,几乎所有实时交互场景(在线聊天、实时监控、订单状态推送、弹幕、股票行情),都在使用WebSocket,它已经成为Java后端开发必备的技能之一。

二、WebSocket vs HTTP(新手必懂,避免混淆)

新手最容易混淆WebSocket和HTTP,两者都是应用层协议,基于TCP协议,但定位和用法完全不同,一张表格讲清区别,新手可直接对照记忆:

对比维度 HTTP WebSocket
通信方式 单向通信(客户端请求,服务器响应) 双向通信(客户端↔服务器,可互相发送数据)
连接类型 短连接(请求响应后,连接立即断开) 长连接(建立后持续保持,直到主动关闭)
实时性 差(依赖轮询,有延迟) 强(服务器主动推送,无延迟)
资源消耗 高(频繁发起请求,无效请求多) 低(一次连接,持续通信)
适用场景 普通接口请求(查询、新增、修改、删除) 实时交互场景(聊天、通知、监控)
协议标识 URL以http://、https://开头 URL以ws://、wss://开头(wss是加密版)

⚠️ 重要提醒:WebSocket不是替代HTTP,而是补充HTTP------普通接口请求(如查询用户信息、提交订单)依然用HTTP,实时交互场景(如订单推送、聊天)用WebSocket,两者结合使用,才能适配所有业务场景。

三、WebSocket核心原理(简化讲解,新手能懂)

WebSocket的原理不复杂,核心是"握手建立连接→双向通信→关闭连接"三个步骤,新手无需深入研究底层TCP协议,重点掌握这三个步骤,就能理解WebSocket的工作流程:

  1. 握手建立连接(核心步骤)
  • 客户端发起WebSocket连接请求(URL以ws://开头),请求头中会携带"Upgrade: websocket"(表示要升级为WebSocket协议);

  • 服务器收到请求后,确认协议升级,返回响应头"Upgrade: websocket",表示同意建立WebSocket连接;

  • 握手成功后,HTTP连接升级为WebSocket长连接,后续客户端和服务器之间的通信,不再使用HTTP协议,而是使用WebSocket协议。

  1. 双向通信
  • 连接建立后,客户端和服务器可随时向对方发送数据(数据格式通常为JSON);

  • 服务器无需等待客户端请求,只要有数据更新,就能主动推送给客户端(这是WebSocket最核心的优势);

  • 通信过程中,连接一直保持,除非客户端主动关闭连接,或服务器主动断开连接。

  1. 关闭连接
  • 客户端可主动关闭连接(如用户退出页面、关闭浏览器);

  • 服务器可主动关闭连接(如连接超时、服务器重启);

  • 关闭连接时,双方会发送关闭帧,确认连接关闭,避免资源泄露。

补充:WebSocket连接建立后,会保持心跳(默认每隔一段时间发送一次心跳包),用于检测连接是否正常,若心跳中断,说明连接异常,客户端会自动重新建立连接。

四、WebSocket核心概念(新手必记)

学习WebSocket实战前,需掌握3个核心概念,后续实战中会频繁用到,新手可直接记牢:

  1. Session(会话):客户端和服务器建立WebSocket连接后,会创建一个唯一的Session对象,代表当前连接,服务器可通过Session对象向指定客户端推送数据;

  2. 消息推送方式

  • 点对点推送:服务器向单个客户端推送数据(如一对一聊天、个人通知);

  • 广播推送:服务器向所有连接的客户端推送数据(如公告、弹幕、全局通知);

  • 分组推送:服务器向指定分组的客户端推送数据(如群聊、部门通知)。

  1. 消息格式:WebSocket支持文本消息(String)和二进制消息(byte[]),实际开发中,最常用的是文本消息,格式为JSON(便于前后端解析)。

五、实战:WebSocket基础入门(原生Java实现,新手必练)

新手入门,建议先从原生Java实现WebSocket开始,熟悉WebSocket的核心API和工作流程,后续再学习Spring Boot整合(更简洁、更常用)。以下是原生Java实现WebSocket的完整步骤,可直接复制代码使用。

1. 导入依赖(pom.xml)

原生Java WebSocket依赖Java EE的javax.websocket包,Spring Boot项目中可直接导入以下依赖:

xml 复制代码
<!-- 原生WebSocket依赖(Java EE) -->
<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>

<!-- Spring Web依赖(若项目未导入) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2. 编写WebSocket服务端(核心)

创建WebSocket服务端类,继承Endpoint,重写连接建立、消息接收、连接关闭、异常处理的方法,这是WebSocket服务端的核心:

java 复制代码
package com.example.websocketdemo.ws;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 原生Java WebSocket服务端
 * ServerEndpoint:指定WebSocket的访问路径(客户端通过该路径连接)
 */
@ServerEndpoint("/ws/native")
public class NativeWebSocketServer {

    // 存储所有连接的Session(key:Session ID,value:Session对象)
    // ConcurrentHashMap:线程安全,适合高并发场景
    private static final ConcurrentHashMap<String, Session> SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 连接建立成功时触发(客户端连接服务器后,自动调用)
     * @param session 当前连接的Session对象
     */
    @OnOpen
    public void onOpen(Session session) {
        // 将当前Session加入集合,便于后续推送数据
        SESSION_MAP.put(session.getId(), session);
        System.out.println("WebSocket连接建立成功!Session ID:" + session.getId());
        // 连接成功后,向客户端推送欢迎消息
        sendMessage(session, "欢迎连接WebSocket服务端,当前在线人数:" + SESSION_MAP.size());
    }

    /**
     * 接收客户端发送的消息时触发
     * @param session 当前连接的Session
     * @param message 客户端发送的消息
     */
    @OnMessage
    public void onMessage(Session session, String message) {
        System.out.println("收到客户端[" + session.getId() + "]的消息:" + message);
        // 示例1:点对点推送(向发送消息的客户端回复消息)
        sendMessage(session, "服务端已收到你的消息:" + message);

        // 示例2:广播推送(向所有连接的客户端推送消息)
        // broadcastMessage("客户端[" + session.getId() + "]发送消息:" + message);
    }

    /**
     * 连接关闭时触发(客户端关闭连接、浏览器关闭,自动调用)
     * @param session 当前连接的Session
     */
    @OnClose
    public void onClose(Session session) {
        // 将当前Session从集合中移除,释放资源
        SESSION_MAP.remove(session.getId());
        System.out.println("WebSocket连接关闭!Session ID:" + session.getId() + ",当前在线人数:" + SESSION_MAP.size());
        // 广播通知所有客户端,该用户已下线
        broadcastMessage("客户端[" + session.getId() + "]已下线,当前在线人数:" + SESSION_MAP.size());
    }

    /**
     * 连接异常时触发(如网络中断、服务器异常)
     * @param session 当前连接的Session
     * @param throwable 异常信息
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.out.println("WebSocket连接异常!Session ID:" + session.getId());
        throwable.printStackTrace();
        // 异常时,关闭连接并移除Session
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        SESSION_MAP.remove(session.getId());
    }

    /**
     * 点对点推送消息(向指定客户端发送消息)
     * @param session 目标客户端的Session
     * @param message 要发送的消息
     */
    private void sendMessage(Session session, String message) {
        try {
            // 判断Session是否打开(连接是否正常)
            if (session.isOpen()) {
                // 发送文本消息
                session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 广播推送消息(向所有连接的客户端发送消息)
     * @param message 要发送的消息
     */
    private void broadcastMessage(String message) {
        // 遍历所有Session,向每个客户端推送消息
        for (Session session : SESSION_MAP.values()) {
            sendMessage(session, message);
        }
    }
}

3. 编写WebSocket客户端(HTML+JS,用于测试)

创建一个HTML页面,使用JavaScript原生WebSocket API连接服务端,发送消息、接收消息,用于测试WebSocket服务端是否正常工作:

html 复制代码
<!DOCTYPE html>
原生WebSocket客户端原生WebSocket客户端测试

4. 测试验证(核心步骤)

  1. 启动Spring Boot项目,确保项目无报错;

  2. 将上述HTML页面保存为"index.html",放在项目的"src/main/resources/static"目录下(Spring Boot默认静态资源目录);

  3. 打开浏览器,访问地址:http://localhost:8080/index.html,此时会自动建立WebSocket连接;

  4. 在输入框中输入消息(如"Hello WebSocket"),点击"发送消息",可看到服务端回复的消息,控制台也会打印对应日志;

  5. 打开多个浏览器窗口(或多个标签页),访问同一个地址,发送消息,可测试广播推送(取消服务端onMessage方法中广播推送的注释);

  6. 点击"关闭连接",或关闭浏览器窗口,可看到服务端打印连接关闭日志,其他客户端会收到"用户下线"的广播通知。

5. 原生实现注意事项(新手避坑)

  • 服务端@ServerEndpoint注解的路径,必须和客户端WebSocket连接的URL一致,否则无法建立连接;

  • 存储Session的集合,必须使用线程安全的集合(如ConcurrentHashMap),因为WebSocket是多线程的,多个客户端连接会触发多个线程;

  • 发送消息前,必须判断Session是否打开(session.isOpen()),避免连接已关闭时发送消息,导致异常;

  • 异常处理必须完善,连接异常时要关闭Session、移除Session,避免资源泄露;

  • 原生WebSocket代码繁琐,适合学习原理,实际开发中,优先使用Spring Boot整合WebSocket(更简洁、更易维护)。


第二章:实战进阶------Spring Boot整合WebSocket(实际开发首选)

掌握了原生Java WebSocket的实现后,我发现原生代码过于繁琐------需要手动管理Session、处理线程安全、编写推送逻辑,实际开发中效率太低。李哥告诉我:"Spring Boot提供了对WebSocket的自动配置,整合后代码更简洁,无需手动管理Session,还支持更多高级功能(如分组推送、消息拦截),是实际开发的首选。"

以下是Spring Boot 3.2.x整合WebSocket的完整实战步骤,涵盖基础配置、核心代码、多场景推送、测试验证,可直接复制到项目中使用,贴合实际开发场景。

一、Spring Boot整合WebSocket(基础配置)

1. 导入Maven依赖(pom.xml)

Spring Boot提供了专门的WebSocket依赖,无需导入原生Java WebSocket依赖,直接导入以下依赖即可:

xml 复制代码
<!-- Spring Boot WebSocket依赖(Spring Boot 3.2.x 适配) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- Spring Web依赖(若项目未导入) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Lombok简化代码(可选) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- fastjson(用于JSON格式转换,可选) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>

2. 编写WebSocket配置类(核心)

创建WebSocket配置类,开启WebSocket支持,注册WebSocket服务端,配置消息拦截器(可选),这是Spring Boot整合WebSocket的核心配置:

java 复制代码
package com.example.websocketdemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import com.example.websocketdemo.ws.SpringWebSocketHandler;
import com.example.websocketdemo.ws.WebSocketInterceptor;

/**
 * Spring Boot WebSocket配置类
 * EnableWebSocket:开启WebSocket支持
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    /**
     * 注册WebSocket服务端
     * @param registry WebSocket处理器注册器
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 1. 注册WebSocket处理器(核心业务逻辑)
        // 2. 指定WebSocket访问路径(/ws/spring)
        // 3. addInterceptors:添加消息拦截器(可选,用于权限校验、参数解析)
        // 4. setAllowedOrigins("*"):允许所有跨域请求(开发环境,生产环境需指定具体域名)
        registry.addHandler(springWebSocketHandler(), "/ws/spring")
                .addInterceptors(webSocketInterceptor())
                .setAllowedOrigins("*");
    }

    /**
     * 注入WebSocket处理器(核心业务逻辑)
     */
    @Bean
    public SpringWebSocketHandler springWebSocketHandler() {
        return new SpringWebSocketHandler();
    }

    /**
     * 注入WebSocket消息拦截器(可选)
     */
    @Bean
    public WebSocketInterceptor webSocketInterceptor() {
        return new WebSocketInterceptor();
    }
}

3. 编写WebSocket消息拦截器(可选,权限校验)

实际开发中,WebSocket连接需要权限校验(如用户登录后才能连接),可通过消息拦截器实现,在连接建立前校验用户身份,避免未授权用户连接:

java 复制代码
package com.example.websocketdemo.ws;

import jakarta.servlet.http.HttpSession;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

/**
 * WebSocket消息拦截器(用于权限校验、参数解析)
 */
public class WebSocketInterceptor implements HandshakeInterceptor {

    /**
     * 握手前触发(连接建立前,用于权限校验)
     * @param request 客户端请求
     * @param response 服务器响应
     * @param wsHandler WebSocket处理器
     * @param attributes 用于存储参数,可传递到WebSocket处理器
     * @return true:允许握手(连接建立);false:拒绝握手(连接失败)
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        // 1. 解析客户端请求中的参数(如用户ID、Token)
        // 示例:从请求参数中获取用户ID(客户端连接时携带参数:ws://localhost:8080/ws/spring?userId=1)
        ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
        String userId = servletRequest.getServletRequest().getParameter("userId");
        String token = servletRequest.getServletRequest().getParameter("token");

        // 2. 权限校验(实际开发中,需校验Token是否有效、用户是否存在)
        if (userId == null || token == null || token.isEmpty()) {
            // 未携带用户ID或Token,拒绝连接
            System.out.println("WebSocket连接失败:未授权,缺少用户ID或Token");
            return false;
        }

        // 3. 校验Token有效性(模拟逻辑,实际开发中替换为真实Token校验)
        if (!"test_token_123".equals(token)) {
            System.out.println("WebSocket连接失败:Token无效");
            return false;
        }

        // 4. 将用户ID存入attributes,便于后续WebSocket处理器使用(点对点推送时需要)
        attributes.put("userId", userId);
        System.out.println("WebSocket握手成功,用户ID:" + userId);
        return true;
    }

    /**
     * 握手后触发(连接建立后,可做一些后续处理)
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        // 无需处理,可留空
    }
}

4. 编写WebSocket处理器(核心业务逻辑)

Spring Boot通过WebSocketHandler处理WebSocket的核心业务逻辑(连接建立、消息接收、连接关闭、异常处理),无需手动管理Session,Spring会自动维护Session,代码更简洁:

java 复制代码
package com.example.websocketdemo.ws;

import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Spring Boot WebSocket处理器(核心业务逻辑)
 * 继承TextWebSocketHandler,专注处理文本消息(最常用)
 */
@Component
@Slf4j // 日志注解(Lombok)
public class SpringWebSocketHandler extends TextWebSocketHandler {

    // 存储所有连接的Session(key:用户ID,value:WebSocketSession)
    // 实际开发中,用户ID唯一,便于点对点推送(比用Session ID更直观)
    private static final ConcurrentHashMap<String, WebSocketSession> USER_SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 连接建立成功时触发
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 从拦截器传递的attributes中获取用户ID
        String userId = (String) session.getAttributes().get("userId");
        if (userId != null && !userId.isEmpty()) {
            // 将用户ID和Session关联,存入集合
            USER_SESSION_MAP.put(userId, session);
            log.info("WebSocket连接建立成功!用户ID:{},当前在线人数:{}", userId, USER_SESSION_MAP.size());
            // 向当前用户推送欢迎消息(JSON格式,实际开发中常用)
            JSONObject welcomeMsg = new JSONObject();
            welcomeMsg.put("type", "welcome");
            welcomeMsg.put("content", "欢迎连接WebSocket服务端,当前在线人数:" + USER_SESSION_MAP.size());
            sendMessageToUser(userId, new TextMessage(welcomeMsg.toString()));
        } else {
            // 未获取到用户ID,关闭连接
            session.close(CloseStatus.POLICY_VIOLATION.withReason("未获取到用户ID"));
            log.error("WebSocket连接失败:未获取到用户ID");
        }
    }

    /**
     * 接收客户端发送的文本消息时触发
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 获取当前用户ID
        String userId = (String) session.getAttributes().get("userId");
        // 解析客户端发送的消息(JSON格式)
        String messageContent = message.getPayload();
        JSONObject msgJson = JSONObject.parseObject(messageContent);
        String msgType = msgJson.getString("type"); // 消息类型(如chat-聊天、notice-通知)
        String content = msgJson.getString("content"); // 消息内容
        String targetUserId = msgJson.getString("targetUserId"); // 目标用户ID(点对点推送时使用)

        log.info("收到用户[{}]的消息:类型={},内容={},目标用户ID={}", userId, msgType, content, targetUserId);

        // 根据消息类型,处理不同的业务逻辑
        switch (msgType) {
            case "chat":
                // 1. 点对点聊天(向目标用户推送消息)
                if (targetUserId != null && USER_SESSION_MAP.containsKey(targetUserId)) {
                    JSONObject chatMsg = new JSONObject();
                    chatMsg.put("type", "chat");
                    chatMsg.put("fromUserId", userId);
                    chatMsg.put("content", content);
                    chatMsg.put("time", System.currentTimeMillis());
                    // 向目标用户推送消息
                    sendMessageToUser(targetUserId, new TextMessage(chatMsg.toString()));
                    // 向发送方回复"发送成功"
                    sendMessageToUser(userId, new TextMessage("{\"type\":\"reply\",\"content\":\"消息发送成功\"}"));
                } else {
                    // 目标用户不在线或不存在
                    sendMessageToUser(userId, new TextMessage("{\"type\":\"error\",\"content\":\"目标用户不在线或不存在\"}"));
                }
                break;
            case "broadcast":
                // 2. 广播消息(向所有在线用户推送消息)
                JSONObject broadcastMsg = new JSONObject();
                broadcastMsg.put("type", "broadcast");
                broadcastMsg.put("fromUserId", userId);
                broadcastMsg.put("content", content);
                broadcastMsg.put("time", System.currentTimeMillis());
                broadcastMessage(new TextMessage(broadcastMsg.toString()));
                break;
            default:
                // 未知消息类型,回复错误
                sendMessageToUser(userId, new TextMessage("{\"type\":\"error\",\"content\":\"未知消息类型\"}"));
                break;
        }
    }

    /**
     * 连接关闭时触发
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 获取当前用户ID
        String userId = (String) session.getAttributes().get("userId");
        if (userId != null && USER_SESSION_MAP.containsKey(userId)) {
            // 从集合中移除Session,释放资源
            USER_SESSION_MAP.remove(userId);
            log.info("WebSocket连接关闭!用户ID:{},当前在线人数:{}", userId, USER_SESSION_MAP.size());
            // 广播通知所有用户,该用户已下线
            JSONObject offlineMsg = new JSONObject();
            offlineMsg.put("type", "offline");
            offlineMsg.put("content", "用户[" + userId + "]已下线,当前在线人数:" + USER_SESSION_MAP.size());
            broadcastMessage(new TextMessage(offlineMsg.toString()));
        }
    }

    /**
     * 连接异常时触发
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 获取当前用户ID
        String userId = (String) session.getAttributes().get("userId");
        log.error("WebSocket连接异常!用户ID:{}", userId, exception);
        // 异常时,关闭连接并移除Session
        if (session.isOpen()) {
            session.close(CloseStatus.SERVER_ERROR.withReason("连接异常"));
        }
        if (userId != null && USER_SESSION_MAP.containsKey(userId)) {
            USER_SESSION_MAP.remove(userId);
        }
    }

    /**
     * 点对点推送消息(向指定用户发送消息)
     * @param userId 目标用户ID
     * @param message 要发送的消息(TextMessage:文本消息)
     */
    public void sendMessageToUser(String userId, TextMessage message) {
        try {
            // 获取目标用户的Session
            WebSocketSession session = USER_SESSION_MAP.get(userId);
            // 判断Session是否打开(连接是否正常)
            if (session != null && session.isOpen()) {
                session.sendMessage(message);
            } else {
                log.warn("向用户[{}]推送消息失败:用户不在线或连接已关闭", userId);
            }
        } catch (IOException e) {
            log.error("向用户[{}]推送消息异常", userId, e);
        }
    }

    /**
     * 广播推送消息(向所有在线用户发送消息)
     * @param message 要发送的消息
     */
    public void broadcastMessage(TextMessage message) {
        try {
            // 遍历所有在线用户的Session,推送消息
            for (WebSocketSession session : USER_SESSION_MAP.values()) {
                if (session.isOpen()) {
                    session.sendMessage(message);
                }
            }
        } catch (IOException e) {
            log.error("广播消息异常", e);
        }
    }

    /**
     * 分组推送消息(向指定分组的用户发送消息,示例:按部门分组)
     * @param groupId 分组ID(如部门ID)
     * @param message 要发送的消息
     * @param userIds 分组内的用户ID列表
     */
    public void sendMessageToGroup(String groupId, TextMessage message, String... userIds) {
        try {
            for (String userId : userIds) {
                WebSocketSession session = USER_SESSION_MAP.get(userId);
                if (session != null && session.isOpen()) {
                    session.sendMessage(message);
                    log.info("向分组[{}]的用户[{}]推送消息成功", groupId, userId);
                }
            }
        } catch (IOException e) {
            log.error("向分组[{}]推送消息异常", groupId, e);
        }
    }
}

5. 编写WebSocket客户端(HTML+JS,适配Spring Boot)

修改客户端代码,适配Spring Boot的WebSocket连接(携带用户ID和Token,用于权限校验),支持点对点聊天、广播消息,贴合实际开发场景:

html 复制代码
<!DOCTYPE html>
Spring Boot WebSocket客户端当前用户ID:Token:

6. 测试验证(实战场景模拟)

完成上述配置和代码编写后,启动Spring Boot项目,进行多场景测试,确保WebSocket功能正常,贴合实际开发场景:

6.1 权限校验测试

  1. 打开浏览器,访问http://localhost:8080/index.html;

  2. 不输入用户ID或Token,点击"建立连接",会提示"请输入用户ID和Token";

  3. 输入用户ID=1,Token=错误值(如123),点击"建立连接",控制台会打印"Token无效",客户端会提示"连接异常";

  4. 输入用户ID=1,Token=test_token_123,点击"建立连接",连接成功,客户端会收到欢迎消息。

6.2 点对点聊天测试

  1. 打开两个浏览器窗口(或标签页),访问同一个地址;

  2. 窗口1:用户ID=1,Token=test_token_123,建立连接;

  3. 窗口2:用户ID=2,Token=test_token_123,建立连接;

  4. 窗口1中,目标用户ID输入2,消息输入"你好,用户2",点击"点对点发送";

  5. 窗口2会收到用户1发送的消息,窗口1会收到"消息发送成功"的回复,控制台会打印对应日志。

6.3 广播消息测试

  1. 打开3个浏览器窗口,分别以用户ID=1、2、3建立连接;

  2. 用户1输入消息"大家好,这是广播消息",点击"广播发送";

  3. 所有窗口(用户1、2、3)都会收到这条广播消息,控制台会打印广播日志。

6.4 连接关闭测试

  1. 用户1建立连接后,点击"关闭连接",或关闭浏览器窗口;

  2. 其他用户(2、3)会收到"用户[1]已下线"的系统通知;

  3. 控制台会打印"用户1连接关闭"的日志,在线人数会减少1。

7. Spring Boot整合注意事项(新手避坑)

  • 配置类必须添加@EnableWebSocket注解,否则无法开启WebSocket支持;

  • WebSocketHandler必须注入Spring容器(添加@Component注解),否则配置类无法获取到;

  • 跨域配置:开发环境可设置setAllowedOrigins("*"),生产环境需指定具体域名(如setAllowedOrigins("https://xxx.com")),避免跨域问题;需注意域名拼写规范,避免出现多余符号(如引号残留),否则可能导致网页解析失败;

  • Session存储:实际开发中,建议用用户ID作为key存储Session(比Session ID更直观),便于点对点推送;同时需确保Session操作线程安全,优先使用ConcurrentHashMap,避免多线程并发修改导致的异常;

  • 消息格式:优先使用JSON格式,便于前后端解析,可通过fastjson或Jackson进行JSON转换;避免使用复杂二进制格式,降低前后端联调成本;

  • 连接异常处理:必须完善handleTransportError方法,连接异常时及时关闭Session、移除Session,避免资源泄露;同时可添加重连机制,提升客户端体验;

  • 权限校验:拦截器中需严格校验用户身份(如Token有效性、用户合法性),避免未授权用户建立WebSocket连接,造成服务器资源浪费;

  • 访问路径规范:WebSocket访问路径(如/ws/spring)需与客户端连接URL严格一致,避免URL拼写错误(如多写、少写字符),否则会出现"URL拼写可能存在错误,请检查"的报错;

  • 依赖版本适配:Spring Boot 3.x需搭配2.0+版本的spring-boot-starter-websocket依赖,版本不匹配会导致项目启动失败或WebSocket功能异常;

  • 客户端资源部署:HTML客户端页面需放在Spring Boot默认静态资源目录(src/main/resources/static),否则访问时可能出现网页解析失败、不支持的网页类型等报错;

  • 心跳机制配置:生产环境需手动配置WebSocket心跳机制,避免因网络波动、连接超时导致的长连接断开,可通过定时发送心跳包检测连接状态;

  • 日志打印:关键业务节点(连接建立、消息发送、异常触发)需添加详细日志,便于后续故障排查,建议使用@Slf4j注解简化日志编写;

  • 生产环境优化:高并发场景下,可引入WebSocket集群部署,配合Redis实现Session共享,避免单节点压力过大,同时防止用户连接丢失。

相关推荐
不做菜鸟的网工2 小时前
H3C防火墙 SNMP跨三层获取MAC地址
网络协议
HoldBelief2 小时前
MCP中streamable-http与sse协议的区别
网络·网络协议·http
kkkkatoq3 小时前
http相关整理
网络·网络协议·http
阿豪学编程4 小时前
【网络】应用层-HTTP协议
网络·网络协议·http
一直都在5726 小时前
Socket和 WebSocket核心区别
网络·websocket·网络协议
北京耐用通信15 小时前
协议融合的工业钥匙:耐达讯自动化网关如何打通CC-Link IE转DeviceNet的通信壁垒
人工智能·物联网·网络协议·自动化·信息与通信
IP搭子来一个17 小时前
静态独享IP是什么?在数据采集任务中有哪些作用?
网络·网络协议·tcp/ip
皙然19 小时前
Socket 与 WebSocket 深度解析
网络·websocket·网络协议
小涛不学习1 天前
WebSocket 技术详解(原理 + 使用 + 面试总结)
websocket·网络协议·面试