告别轮询!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 服务端可以主动推送数据,无需客户端轮询。

相关推荐
无限大.5 小时前
为什么网站需要“域名“?——从 IP 地址到网址的演进
网络·网络协议·tcp/ip
wha the fuck4045 小时前
(渗透脚本)TCP创建连接脚本----解题----极客大挑战2019HTTP
python·网络协议·tcp/ip·网络安全·脚本书写
Upper9996 小时前
简单记录:TCP数据包的抓取--3次握手、4次挥手
网络·网络协议·tcp/ip
破烂pan6 小时前
Python 长连接实现方式全景解析
python·websocket·sse
ZeroNews内网穿透6 小时前
Dify AI 结合ZeroNews 实现公网快速访问
网络·人工智能·网络协议·tcp/ip·安全·web安全
yBmZlQzJ6 小时前
内网穿透 + 域名解析:到底解决了什么核心问题?
运维·经验分享·网络协议·docker·容器
黑贝是条狗6 小时前
用mormot2 orm模式搭建一个http服务验证设备的注册信息
网络·网络协议·http
真上帝的左手7 小时前
15. 实时数据- SSE VS WebSocket
websocket
北京耐用通信7 小时前
工程师实战:如何以最小成本,耐达讯自动化无缝连接Profinet转DeviceNet网关
人工智能·物联网·网络协议·自动化·信息与通信
福尔摩斯张7 小时前
基于TCP的FTP文件传输系统设计与实现(超详细)
linux·开发语言·网络·网络协议·tcp/ip·udp