1. WebSocket 与浏览器 API 概览
1.1 协议在做什么
WebSocket 在 HTTP 握手之后升级为全双工长连接,适合聊天、会中控件、语音信令等高频、低延迟场景。与「请求—响应」式的短连接相比,减少了重复建连与头部开销。
1.2 构造函数
语法:
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 主要方法
close([code[, reason]]):关闭连接;已关闭则无操作。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 使用:
@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 | 连接关闭 |
@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,并挂上握手拦截器。
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(meetingWebSocketHandler, "/ws/meeting/{meetingId}")
.addInterceptors(webSocketInterceptor)
.setAllowedOriginPatterns("*");
}
同时通过 ServletServerContainerFactoryBean 调整 Tomcat WebSocket 容器 的单帧大小与空闲超时,避免大消息被默认限制截断:
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 一致)及会议 id 与 token 查询参数:
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 信令:
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…