告别轮询!WebSocket 实时进度推送的实现与问题总结

在日常后端开发中,实时通信场景(如消息推送、任务进度同步、系统实时监控等)愈发普遍。但传统 HTTP 协议基于 "请求 - 响应" 的单向通信模式,无法实现服务端主动向客户端推送数据 ------ 若要模拟实时效果,只能通过 "轮询" 方式让客户端反复调用后端接口。可轮询方案存在天然矛盾:轮询间隔设得过长,会导致数据更新延迟高、用户体验差;间隔设得过短,又会引发大量无效请求,加剧服务器的连接与资源压力,最终难以满足这类场景对 "低延迟""高实时性" 的核心需求。WebSocket 技术出现解决了这一痛点,今天来总结下 WebSocket 写法。

一:概述

WebSocket 能在客户端与服务端之间建立一条持久化的双向通信通道,两端无需反复建立连接,可随时向对方发送数据。这种全双工通信模式彻底突破了 HTTP 单向通信的瓶颈,成为适配实时场景的关键解决方案。

二:使用场景

1:状态实时同步场景,如导入、导出大文件显示进度百分比,WebSocket 让服务端主动将状态变化推送给客户端,避免无效的高频查询。

2:监控数据高频更新,如实时成交金额、订单量动态更新,运维监控各种指标,WebSocket 可通过定时推送减少带宽浪费,比轮训更高效。

3:即时通信,如网页版的聊天、在线客服、多人在线游戏,WebSocket 可通过单连接双向传输实现发送消息实时到达。

三:代码实现

1:引入包
复制代码
<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2:任务进度类
复制代码
@Data
public class TaskProgressInfo {

    /**
     * 任务状态消息
     */
    private String message;

    /**
     * 进度百分比
     */
    private int progress;

    /**
     * 是否完成
     */
    private boolean completed;

    public TaskProgressInfo(String message, int progress, boolean completed) {
        this.message = message;
        this.progress = progress;
        this.completed = completed;
    }
    
}
3:任务处理类(主要代码)
复制代码
@Slf4j
public class AsyncTaskWebSocketHandler extends TextWebSocketHandler {

    // 线程安全的会话存储(key:会话ID,value:会话对象)
    private final ConcurrentMap<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
    private final ExecutorService executor = Executors.newFixedThreadPool(5); // 建议用线程池工厂
    private final ObjectMapper objectMapper = new ObjectMapper();


    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessionMap.put(session.getId(), session);
        sendMessage(session.getId(), "连接成功,可发送 '任意字符' 启动任务,实时返回任务进度", 0, false);
    }


    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("输入任务为:{}", payload);
        startAsyncTask(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 连接关闭时移除会话
        sessionMap.remove(session.getId());
    }

    /**
     * 开始一个异步任务,并实时推送进度
     */
    private void startAsyncTask(WebSocketSession session) {
        String sessionId = session.getId(); // 获取会话ID
        executor.submit(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    Thread.sleep(1000);
                    // 传递会话ID给sendMessage,动态获取会话
                    sendMessage(sessionId, "任务执行中... 第" + i + "步", i * 10, false);
                }
                // 任务完成
                sendMessage(sessionId, "任务已完成", 100, true);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
                sendMessage(sessionId, "任务被中断: " + e.getMessage(), 0, true);
            } catch (Exception e) {
                sendMessage(sessionId, "任务执行失败: " + e.getMessage(), 0, true);
            }
        });
    }

    /**
     * 向客户端发送消息
     *
     * @param sessionId
     * @param message
     * @param progress
     * @param completed
     */
    private void sendMessage(String sessionId, String message, int progress, boolean completed) {
        // 1. 从线程安全容器中获取会话(避免直接引用可能失效的session)
        WebSocketSession session = sessionMap.get(sessionId);
        if (session == null || !session.isOpen()) {
            log.info("会话不存在或已关闭,无法发送消息");
            return;
        }

        try {
            // 2. 构建消息
            TaskProgressInfo taskProgress = new TaskProgressInfo(message, progress, completed);
            String json = objectMapper.writeValueAsString(taskProgress);
            TextMessage textMessage = new TextMessage(json);

            // 3. 发送消息(用同步发送,确保消息顺序;WebSocketSession默认支持异步发送,但需处理回调)
            // 同步锁,避免多线程并发操作会话
            synchronized (session) {
                // 二次校验,减少发送失败概率
                if (session.isOpen()) {
                    session.sendMessage(textMessage);
                    log.info("向会话 {} 推送消息:{}", sessionId, json);
                }
            }
        } catch (IOException e) {
            log.error("向会话 {} 发送消息失败", sessionId, e);
            // 发送失败时,移除无效会话
            sessionMap.remove(sessionId);
        }
    }
}
4:注册 WebSocket 处理器
复制代码
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册 WebSocket 处理器,映射到"/async-task"路径,允许跨域
        registry.addHandler(new AsyncTaskWebSocketHandler(), "/async-task")
                .setAllowedOrigins("*");
    }
}

