Springboot Websocket 实现
在 Spring Boot 中集成 WebSocket 可以快速实现实时双向通信功能,常用场景包括聊天系统、实时通知、在线协作等。Spring 提供了对 WebSocket 的原生支持,结合 @ServerEndpoint 注解或 Spring WebSocket 抽象层可快速开发。
核心原理
WebSocket 是一种在单个 TCP 连接上实现全双工通信的协议,允许客户端和服务器之间进行实时、双向的数据传输。与传统的 HTTP 协议相比,它解决了频繁轮询带来的性能问题,适用于实时聊天、实时数据更新、在线游戏等场景。
建立连接:HTTP 握手升级
WebSocket 连接的建立依赖于 HTTP 协议的“握手升级”机制:- 客户端发送一个特殊的 HTTP 请求,声明要升级到 WebSocket 协议:
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket # 声明要升级的协议 Connection: Upgrade # 表示要升级连接 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # 随机字符串,用于验证服务器 Sec-WebSocket-Version: 13 # 支持的 WebSocket 版本
- 服务器确认升级后,返回 101 状态码(切换协议),并通过
Sec-WebSocket-Accept
字段验证客户端:HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 由客户端的 Key 计算而来
- 握手成功后,TCP 连接保持打开状态,后续通信不再使用 HTTP 协议,而是基于 WebSocket 帧格式。
- 客户端发送一个特殊的 HTTP 请求,声明要升级到 WebSocket 协议:
数据传输:帧格式
WebSocket 数据以“帧(Frame)”为单位传输,帧格式包含:- 操作码(Opcode):标识帧类型(如文本帧
0x1
、二进制帧0x2
、关闭帧0x8
等)。 - 掩码(Mask):客户端发送的帧必须包含掩码(随机密钥),用于防止代理缓存污染。
- 有效载荷(Payload):实际传输的数据(文本或二进制)。
示例:客户端发送文本消息
Hello
时,会被封装为一个文本帧,服务器接收后解析帧得到原始数据。- 操作码(Opcode):标识帧类型(如文本帧
全双工通信
连接建立后,客户端和服务器可以同时双向发送数据,无需等待对方响应:- 客户端可以随时向服务器发送消息(如用户输入的聊天内容)。
- 服务器也可以主动向客户端推送数据(如实时股价更新、新消息通知)。
连接关闭
任何一方可发送“关闭帧”主动关闭连接,另一方确认后释放 TCP 连接。
与 HTTP 的区别
特性 | HTTP | WebSocket |
---|---|---|
通信方式 | 单向(客户端请求 → 服务器响应) | 双向(客户端 ↔ 服务器 实时交互) |
连接状态 | 无状态(每次请求独立,需重新建立连接) | 持久连接(一次握手后保持连接) |
数据格式 | 基于文本的请求头+响应体 | 二进制帧格式(更轻量) |
适用场景 | 普通网页请求、API 调用 | 实时聊天、实时数据推送、在线协作 |
优势
- 低延迟:避免 HTTP 频繁握手的开销,数据传输更高效。
- 减少带宽:帧格式比 HTTP 头更简洁,尤其适合频繁小数据传输。
- 实时性:服务器可主动推送数据,无需客户端轮询(如
setInterval
不断发请求)。
典型应用场景
- 即时通讯(如微信网页版、在线客服)
- 实时数据展示(如股票行情、监控仪表盘)
- 多人协作工具(如在线文档共同编辑)
- 在线游戏(实时同步玩家操作)
WebSocket 协议的出现,极大地优化了实时通信场景的性能,成为现代 Web 应用不可或缺的技术。
package cn.netkiller.websocket;
import cn.netkiller.record.WebsocketMessage;
import com.google.gson.Gson;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint(value = "/websocket/{appId}/{device}")
@Slf4j
public class WebsocketEndpoint {
private static final ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<String, Session>();
private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Marker Android = MarkerFactory.getMarker("Android");
private final Gson gson = new Gson();
// private Session session;
private String device;
// 连接打开
@SneakyThrows
@OnOpen
public void onOpen(Session session, EndpointConfig endpointConfig,
@PathParam("appId") String appId, @PathParam("device") String device
) {
// 保存 session 到对象
// this.session = session;
this.device = device;
sessions.put(device, session);
// session.getBasicRemote().sendObject();
// String jsonString = gson.toJson(new WebsocketMessage("OnOpen", device, "", new Date()));
// session.getBasicRemote().sendText(jsonString);
log.info("[websocket] onOpen:session={} device={}", session.getId(), device);
}
// 收到消息
@OnMessage
public void onMessage(String message) throws IOException {
log.info("[websocket] onMessage:session={},message={}", sessions.get(device).getId(), message);
WebsocketMessage websocketMessage = gson