SpringBoot下websocket使用

整体描述

在SpringBoot下使用websocket,达到前后端通信的目的,这里简单写下使用。就使用SpringBoot自带的websocket实现。websocket涉及的情况比较多,一定还有一些考虑不到的问题,这里只是提供一个思路,发现问题就具体问题具体分析了。

具体使用

1. 添加依赖

添加websocket的依赖,在pom里添加:

        <!-- websocket 前后端通信-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2. 消息格式

添加依赖之后,创建一个消息的结构体,也就是自定义消息,分为消息id,消息类型,消息内容和消息时间,前后端通信时,可以根据消息类型判断,对消息内容进行解析。比如消息类型为HeartBeat,也就是心跳,可能消息内容就是空,就不用对消息内容进行解析了,再比如消息类型是Broadcast,代表后台传给前端的广播信息,就需要对消息内容进行判断。
注:消息内容格式使用Json字符串,信息灵活多样。

import lombok.Data;

/**
 * websocket消息体
 */

@Data
public class WebSocketMessage {

    /**
     * 消息ID
     */
    private String messageId;

    /**
     * 消息类型
     */
    private String messageType;

    /**
     * 消息内容
     */
    private String messageContent;

    /**
     * 消息时间
     */
    private String messageTime;
}

3. 具体操作

此类主要封装websocket相关操作,连接,发送消息和相关回调等。

@Component
@ServerEndpoint("/websocket/{username}") //暴露的ws应用的路径
public class WebSocket {

    private static final Logger log = LoggerFactory.getLogger(WebSocket.class);

    /**
     * 超时时间间隔,超过此时间(单位:ms),认为websocket客户端连接超时
     * 和定时任务配合,定时任务在发送心跳一分钟之后检查各个Session
     * 这里设定时间间隔为2分钟
     */
    private static final Long timeInterval = (long) (2 * 60 * 1000);

    /**
     * 当前在线客户端数量
     */
    private static AtomicInteger onlineClientNumber = new AtomicInteger(0);

    /**
     * 当前在线客户端集合
     * 以键值对方式存储,key是连接的编号,value是连接的对象
     */
    private static Map<String, Session> onlineClientMap = new ConcurrentHashMap<>();

    /**
     * 当前在线客户端时间集合,用于心跳判断
     * 以键值对方式存储,key是连接的编号,value是最近一次通信时间
     */
    private static Map<String, Long> onlineUserTimeMap = new ConcurrentHashMap<>();

    /**
     * 客户端与服务端连接成功
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        // 在线数+1
        onlineClientNumber.incrementAndGet();
        // 添加当前连接的session
        onlineClientMap.put(session.getId(), session);
        // 添加当前连接的session的时间
        onlineUserTimeMap.put(session.getId(), System.currentTimeMillis());
        log.info("onOpen---->time:[{}],User:[{}],Session:[{}],total:[{}]",
                DateUtils.getTime(),
                username,
                session.getId(),
                onlineClientNumber);
    }

    /**
     * 客户端与服务端连接关闭
     */
    @OnClose
    public void onClose(Session session, @PathParam("username") String username) {
        // 在线数-1
        onlineClientNumber.decrementAndGet();
        // 移除当前连接的session
        onlineClientMap.remove(session.getId());
        // 移除当前连接的session的时间
        onlineUserTimeMap.remove(session.getId());
        log.info("onClose---->Time:[{}],User:[{}],Session:[{}],total:[{}]",
                DateUtils.getTime(),
                username,
                session.getId(),
                onlineClientNumber);
    }

    /**
     * 客户端与服务端连接异常
     */
    @OnError
    public void onError(Throwable error, Session session, @PathParam("username") String username) {
        // 在线数-1
        onlineClientNumber.decrementAndGet();
        // 移除当前连接的session
        onlineClientMap.remove(session.getId());
        // 移除当前连接的user
        onlineUserTimeMap.remove(session.getId());
        log.error("onError---->Time:[{}],User:[{}],Session:[{}],error:[{}]",
                DateUtils.getTime(),
                username,
                session.getId(),
                error.toString());
    }

    /**
     * 客户端向服务端发送消息
     *
     * @param message  接受消息内容
     * @param username 客户端用户名
     */
    @OnMessage
    public void onMsg(Session session, String message, @PathParam("username") String username) {
        log.info("onMsg---->Time:[{}],User:[{}],Session[{}],message:[{}]",
                DateUtils.getTime(),
                username,
                session.getId(),
                message);
        if (message != null && !message.equals("")) {
            try {
                JSONObject jsonMessage = JSONObject.parseObject(message);
                if (jsonMessage.containsKey("messageType")
                        && jsonMessage.get("messageType") != null) {
                    // 心跳消息
                    if (jsonMessage.get("messageType").equals("HeartBeat")) {
                        onlineUserTimeMap.put(session.getId(), System.currentTimeMillis());
                        log.info("onMsg---->HeartBeat:" + onlineUserTimeMap.toString());
                    }

                }
            } catch (Exception e) {
                log.error("onMsg---->Exception:[{}]",e.toString());
            }

        }
    }

    /**
     * 向所有客户端发送消息
     *
     * @param type    消息类别
     * @param content 消息内容
     */
    public static void sendMessageToAll(String type, String content) {
        WebSocketMessage webSocketMessage = new WebSocketMessage();
        webSocketMessage.setMessageId(DateUtils.getDateFormat(new Date(), DateUtils.FULL_TIME_PATTERN_ALL));
        webSocketMessage.setMessageType(type);
        webSocketMessage.setMessageContent(content);
        webSocketMessage.setMessageTime(DateUtils.getTime());
        String message = JSON.toJSON(webSocketMessage).toString();
        // 获得Map的Key的集合
        Set<String> sessionIdSet = onlineClientMap.keySet();
        // 迭代Key集合
        for (String sessionId : sessionIdSet) {
            // 根据Key得到value
            Session session = onlineClientMap.get(sessionId);
            // 发送消息给客户端
            session.getAsyncRemote().sendText(message);
        }
    }

