【webSocket协议】进阶实战案例(Spring 原生低层 API)
目录
使用Spring 原生低层 API
1、引入依赖
2、按需编写webSocket的拦截器
3、编写消息处理器类
4、注册 WebSocket 处理器
使用低层 API的总结
大家好,我是jstart千语。今天给大家带来webSocket在使用过程的实战案例,让大家明白webSocket在项目中是如何使用的,其实webSocket的实现方式有好几种,这里给大家介绍三种使用方式。分别是Spring 原生低层 API、 使用 @EnableWebSocketMessageBroker + STOMP 协议、使用JSR 356 标准。这一篇就先只讲Spring 原生低层 API的使用案例。
关于webSocket的原理等基础知识在上一篇有讲到: 【网络协议】WebSocket讲解-CSDN博客
使用Spring 原生低层 API
这种方式的使用步骤就是:
- 引入依赖:spring-boot-starter-websocket
- 按需编写webSocket的拦截器(与springMVC提供的拦截器不同)
- 编写消息处理器类
- 注册 WebSocket 处理器
1、引入依赖
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、按需编写webSocket的拦截器
拦截器示例:
握手前的方法形参中有一个attributes的map参数。这里就可以往里面塞一些需要进行业务处理的数据了,在后续可以通过session取出来。
@Component public class exampleHandshakeInterceptor implements HandshakeInterceptor { //这里可以注入其他的service类,辅助业务的完成 @Resource private UserService userService; @Resource private PictureService pictureService; @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { //校验权限、、、 return false; //false表示不放行、返回true表示放行 } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { } }
3、编写消息处理器类
让类继承一个父类TextWebSocketHandler ,其实更加原生的是WebSocketHandler,但我们最终要发的是文本消息嘛,所以就使用TextWebSocketHandler;
然后重写里面的三个方法,分别表示建立连接时、客户端发送消息时、连接关闭时执行的方法。
示例:项目背景为一个共同编辑一张图片的场景,但是同一时间只有一个用户能正在编辑,在同一张图片的编辑室内的其他用户可以同步看到被修改后的图片,当正在编辑的用户退出编辑到编辑室里时,编辑室内的其他用户才可以进入编辑。
可以看到下面的代码成员变量的map都是使用ConcurrentHashMap,这是一个线程安全的map,避免出现并发问题。
userEditing:表示正在编辑的用户和图片,key表示正在被编辑的图片,value表示正在编辑的用户
pictureEditRome:编辑室内的用户,key表示某个图片的编辑室,value表示这个编辑室内的用户的session。(对用户发消息是基于session来发的)
@Component
@Slf4j
public class exampleHandle extends TextWebSocketHandler {
@Resource
private UserService userService;
//正在编辑图片的用户,key:pictureId、value:userId
private static final ConcurrentHashMap<Long,Long> userEditing = new ConcurrentHashMap<>();
//各个图片的编辑室 key:pictureId、value:当前图片编辑室的用户session
private static final ConcurrentHashMap<Long, Set<WebSocketSession>> pictureEditRome = new ConcurrentHashMap<>();
/**
* 连接建立时,加入编辑室,并广播给所有用户
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
//具体业务。。。
/**
* 给用户放到对应的编辑室,
* 用户和图片的数据并没有现成的,但可以从上面的拦截器中携带过来
* 拦截器通过attributes设置的属性可以通过形参的session获取
* 如果拦截器设置了:
* attributes.put("user", loginUser);
* attributes.put("picture", picture);
* 这里就可以通过这样获取:
* User user = (User) session.getAttributes().get("user");
* Picture picture = (Picture) session.getAttributes().get("picture");
* 下面的两个方法同理,不再过多赘述
*/
super.afterConnectionEstablished(session);
}
/**
* 客户端给服务端发消息时
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//具体业务。。。
super.handleTextMessage(session, message);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
}
具体的实现代码就不占用大家的阅读时间了,这里只提供一下广播消息(给用户发消息)是如何进行的。
因为给用户发消息的场景比较常用,可以单独抽取出来一个方法:
/**
* 广播消息
* @param excludeSession 被排除的session,表示不需要广播的用户
* @param responseMessage 这是一个自定义的实体,用于封装返回消息,可根据业务的不同自定义
* @param picture 可以取出pictureId,表示广播给哪一张图片的编辑室用户
* @throws IOException
*/
public void broadcastPictureToAll(WebSocketSession excludeSession, PictureEditResponseMessage responseMessage, Picture picture) throws IOException {
//构造广播消息
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(module); //自定义objectMapper转换规则,解决long类型在前端精度丢失问题
String respMessageJson = objectMapper.writeValueAsString(responseMessage);
TextMessage textMessage = new TextMessage(respMessageJson);
//在该图片编辑室广播消息
Set<WebSocketSession> sessions = pictureEditRome.get(picture.getId());
for (WebSocketSession session : sessions) {
//如果是要排除的session,那就不用对这个session广播消息
if (excludeSession != null && excludeSession.equals(session)) {
continue;
}
//如果这个session是开启的才发送
if (session.isOpen()) {
session.sendMessage(textMessage);
}
}
}
重点讲解:
4、注册 WebSocket 处理器
我们怎么让请求发过来时,执行的是哪一个webSocket处理类呢?以及执行哪一个拦截器呢?
这就要将他们注册了,规定一些路径、排除一些路径。与webMVC注册拦截器相似。
@Component
@EnableWebSocket //启用webSocket的重要注解
public class PictureWebSocketRoute implements WebSocketConfigurer {
@Resource
private PictureEditHandle pictureEditHandle;
@Resource
private WsHandshakeInterceptor wsHandshakeInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(pictureEditHandle,"/ws/picture/edit")//注册处理类,已经选择开发的路径
.addInterceptors(wsHandshakeInterceptor)//注册拦截器
.setAllowedOrigins("*");//允许跨域
}
}
使用低层 API的总结
完成上述的操作后就大功告成了,讲讲使用这种方法的特点:
特性 | 类要继承 TextWebSocketHandler |
---|---|
协议层级 | 直接处理 WebSocket 原始帧(文本/二进制) |
消息格式 | 需自行解析消息内容(如 JSON、XML) |
连接管理 | 需手动管理连接状态(如在线用户列表、心跳检测) |
消息路由 | 需自行实现消息路由逻辑(如根据消息类型分发) |
广播与点对点 | 需手动实现(如遍历所有 Session 发送消息) |
鉴权与拦截 | 需手动实现(如通过 HandshakeInterceptor 拦截连接) |
集群支持 | 需自行处理多节点消息同步(如通过 Redis 广播) |
适用场景:
- 需要完全自定义 WebSocket 协议(如私有二进制协议)。
- 对性能有极致要求(避免 STOMP 协议解析开销)。
- 项目规模小,消息逻辑简单,无需复杂路由。