SpringBoot 整合WebSocket,推送数据给前台

310 阅读2分钟

1、WebSocket的优势

我们都知道HTPP协议是基于请求响应模式,并且无状态的。HTTP通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息

举例来说,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此WebSocket 就是在这样的背景下发明的。

2、SpringBoot 整合 WebSocket

1、添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2、新建WebSocket 配置类

这个配置类检测带注解@ServerEndpoint的bean并注册它们,配置类代码如下:

@Configuration
public class WebSocketConfig {
    /**
     * 注入ServerEndpointExporter,
     * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3、 OnLineWebSocket

在线编辑的OnLineWebSocket类,当前页面被用户修改后,推送页面的变更信息给前台 前台根据页面id和页面的版本,判断是否重新加载页面数据。

@Slf4j
@Component
@ServerEndpoint("/onLineWebSocket/{pageId}")
public class OnLineWebSocket {
   
  private static Interner<String> lock = Interners.newWeakInterner();
  
  private static final String LOCK_PREFIX = "socket_";
  
  // 长连接的超时时间,如果10分钟没有操作,认为超时
   private static final long TIMEOUT = 10 * 60 * 1000;
   
   /**
    * 长连接会话Id
    */
   private String sessionId;
   
   /**
    * pageId 页面id
    */
   private String pageId;
   
   /**
    * 长连接会话
    */
   private Session session;
   
   /**
    * 最近一次心跳时间, 用于定时清理失效的会话连接
    */
   private long heart;
   
   /**
     sessionId 与 onLineWebSocket 对应关系
     一个会话,对应一个 onLineWebSocket 对象
    */
   private static ConcurrentHashMap<String, OnLineWebSocket> webSocketMap = new ConcurrentHashMap<>();
   
   
   /** 
       一个页面id,如果有多个浏览器打开,就对应多个会话,
       所以页面Id,和回话id是一对多的关系
    *  pageId 与sessionId一对多关系,
    */
   private static ConcurrentHashMap<String, Set<String>> pageSessionMap = new ConcurrentHashMap<>();
   
   /**
    * 打开连接时调用
    *
    * @param session 会话
    * @param pageId  页面id
    */
   @OnOpen
   public void onOpen(Session session, @PathParam(value = "pageId") String pageId) {
      if (StringUtil.isNotEmpty(pageId)) {
         this.pageId = pageId;
         this.session = session;
         this.sessionId = session.getId();
         // 每次连接时,更新心跳时间
         this.heart = System.currentTimeMillis();
         webSocketMap.put(this.sessionId, this);
         this.addPageSession();
      }
      else {
         throw new EcoBootException("页面id参数不能为空");
      }
   }
   
   /**
    * 关闭连接时调用
    连接关闭时,要清除对应的关系
    */
   @OnClose
   public void onClose() {
      this.closeSession();
      this.deletePageSession();
      if (StringUtil.isNotEmpty(sessionId)) {
         webSocketMap.remove(sessionId);
      }
   }
   
   /**
    * 错误时调用
    *
    * @param throwable 异常
    */
   @OnError
   public void onError(Throwable throwable) {
      System.out.println("发生错误");
      throwable.printStackTrace();
   }
   
   
   /**
    * 收到客户端信息后,根据接收人的 pageId 把消息推下去或者群发
    *
    * @param message json格式消息
    * @throws IOException 异常
    */
   @OnMessage
   public void onMessage(String message) throws IOException {
      if (session!=null && session.isOpen() && !StringUtil.isEmpty(message)) {
         if (StringPool.STAR.equals(message)) {
            //心跳测试消息
            session.getBasicRemote().sendText(message);
            heart = System.currentTimeMillis();
         }
         else {
            session.getBasicRemote().sendText(message);
            heart = System.currentTimeMillis();
            return;
         }
         
      }
   }
   
   /**
    * 给页面添加session
    */
   private void addPageSession() {
      String joinId = this.pageId;
      synchronized (lock.intern(LOCK_PREFIX + joinId)) {
         Set<String> sessionIds = pageSessionMap.computeIfAbsent(joinId, k -> new HashSet<>());
         sessionIds.add(this.session.getId());
      }
   }

   
   /**
    * 推送消息到客户端
    * @param message 消息文本
    */
   public boolean pushMessage(String message) {
      log.info("start to push Msg = {}",message);
      try {
         if (session!=null && session.isOpen()) {
            log.info("call push to send");
            this.session.getBasicRemote().sendText(message);
            // 更新心跳时间
            this.heart = System.currentTimeMillis();
            log.info("push msg ok");
            return true;
         } else {
            return false;
         }
      } catch (Exception e) {
         log.error("pushMessage",e);
         return false;
      }
   }
   
