WebSocket :从浏览器 API 到 Spring 握手、Handler 与前端客户端

1. WebSocket 与浏览器 API 概览

1.1 协议在做什么

WebSocket 在 HTTP 握手之后升级为全双工 长连接,适合聊天、会中控件、语音信令等高频、低延迟场景。与「请求---响应」式的短连接相比,减少了重复建连与头部开销。

1.2 构造函数

语法:

js 复制代码
const myWebSocket = new WebSocket(url [, protocols]);
  • url :服务端响应的 WebSocket 地址(ws://wss://)。
  • protocols(可选):子协议字符串或字符串数组,用于同一服务上区分多种交互语义。

若连接因安全策略等原因被拒绝,可能抛出 SECURITY_ERR(具体以浏览器为准)。

1.3 常用属性

属性 说明
binaryType 二进制帧的解析方式(如 blob / arraybuffer
bufferedAmount(只读) 尚未发往服务器的字节数
extensions(只读) 协商到的扩展
protocol(只读) 服务器选中的子协议
readyState(只读) CONNECTING(0) / OPEN(1) / CLOSING(2) / CLOSED(3)
url(只读) 实例化时使用的绝对 URL
onopen / onmessage / onerror / onclose 各阶段回调

1.4 主要方法

  1. close([code[, reason]]):关闭连接;已关闭则无操作。
  2. send(data) :将数据入队发送;会根据数据量增加 bufferedAmount,缓冲区异常时连接可能被关闭。

1.5 事件

除上述 on* 属性外,也可用 addEventListener 监听:

  • open:连接建立成功
  • message:收到服务端数据
  • error:发生错误
  • close:连接关闭
前端例子 复制代码
// const socket = new WebSocket("ws://echo.websocket.org");
// const receivedMsgContainer = document.querySelector("#receivedMessage");  
socket.addEventListener("message", function(event) {   
console.log("Message from server ", event.data);  
receivedMsgContainer.value = event.data; 
});

更完整的说明可参考 MDN WebSocket 及 W3C/HTML 相关规范。


2. 握手拦截器:在连接建立前校验 Token

浏览器发起 WebSocket 时,首包仍是 HTTP Upgrade 握手 。Spring 的 HandshakeInterceptor 在握手完成前完成后可以介入。握手前适合放置鉴权逻辑:不通过则拒绝握手,连接不会进入业务 Handler。

项目中 WebSocketInterceptor 从查询参数读取 token,用 JwtUtil 校验,并把用户标识写入 attributes ,供后续 WebSocketSession 使用:

java 复制代码
@Component
public class WebSocketInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        try {
            if (request instanceof ServletServerHttpRequest) {
                ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
                HttpServletRequest httpRequest = servletRequest.getServletRequest();

                // 从请求参数中获取token
                String token = httpRequest.getParameter("token");
                // ...
                if (token != null && !token.trim().isEmpty()) {
                    if (jwtUtil.validateToken(token)) {
                        Long userId = jwtUtil.getUserIdFromToken(token);
                        String username = jwtUtil.getUsernameFromToken(token);
                        attributes.put("userId", userId);
                        attributes.put("username", username);
                        return true;
                    }
                }
            }
        } catch (Exception e) {
            // ...
        }
        return false;
    }
    
    ```
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                           WebSocketHandler wsHandler, Exception exception) {
    // 握手后的处理
}
}

要点:

  • return true :允许握手,attributes 会并入 WebSocketSession.getAttributes()
  • return false:拒绝握手,前端表现为连接失败或立即关闭。

3. Handler:连接、消息与关闭的生命周期

MeetingWebSocketHandler 继承 TextWebSocketHandler ,专门处理文本帧(JSON 信令与聊天等)。核心重写方法:

方法 时机
afterConnectionEstablished 握手成功,会话已可用
handleTextMessage 收到文本消息
afterConnectionClosed 连接关闭
ini 复制代码
@Component
public class MeetingWebSocketHandler extends TextWebSocketHandler {
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            Map<String, Object> attributes = session.getAttributes();
            Long userId = (Long) attributes.get("userId");
            String meetingId = extractMeetingId(session);
            // ...
    }
    
    ```
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    String payload = message.getPayload();
    JSONObject json = JSON.parseObject(payload);
    String type = json.getString("type");
    JSONObject data = json.getJSONObject("data");
   //...
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    Map<String, Object> attributes = session.getAttributes();
    Long userId = (Long) attributes.get("userId");
    String meetingId = extractMeetingId(session);
    //...
}
}

