全部开发者教程

企业级在线办公系统

上个小节我们已经可以成功付款,并且后端项目的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);
        }
    }
}