   /**
    * 关闭连接会话
    *
    */
   private void closeSession() {
      if (session!=null && session.isOpen()) {
         try {
            session.close();
         } catch (Exception e) {
            log.error("closeSession", e);
         }
      }
   }
   
   /**
    * 长连接会话是否有效
    心跳时间进行判断处理
    * @return boolean
    */
   private boolean isValid() {
      return session!=null &&  session.isOpen() && heart + TIMEOUT > System.currentTimeMillis();
   }
   
   /**
    * 清理超时会话,10分钟之内无心跳会话,或会话已经关闭
    */
   public static void clearSession() {
      Iterator<Map.Entry<String, OnLineWebSocket>> iterator = webSocketMap.entrySet().iterator();
      while (iterator.hasNext()) {
         Map.Entry<String, OnLineWebSocket> entry = iterator.next();
         OnLineWebSocket ws = entry.getValue();
         if(!ws.isValid()){
            ws.closeSession();
            ws.deletePageSession();
            iterator.remove();
         }
      }
   }
   
   /**
    * 删除页面id和会话id对应关系
    */
   private  void deletePageSession() {
      String joinId = this.pageId;
      synchronized (lock.intern(LOCK_PREFIX + joinId)) {
         Set<String> sessionIds = pageSessionMap.get(joinId);
         if (sessionIds != null && sessionId != null) {
            sessionIds.remove(sessionId);
         }
         if (CollectionUtil.isEmpty(sessionIds)) {
            pageSessionMap.remove(joinId);
         }
      }
   }
   
   
   /**
    根据pageId,来获取会话Id,列表
    *
    * @param pageId 页面id
    * @return Set
    */
   public static Set<String> getPageSessions(String pageId) {
      String joinId = pageId;
      synchronized (lock.intern(LOCK_PREFIX + joinId)) {
         Set<String> ids = pageSessionMap.get(joinId);
         if (ids != null) {
            return new HashSet<>(ids);
         }
      }
      return null;
   }
   
   // 根据回话id,来获取 OnLineWebSocket 对象
   public static OnLineWebSocket get(String sessionId){
      if(!StringUtil.isEmpty(sessionId)){
         return webSocketMap.get(sessionId);
      }
      return null;
   }
   
   
   /**
    * 每隔10分钟进行清理
    */
   @Async("scheduledPoolTaskExecutor")
   @Scheduled(cron = "0 */10 * * * *")
   public void taskToCleanSession() {
      clearSession();
   }
}

4、当页面有修改时,通知前台进行刷新

/**
 * 推送消息给前台,通知刷新页面内容
 */
public void pushWebSocketMsg(String pageId) {
   // 启动线程来进行消息的推送
   ThreadUtil.execute(()->{
      Set<String> sessions =  OnLineWebSocket.getPageSessions(pageId);
      // 给所有的会话进行推送
      if(!CollectionUtil.isEmpty(sessions)) {
         log.info("sessions size = {}",sessions.size());
         PageInfo pageInfo = pageInfoService.getById(pageId);
         WebSocketData webSocketData = new WebSocketData();
         webSocketData.setPageId(pageId);
         webSocketData.setVersion(pageInfo.getDraftVersion());
         String msg = BeanUtil.toJsonString(webSocketData);
         for(String id : sessions) {
            log.info("session id = {}",id);
            OnLineWebSocket onLineWebSocket = OnLineWebSocket.get(id);
            if(onLineWebSocket != null ){
               onLineWebSocket.pushMessage(msg);
            }
         }
      }
   });
}

3、测试运行

1、在前端控制台输入如下代码

const socket = new WebSocket('ws://xxxx:2022/eco-online-edit-server/onLineWebSocket/9/');

// Connection opened
socket.addEventListener('open', function (event) {
    socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
    console.log('Message from server ', event.data);
});

2、效果图如下:

  • 控制台端效果

image.png

  • NetWork端效果 image.png