    /**
     * 判断当前的Session是否超时
     */
    public static void checkSession() {
        if (onlineUserTimeMap != null && onlineUserTimeMap.size() > 0) {
            // 获得Map的Key的集合
            Set<String> sessionIdSet = onlineUserTimeMap.keySet();
            // 获得当前时间戳
            Long currentTime = System.currentTimeMillis();
            // 迭代Key集合
            for (String sessionId : sessionIdSet) {
                // 根据Key得到value,即时间戳
                Long time = onlineUserTimeMap.get(sessionId);
                // 根据Key得到value,即Session
                Session session = onlineClientMap.get(sessionId);
                if (currentTime - time > timeInterval) {
                    // 此Session已超时
                    // 移除当前连接的session
                    onlineClientMap.remove(session.getId());
                    // 移除当前连接的user
                    onlineUserTimeMap.remove(session.getId());
                    log.info("checkSession---->remove:" + session.getId());
                    // 关闭session
                    try {
                        session.close();
                    } catch (Exception e) {
                        log.error("checkSession---->Exception:" + e.toString());
                    }
                }
            }
            log.info("checkSession---->onlineUserTimeMap:" + onlineUserTimeMap.toString());
        }
    }

4. 定时任务

此模块主要用于心跳检测和对当前Session进行检测,将没有心跳返回的Session关闭。此处前端需要在收到心跳消息时,给服务器返回一条消息,证明前端还在。定时任务用的就是SpringBoot自带的定时任务模块。

@Component
@EnableScheduling
public class WebSocketTimer {

    private static final Logger log = LoggerFactory.getLogger(WebSocket.class);

    /**
     * 心跳,每5分钟一次
     */
    @Bean
    @Scheduled(cron = "0 0/5 * * * ?")
    public void WebSocketHeartBeat() {
        log.info("WebSocketHeartBeat");
        WebSocket.sendMessageToAll("HeartBeat", "");
    }

    /**
     * 检测Session是否还在连接状态,每5分钟一次
     * 在心跳发出的1分钟后执行
     */
    @Bean
    @Scheduled(cron = "0 1/5 * * * ?")
    public void WebSocketCheckSession() {
        log.info("WebSocketCheckSession");
        WebSocket.checkSession();
    }
}

5. 拦截修改

SpringBoot自带拦截器,将一些认为非法的请求过滤掉,如果你的项目里有SecurityConfig的配置,需要添加websocket地址。

httpSecurity
				// 省略前面的配置代码
				// 在此处添加
                // websocket
                .antMatchers("/websocket/**").anonymous()
                // 省略后面的配置代码

6. 前端代码

前端就是连接,主要就是开启连接,接收消息,这里注意,前端接收到心跳消息,需要给服务器回一条心跳消息。这个前端代码就是测试代码,用户名使用的随机数序列,如果使用需要定用户名。
注:前端需要加个断开重连的逻辑,下面的代码里没有。

<script>
export default {
  // name: "Line",
  data() {
    return {
      username: "",
    };
  },
  created() {
    this.username = Math.random() + "";
    this.initWebSocket();
  },
  mounted() {
    window.addEventListener("message", function (e) {
    });
  },
  methods: {
    initWebSocket () {
      const wsuri = 'ws://localhost:8088/websocket/' + this.username;
      this.webSocketObject = new WebSocket(wsuri);
      this.webSocketObject.onopen = this.webSocketOnOpen
      this.webSocketObject.onmessage = this.webSocketOnMessage
      this.webSocketObject.onerror = this.webSocketOnError
      this.webSocketObject.onclose = this.webSocketOnClose
    },
    webSocketOnOpen(e){
      console.log('与服务端连接打开->',e)
    },
    webSocketOnMessage(e){
      console.log('来自服务端的消息->',e)
      let tempData = JSON.parse(e.data)
      if (tempData.messageType == "HeartBeat") {
        const message = {
          messageType: 'HeartBeat',
          messageContent: ''
        }
        this.webSocketObject.send(JSON.stringify(message))
        return
      }
    },
    webSocketOnError(e){
      console.log('与服务端连接异常->',e)
      // 在此处加重连逻辑
    },
    webSocketOnClose(e){
      console.log('与服务端连接关闭->',e)
      // 在此处加重连逻辑
    },
  },
  watch: {},
};
</script>

7. 其他配置

到这里在本地调试应该是没有问题了,但是在部署到服务器上,如果使用了nginx,可能在nginx还需要一些配置,包括添加websocket支持,这里需要注意的是,websocket连接超时的问题,如果不配置,nginx默认是一分钟没有消息发送,就会关闭,这里需要把参数改为10分钟(因为心跳消息是5分钟发送一次)。
具体配置如下:
在http节点下添加如下配置:

map $http_upgrade $connection_upgrade {
      default upgrade;
      '' close;
  }
  upstream websocket {
      #ip_hash;
      server localhost:8080;
  }

在server节点下添加如下配置:其中最后一项就是超时时间的参数,这里设置为10分钟。

location /websocket {
      proxy_pass http://localhost:8080;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_read_timeout 600s;
  }

还有前端连接url,服务器IP的问题,上面前端代码写的是localhost,是在本地调试的时候用的。在服务器上部署之后,不能是localhost,要写成对应的服务器IP地址和端口号。

Guess you like

Origin blog.csdn.net/nhx900317/article/details/119731779