背景:

最近在项目里实现了一个团队图片协同编辑功能,需要用到 WebSocket 实时同步多个用户的编辑操作。本以为是个简单的功能,结果在 WebSocket 连接上卡了整整一天!
前端死活连不上后端,报错 WebSocket connection failed,后端要么没日志,要么报各种奇怪的错误。今天就把这个完整的排查过程和解决方案分享给大家,希望能帮到遇到同样问题的同学。

注意待会这个特殊配置要考,就是这个让我找了好久问题

一开始,发现前端的socket和后端握手握不上

排查1:直接在浏览器上发一个请求

所以就在浏览器控制台上调试

const ws = new WebSocket('ws://localhost:8123/api/ws/picture/edit?pictureId=2049500849322913794');

ws.onopen = () => console.log('✅ 连接成功!');
ws.onmessage = (e) => console.log('📨 收到消息:', e.data);
ws.onerror = (e) => console.error('❌ 错误:', e);
ws.onclose = (e) => console.log('🔒 关闭, code:', e.code);

排查2:查看websocket依赖有没有引入

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

排查3:检查 WebSocket 配置类

@Configuration
@EnableWebSocket
@Slf4j
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Autowired
    private PictureEditHandler pictureEditHandler;
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(pictureEditHandler, "/api/ws/picture/edit") <---问题在这
                .addInterceptors(wsHandshakeInterceptor)
                .setAllowedOriginPatterns("*");
    }
}

排查4:排查前端(写一个更具体的脚本)

// TEST_WEBSOCKET.js - 在浏览器控制台直接运行

console.log('========== WebSocket 连接测试 ==========');

const ws = new WebSocket('ws://localhost:8123/api/ws/picture/edit?pictureId=2049500849322913794');

ws.onopen = () => {
    console.log('✅ 连接成功!');
    console.log('就绪状态:', ws.readyState);
};

ws.onmessage = (e) => {
    console.log('📨 收到消息:', e.data);
};

ws.onerror = (e) => {
    console.error('❌ 连接错误:', e);
};

ws.onclose = (e) => {
    console.log('🔒 连接关闭');
    console.log('  - Code:', e.code);
    console.log('  - Reason:', e.reason);
    console.log('  - WasClean:', e.wasClean);
};

前端显示连接失败,而且后端这边一边反应都没有

排查5:(写一个过滤器来追踪请求)

@Order(0)
@Component
@Slf4j
public class WebSocketDiagnosticFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws ServletException, IOException {
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String uri = httpRequest.getRequestURI();
            
            // 只记录 WebSocket 相关的请求
            if (uri.contains("/ws/")) {
                log.info("🔍 [WebSocket Diagnostic] 请求 URI: {}", uri);
                log.info("🔍 [WebSocket Diagnostic] 请求方法: {}", httpRequest.getMethod());
                log.info("🔍 [WebSocket Diagnostic] Upgrade Header: {}", 
                    httpRequest.getHeader("Upgrade"));
                log.info("🔍 [WebSocket Diagnostic] Connection Header: {}", 
                    httpRequest.getHeader("Connection"));
            }
        }
        
        chain.doFilter(request, response);
    }
}

前端继续测刚刚的脚本,后端输出了,说明后端收到了发来的请求,但是为什么websocket却收不到握手请求?

🔍 [WebSocket Diagnostic] 请求 URI: /api/ws/picture/edit
🔍 [WebSocket Diagnostic] 请求方法: GET
🔍 [WebSocket Diagnostic] Upgrade Header: websocket
🔍 [WebSocket Diagnostic] Connection Header: Upgrade

排查6:握手拦截器

@Override
public boolean beforeHandshake(ServerHttpRequest request, ...) {
    // ServerHttpRequest 不能直接转换为 HttpServletRequest!
    if (!(request instanceof HttpServletRequest)) {
        return false;
    }
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    // ...
}

问了一下AI,发现这个是错的,问了一下AI:

正确的写法:

@Override
public boolean beforeHandshake(ServerHttpRequest request, ...) {
    if (!(request instanceof ServletServerHttpRequest)) {
        return false;
    }
    // 拿到 Spring 的包装类
    ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
    // 从包装类里取出真正的 HttpServletRequest
    HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
    // ...
}

排查7:项目中还使用了satoken权限校验,怕这个会影响到握手请求所以就加了一个排除的请求(后来又重新排查,发现不是这个的问题)

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor())
            .addPathPatterns("/**");  
}

暂时把websocket的请求路径排除掉

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor())
            .addPathPatterns("/**")
            .excludePathPatterns("/api/ws/**");  // 排除 WebSocket 路径
}

排查8:排查websocket的拦截器,而且项目还有一个统一的项目访问前缀

配置类

websocket的拦截器

查阅 Spring Boot 官方文档和问了一下AI后发现:
server.servlet.context-path 只对 Spring MVC 的 HTTP 路由生效,对 WebSocket 是不会生效的
这就是为什么:
HTTP 接口访问 /api/picture/xxx 正常
但 WebSocket 注册 /api/ws/picture/edit 却失败
因为 Spring WebSocket 会尝试注册 /api/ws/picture/edit,但 Tomcat 实际监听的是 /ws/picture/edit(不带 /api 前缀)

而对于前端来说,发的请求是不带/api的

// 错误:不要加 /api 前缀
const ws = new WebSocket('ws://localhost:8123/api/ws/picture/edit?...');

// 正确:直接使用 WebSocket 路径
const ws = new WebSocket('ws://localhost:8123/ws/picture/edit?pictureId=xxx');

总结
这个问题的根本原因是:server.servlet.context-path 不影响 WebSocket,导致路径配置错误。
最终的正确配置:
HTTP 接口:/api/picture/xxx(受 context-path 影响)
WebSocket:/ws/picture/edit(不受 context-path 影响)
希望这篇文章能帮你少走弯路!如果有帮助,记得点赞收藏哦~ 😊

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