WebSocket集成

143 阅读4分钟

1. 增加 ape-common-websocket 模块

image.png

2. 引入相关依赖

<!-- websocket相关依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.4.2</version>
</dependency>

3. 定义WebSocketConfig配置类

说明: 只要使用WebSocket,就需要创建一个WebSocketConfig配置类,这样就可以自动启动一个WebSocket服务!

public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

4. WebSocketCheckConfig鉴权配置类

说明: 业务场景中可能不能什么用户都可以进行访问WebSocket,所以需要进行鉴权和配置相关信息的功能!

@Component
public class WebSocketServerConfig extends ServerEndpointConfig.Configurator {

    @Override
    public boolean checkOrigin(String originHeaderValue) { //实现鉴权效果,要重写方法
        //获取websocket的request对象
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        //获取http的request对象
        HttpServletRequest request = servletRequestAttributes.getRequest();

        //校验逻辑

        return true; //校验通过
    }

    @Override
    public void modifyHandshake(ServerEndpointConfig sec,
                                HandshakeRequest request, HandshakeResponse response) {
        Map<String, List<String>> parameterMap = request.getParameterMap(); //获取WebSocket的所有请求参数
        List<String> erpList = parameterMap.get("erp"); //获取前端传递的erp相关参数信息(作为,每个连接的唯一标识 url?erp=xx)

        if(!CollectionUtils.isEmpty(erpList)) {
            //使用ServerEndpointConfig来进行相应的参数传递(此处将erp相关参数信息作为用户参数传递)
            sec.getUserProperties().put("erp", erpList.get(0)); //get(0), 一般请求参数只对应一个值
        }
    }
}

5. 业务模块使用

@Component
@Slf4j
@ServerEndpoint(value = "/ssm/socket", configurator = WebSocketServerConfig.class)
//它将这个类声明为一个WebSocket端点,指定了连接的URL路径为 ws://localhost:8080 + "/ssm/socket"
// configurator提供配置 WebSocket 端点的自定义配置器
public class ssmWebSocket {

    /**
     * 注意:标注 @ServerEndpoint 注解的类对象是多例的,即每个连接都会创建一个新的对象
     *
     * @OnOpen:当 WebSocket 连接建立时,会调用标注有 @onOpen 的方法
     *
     * @OnClose:当 WebSocket 连接关闭时,会调用标注有 @OnClose 的方法
     *
     * @OnMessage:当收到客户端发送的消息时,会调用标注有 @OnMessage 的方法
     *
     * @OnError:当出现错误时,会调用标注有 @OnError 的方法
     *
     */

    /**
     * 记录当前在线的连接数
     * 提供原子操作的Integer类,通过线程安全的方式操作加减。防止同时上线时,只加1。
     */
    private static AtomicInteger onlineCount = new AtomicInteger(0);

    /**
     * 存放所有的在线客户端
     * HashMap线程不安全,ConcurrentHashMap线程安全
     */
    private static Map<String, ssmWebSocket> clients = new ConcurrentHashMap<>();

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 当前会话的唯一标识key
     */
    private String erp = "";

    /**
     * 连接建立成功调用的方法
     * @param session
     * @param config  EndpointConfig类可获取前端传递的参数信息
     * @throws IOException
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) throws IOException {
      try {
          Map<String, Object> userProperties = config.getUserProperties(); //获取WebSocket的所有请求参数
          String erp = (String) userProperties.get("erp"); //获取前端传递的erp参数
          this.erp = erp;
          this.session = session;
          if(clients.containsKey(this.erp)) { //当前erp已建立连接,将旧连接关闭
              clients.get(this.erp).session.close();
              clients.remove(this.erp); //关闭连接后,再从集合中删除
              onlineCount.decrementAndGet(); //在线人数减一
          }

          clients.put(this.erp, this); //将新连接加入集合
          onlineCount.incrementAndGet(); //在线人数加一
          log.info("有新连接加入:{},当前在线人数:{}", erp, onlineCount.get());
          sendMessage("连接成功", session); //告诉前端连接成功了
      } catch (Exception e) {
          log.error("建立链接错误{}", e.getMessage(), e);
      }
    }

    /**
     *
     * @param message 给前端发送什么消息
     * @param session 往哪个连接中发消息
     */
    public void sendMessage(String message, Session session) throws IOException {
        log.info("服务端给客户端[{}]发送消息:{}", this.erp, message);
        session.getBasicRemote().sendText(message);
    }

    /**
     * 当WebSocket连接关闭时,调用的方法
     */
    @OnClose
    public void onClose() throws IOException {
       try {
           if(clients.containsKey(this.erp)) { //如果当前erp还未关闭连接,就手动关闭
               clients.get(this.erp).session.close();
               clients.remove(this.erp);
               onlineCount.decrementAndGet();
           }
           log.info("有一处连接关闭{},当前在线人数:{}", this.erp, onlineCount.get());
       } catch (Exception e) {
           log.error("建立链接错误{}", e.getMessage(), e);
       }
    }

    /**
     *当出现错误时,会调用的方法
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        log.error("websocket:{},发送错误,错误原因:{}", erp, throwable.getMessage(), throwable);
        try {
            session.close();
        } catch (Exception e) {
            log.error("onError.Exception{}", e.getMessage(), e);
        }
    }

    /**
     *群发消息,为每一个客户端发送消息
     */
    public void sendMessages(String message) throws IOException {
        //遍历所有在线的客户端
        for(Map.Entry<String, ssmWebSocket> sessionEntry : clients.entrySet()) {
            String erp = sessionEntry.getKey();
            ssmWebSocket webSocket = sessionEntry.getValue();
            Session session = webSocket.session; //获取当前客户端的session
            log.info("服务端给客户端[{}]发送消息{}", erp, message);
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("{}发送消息发生异常,异常原因{}", this.erp, message);
            }
        }
    }

    /**
     * 当收到客户端发送的消息时,会调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        log.info("服务端收到客户端[{}]发送的消息:{}", this.erp, message);
        // 模拟心跳机制:
        //    前端可以通过setInterval定时任务每个15秒钟调用一次reconnect函数
        //    reconnect会通过socket.readyState来判断这个websocket连接是否正常
        //    如果不正常就会触发定时连接,每15s钟重试一次,直到连接成功
        if(message.equals("ping")) { //如果收到客户端发送的ping
            this.sendMessage("pong", session); //服务端返回一个pong,来不断重连,不会超时断连
        }
    }
}