四:测试

1:在 apifox 地址栏输入地址:如:ws://localhost:9010/async-task,点击连接后,进行测试,如图:

注意 :ws 是 WebSocket 的专属通信协议标识。ws://(非加密)和 wss://(加密,对应 HTTPS)是WebSocket 协议的官方 URL 标识。​

如:访问用 http://xxx.xxx.com(走 HTTP 协议)​,连接 WebSocket 服务用 ws://xxx.xxx.com/ws(走 WebSocket 协议)​,没有ws标识,客户端会默认按 HTTP 协议处理请求。

2:遇到的问题如图:

查看日志报错如下图:

Request processing failed: org.springframework.web.socket.server.HandshakeFailureException: Uncaught failure for request http://localhost:9010/async-task -

No 'jakarta.websocket.server.ServerContainer' ServletContext attribute. Are you running in a Servlet container that supports JSR-356?] with root cause

java.lang.IllegalArgumentException: No 'jakarta.websocket.server.ServerContainer' ServletContext attribute. Are you running in a Servlet container that supports JSR-356?

解决方法:

复制代码
<exclusions>
    <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </exclusion>
</exclusions>

即:在 dynamic-datasource-spring-boot3-starter 包中,将 undertow 排除掉,完整如下:

再次启动调用如图:

输入任意字符进行测试:

如上图,测试成功,发送任意字符后,接口模拟任务执行进度,每隔一秒返回任务进度,直到任务处理完成。

五:总结

通过本文的实践探索,了解了 WebSocket 在实时通信领域的优势。从理论概述到实战应用,实现了一个基于 WebSocket 的任务进度监控系统,实现了前端与后端的高效双向通信。WebSocket 只需一次 HTTP 握手即可建立持久连接,打破了 HTTP "客户端请求 - 服务端响应" 的单向通信瓶颈,实现两端随时双向传数据,无需反复建立连接。WebSocket 服务端可以主动推送数据,无需客户端轮询。

相关推荐
玛卡巴卡015 小时前
HTTPS工作过程
网络协议·http·https
roman_日积跬步-终至千里11 小时前
【系统架构设计(35)】TCP/IP协议族详解
网络协议·tcp/ip·系统架构
2301_8098152512 小时前
网络协议——UDP&TCP协议
网络·网络协议·udp
#include<菜鸡>13 小时前
AXI_CAN IP 简单使用。(仿真、microblaze)
网络协议·tcp/ip·fpga开发
小白iP代理15 小时前
动态住宅IP vs. 静态数据中心IP:未来趋势与当前选择
网络·网络协议·tcp/ip
LeoZY_15 小时前
开源超级终端PuTTY改进之:增加点对点网络协议IocHub,实现跨网段远程登录
运维·网络·stm32·嵌入式硬件·网络协议·运维开发
2501_9159214317 小时前
HTTPS 映射如何做?(HTTPS 映射配置、SNI 映射、TLS 终止、内网映射与 iOS 真机验证实战)
android·网络协议·ios·小程序·https·uni-app·iphone
半桔18 小时前
【网络编程】UDP 编程实战:从套接字到聊天室多场景项目构建
linux·网络·c++·网络协议·udp
qq_3168377518 小时前
spring cloud 同一服务多实例 websocket跨实例无法共享Session 的解决
java·websocket·spring cloud