4. WebSocketConfig:注册端点、拦截器与容器参数

WebSocketConfig 实现 WebSocketConfigurer ,在 registerWebSocketHandlers 中把 URL 路径 映射到 Handler ,并挂上握手拦截器

26:38:smart-meeting-backend/src/main/java/com/smartmeeting/config/WebSocketConfig.java 复制代码
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(meetingWebSocketHandler, "/ws/meeting/{meetingId}")
                .addInterceptors(webSocketInterceptor)
                .setAllowedOriginPatterns("*");


    }

同时通过 ServletServerContainerFactoryBean 调整 Tomcat WebSocket 容器 的单帧大小与空闲超时,避免大消息被默认限制截断:

ini 复制代码
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(524288);
        container.setMaxBinaryMessageBufferSize(524288);
        container.setMaxSessionIdleTimeout(300000L);
        return container;
    }

注意:若应用配置了 server.servlet.context-path (如 /api),前端完整路径需带上该前缀(见下一节)。


5. 前端:拼接 URL、封装客户端与会议页初始化

5.1 会议页中创建连接

MeetingRoom.vue 在初始化 WebSocket 前检查登录 Token ,再用 getWebSocketBaseUrl() 拼出与当前页面协议一致的 ws / wss 基地址,路径包含 /api (与后端 context-path 一致)及会议 idtoken 查询参数:

957:1009:smart-meeting-frontend/src/views/MeetingRoom.vue 复制代码
const initWebSocket = () => {
  return new Promise((resolve, reject) => {
    if (!userStore.token) {
      // ...
      reject(error)
      return
    }
    const wsBaseUrl = getWebSocketBaseUrl()
    const wsUrl = `${wsBaseUrl}/api/ws/meeting/${route.params.id}?token=${userStore.token}`
    wsClient.value = new WebSocketClient(wsUrl)
    const timeout = setTimeout(() => {
      // 超时关闭并 reject
    }, 10000)
    wsClient.value.on('open', () => {
      clearTimeout(timeout)
      wsConnected.value = true
      initVoiceWebSocket()
      resolve()
    })
    wsClient.value.on('error', (error) => {
      clearTimeout(timeout)
      // ...
      reject(error)
    })
    // ...
  })
}

这样后端的 WebSocketInterceptor 才能从 getParameter("token") 取到与 HTTP 接口一致的 JWT。

5.2 对原生 WebSocket 的薄封装

工程内 WebSocketClient 在内部使用 new WebSocket(this.url) ,将 onopen / onmessage / onerror / onclose 转为自定义的 on('open') / emit 模式,并在 send 时对对象做 JSON.stringify,便于业务侧统一发送 JSON 信令:

13:74:smart-meeting-frontend/src/api/websocket.js 复制代码
  connect() {
    this.ws = new WebSocket(this.url)
    this.ws.onopen = () => { this.emit('open') }
    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      this.emit('message', data)
      if (data.type) {
        this.emit(data.type, data.data)
      }
    }
    // onerror / onclose ...
  }
  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      if (typeof data === 'object') {
        this.ws.send(JSON.stringify(data))
      } else {
        this.ws.send(data)
      }
    }
  }

参考资料:

cloud.tencent.com/developer/a... cloud.tencent.com/developer/a...

相关推荐
顶点多余2 小时前
线程互斥+线程同步+生产消费模型
java·linux·开发语言·c++
神奇小汤圆2 小时前
探索springboot程序打包docker的最佳方式
后端
邦爷的AI架构笔记2 小时前
我用Claude API接入了CI/CD安全扫描,踩了这几个坑
后端
⑩-2 小时前
Java基础+集合框架-八股文
java·开发语言
ai产品老杨2 小时前
异构计算时代的安防底座:基于 Docker 的 X86/ARM 双架构 AI 视频管理平台深度解析
arm开发·docker·架构
福运常在2 小时前
股票数据API(19)次新股池数据
java·python·maven
Zaki_gd2 小时前
Cortex-M7 D-Cache 与 DMA 缓存一致性说明
java·spring·缓存
多看书少吃饭2 小时前
Vue3 + Java + Python 打造企业级大模型知识库(含 SSE 流式对话完整源码)
java·python·状态模式