上个小节我们已经可以成功付款,并且后端项目的Web方法接收到了付款成功的通知,罚款记录的状态从“未付款”变成了“已付款”。这节课我们要给SpringBoot配置上WebSocket,这样将来才可以推送付款成功的结果给前端页面。
一、缓存WebSocket连接
WebSocket类似于Servlet,需要我们声明一些生命周期函数,而且这些函数还都不是我们能主动调用的。
@ServerEndpoint(value = "/mysocket")
@Component
public class MyWebSockertService{
@OnOpen
public void onOpen(Session session) {
//创建WebSocket连接时候执行
}
@OnClose
public void onClose(Session session) {
//管理WebSocket连接时候执行
}
@OnMessage
public void onMessage(String message, Session session) {
//接收消息时候执行
}
@OnError
public void onError(Session session, Throwable error) {
//发生错误时候执行
}
}
向客户端发送消息,需要使用Session对象。但是这些生命周期函数都由于客户端某种操作,而触发执行的。如果客户端不触发操作,那么后端是无法主动给客户端发送消息的。所以我们要把Session对象缓存起来。需要的时候,我们提取缓存的Session,主动向客户端发送消息。
因为后端的WebSocket服务类是多例的,所以我们想要全局共享缓存,要么用Redis,要么声明静态的HashMap对象。如果选用Redis,那么保存Session对象要用到序列化,会消耗一定的时间,所以不建议使用。如果全局共享使用HashMap,又会存在并发读写的问题,最终我们选择ConcurrentHashMap类。
二、配置WebSocket
在com.example.emos.api.config
包中创建WebSocketConfig.java
类,用于给SpringBoot添加WebSocket功能。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
创建com.example.emos.api.websocket
包,定义WebSocketService.java
类,用于负责处理WebSocket连接。这个类是多例的,每次有客户端用WebSocket连接这个类,Spring就会创建一个新的WebSocketService
对象,所以大家不用担心线程安全的问题。
因为WebSocket是长连接协议,它完全异于Http协议,所以我们不能用处理Http请求和响应的想法去看待WebSocket连接。例如我们在Http协议上很轻松可以传递Cookie数据,但是WebSocket不支持Cookie,所以我们要自己把Token字符串上传给服务端,然后服务端不能用StpUtil.getUserIdAsInt()
获取到UserId,我们要自己从Token字符串中提取UserId出来。
在WebSocket中,我们要约定跟客户端传递数据的格式。为了能让数据看起来格式规整,我采用传递JSON字符串的方式。
参数 | 含义 | 例子 |
---|---|---|
opt | 操作行为 | ping |
token | 令牌字符串 | eyJzb2Z0d2FyZV9pZCI6IjROUkIxLTBYWkFCWkk5RTYtNVNNM1IiLCJjbGll |
其他参数 | 其他参数 | (略) |
@Slf4j
@ServerEndpoint(value = "/socket")
@Component
public class WebSocketService {
//用于保存WebSocket连接对象
public static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
Map map = session.getUserProperties();
if (map.containsKey("userId")) {
String userId = MapUtil.getStr(map, "userId");
sessionMap.remove(userId);
}
}
/**
* 接收消息
*
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
//把字符串转换成JSON
JSONObject json = JSONUtil.parseObj(message);
String opt = json.getStr("opt");
if("ping".equals(opt)){
return;
}
//从JSON中取出Token
String token = json.getStr("token");
//从Token取出userId
String userId = StpUtil.stpLogic.getLoginIdByToken(token).toString();
//取出Session绑定的属性
Map map = session.getUserProperties();
//如果没有userId属性,就给Session绑定userId属性,关闭连接的时候会用到
if (!map.containsKey("userId")) {
map.put("userId", userId);
}
//把Session缓存起来
if (sessionMap.containsKey(userId)) {
//替换缓存中的Session
sessionMap.replace(userId, session);
} else {
//向缓存添加Session
sessionMap.put(userId, session);
}
sendInfo("ok",userId);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误", error);
}
/**
* 发送消息给客户端
*/
public static void sendInfo(String message, String userId) {
if (StrUtil.isNotBlank(userId) && sessionMap.containsKey(userId)) {
//从缓存中查找到Session对象
Session session = sessionMap.get(userId);
//发送消息
sendMessage(message, session);
}
}
/**
* 封装发送消息给客户端
*/
private static void sendMessage(String message, Session session) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("执行异常", e);
}
}
}