vue使用socketIoClient连接socketIo服务端反复重连问题

最近在项目中使用到了socketIo,spring boot集成socketIo作为服务端,需要前端页面vue使用socketIoClient连接服务端并监听消息,结果在连接socketIo服务端的时候出现了反复连接的情况,当时这个问题卡住了一天时间,网上面关于这个的问题特别少,就问题描述及解决过程记录如下,以供参考。

简单介绍spring boot后端集成socketIo步骤:
添加依赖:

<dependency>
  	<groupId>com.corundumstudio.socketio</groupId>
    <artifactId>netty-socketio</artifactId>
    <version>1.7.17</version>
</dependency>

这儿使用的是netty-socketIo,netty-socketio是一个开源的Socket.io服务器端的一个java的实现,它基于Netty框架,可用于服务端推送消息给客户端。

application配置参数:

# SocketIO配置
socketIo:
  host: 0.0.0.0
  # SocketIO端口
  port: 8083
  # 连接数大小
  workCount: 100
  # 允许客户请求
  allowCustomRequests: true
  # 协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
  upgradeTimeout: 10000
  # Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
  pingTimeout: 60000
  # Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
  pingInterval: 25000
  # 设置HTTP交互最大内容长度
  maxHttpContentLength: 1048576
  # 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
  maxFramePayloadLength: 1048576

config配置代码:

/**logger*/
    private static final Logger logger = LoggerFactory.getLogger(SocketConfig.class);
    @Value("${socketIo.host}")
    private String host;

    @Value("${socketIo.port}")
    private Integer port;

    @Value("${socketIo.workCount}")
    private int workCount;

    @Value("${socketIo.allowCustomRequests}")
    private boolean allowCustomRequests;

    @Value("${socketIo.upgradeTimeout}")
    private int upgradeTimeout;

    @Value("${socketIo.pingTimeout}")
    private int pingTimeout;

    @Value("${socketIo.pingInterval}")
    private int pingInterval;

    @Value("${socketIo.maxFramePayloadLength}")
    private int maxFramePayloadLength;

    @Value("${socketIo.maxHttpContentLength}")
    private int maxHttpContentLength;

    /**
     * SocketIOServer配置
     */
    @Bean("socketIOServer")
    public SocketIOServer socketIOServer() {
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        //配置host
//        config.setHostname(host);
        //配置端口
        config.setPort(port);
        //开启Socket端口复用
        com.corundumstudio.socketio.SocketConfig socketConfig = new com.corundumstudio.socketio.SocketConfig();
        socketConfig.setReuseAddress(true);
        config.setSocketConfig(socketConfig);
        //连接数大小
        config.setWorkerThreads(workCount);
        //允许客户请求
        config.setAllowCustomRequests(allowCustomRequests);
        //协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
        config.setUpgradeTimeout(upgradeTimeout);
        //Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
        config.setPingTimeout(pingTimeout);
        //Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
        config.setPingInterval(pingInterval);
        //设置HTTP交互最大内容长度
        config.setMaxHttpContentLength(maxHttpContentLength);
        //设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
        config.setMaxFramePayloadLength(maxFramePayloadLength);
        config.setTransports(Transport.POLLING, Transport.WEBSOCKET);
        /*config.setOrigin("http://localhost:3000");*/
        return new SocketIOServer(config);
    }

    /**
     * 开启SocketIOServer注解支持
     */
    @Bean
    public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) {
        return new SpringAnnotationScanner(socketServer);
    }

然后是监听代码:

@Component
public class SocketHandler {
    /**logger*/
    private Logger logger = LoggerFactory.getLogger(SocketHandler.class);
    /**存已连接的客户端*/
    private Map<String, List<UUID>> clientMap = new ConcurrentHashMap<>(16);
    private final SocketIOServer socketIOServer;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    public SocketHandler(SocketIOServer socketIOServer) {
        this.socketIOServer = socketIOServer;
    }

    /**
     * 当客户端发起连接时调用
     * @param socketIOClient 客户端
     */
    @OnConnect
    public void onConnect(SocketIOClient socketIOClient) {
        //获取socketClient连接参数
        String userName = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_USER_NAME);
        String appKey = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.APP_KEY);
        String roomId = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_ROOM_ID);
        
        Map<String, Object> headers = new HashMap<>();
        for (Map.Entry<String, String> entry : socketIOClient.getHandshakeData().getHttpHeaders().entries()){
            headers.put(entry.getKey(), entry.getValue());
        }
        logger.info("header:"+ JSON.toJSONString(headers));
        //clientMap存放连接客户端信息
        if (StringUtils.isNotBlank(roomId)) {
            logger.info("用户{}开启长连接通知, roomId: {}, NettySocketSessionId: {}, NettySocketRemoteAddress: {}", userName, roomId, socketIOClient.getSessionId().toString(), socketIOClient.getRemoteAddress().toString());
            List<UUID> uuidList = new ArrayList<>();
            //clientMap-key为appKey与room编号组合
            String clientKey = StringUtils.concatStr(appKey, EwsCommonConstants.SystemParam.CON_SIGN, roomId);
            if(CollectionUtils.isNotEmpty(clientMap.get(clientKey))){
                uuidList = clientMap.get(clientKey);
            }
            uuidList.add(socketIOClient.getSessionId());
            clientMap.put(clientKey, uuidList);
            logger.info(JSON.toJSONString(clientMap));
            //加入房间
            socketIOClient.joinRoom(clientKey);
        }
    }

    /**
     * 客户端断开连接时调用,刷新客户端信息
     * @param socketIOClient 客户端
     */
    @OnDisconnect
    public void onDisConnect(SocketIOClient socketIOClient) {
        String userName = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_USER_NAME);
        String roomId = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.SOCKET_ROOM_ID);
        String appKey = socketIOClient.getHandshakeData().getSingleUrlParam(EwsCommonConstants.SystemParam.APP_KEY);
        if (StringUtils.isNotBlank(userName)) {
            logger.info("用户{}断开长连接通知, roomId: {}, NettySocketSessionId: {}, NettySocketRemoteAddress: {}",
                    userName, roomId, socketIOClient.getSessionId().toString(), socketIOClient.getRemoteAddress().toString());
            //移除客户端
            String clientKey = StringUtils.concatStr(appKey, EwsCommonConstants.SystemParam.CON_SIGN, roomId);
            for (String key : clientMap.keySet()){
                if (key.equals(clientKey)) {
                    //移除该房间内的client
                    clientMap.get(key).remove(socketIOClient.getSessionId());
                }
            }
        }
    }

    /**
     * 监听事件
     * @param socketIOClient 客户端
     * @param ackRequest ack请求
     * @param messageDto 消息主体
     */
    @OnEvent("ewsSocketMsg")
    public void ewsSocketMsg(SocketIOClient socketIOClient, AckRequest ackRequest,
                             MessageDto messageDto){
        String targetRoom = messageDto.getTargetRoom();
        clientMap.forEach((key, value) ->{
            //通过roomId获取
            if (key.contains(targetRoom)) {
                logger.info("ewsSocketMsg: 收到客户{}的消息,发送给{}房间,消息内容是{}", messageDto.getSourceUserName(), messageDto.getTargetRoom(), messageDto.getMsgContent());
                //判断房间内是否有client
                if (CollectionUtils.isNotEmpty(value)) {
                    //获得该房间内所有广播对象发送事件
                    socketIOServer.getRoomOperations(key).sendEvent("ewsSocketMsg", messageDto);
                }
            }
        });
    }

    /**
     * 服务端广播消息到所有客户端,自己除外
     * @param socketIOClient 客户端
     * @param ackRequest ack请求
     * @param messageDto 消息主体
     */
    @OnEvent("ewsAllClientsMsg")
    public void ewsAllClientsMsg(SocketIOClient socketIOClient, AckRequest ackRequest,
                                 MessageDto messageDto){
        logger.info("ewsAllClientsMsg:收到客户{}的消息,广播发送给其他客户,消息内容是{}",
                messageDto.getSourceUserName(), messageDto.getMsgContent());
        try {
            socketIOServer.getBroadcastOperations().sendEvent("ewsAllClientsMsg", messageDto);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 去掉clientMap的无效appKey
     * 断开socketClient连接
     * @param appKey 校验key
     */
    public void invalidWebSocket(String appKey) {
        logger.info("去掉无效appKey");
        clientMap.forEach((key, value) ->{
            if (key.contains(appKey)) {
                value.forEach(e -> {
                    //遍历该房间内的所有uuid,获取socketClient并断开连接
                    SocketIOClient socketIOClient = socketIOServer.getClient(e);
                    socketIOClient.disconnect();
                });
                clientMap.remove(key);
            }
        });
        logger.info("***无效appKey之后clientMap为: {}", JSON.toJSONString(clientMap));
    }

    public Map<String, List<UUID>> getClientMap() {
        return clientMap;
    }
    public void setClientMap(Map<String, List<UUID>> clientMap) {
        this.clientMap = clientMap;
    }
}

最后是socketIoServer启动类:

@Component
@Order(1)
public class SocketServer implements CommandLineRunner {
    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(ServerRunner.class);

    /**
     * socketIOServer
     */
    private final SocketIOServer socketIOServer;

    @Autowired
    public SocketServer(SocketIOServer socketIOServer) {
        this.socketIOServer = socketIOServer;
    }

    @Override
    public void run(String... args) {
        logger.info("---------- NettySocket通知服务开始启动 ----------");
        socketIOServer.start();
        logger.info("---------- NettySocket通知服务启动成功 ----------");
    }
}

以上为spring boot整合socketIo的步骤,作为WebSocket服务端使用。接下来介绍vue使用socket.io-client连接并监听服务端时间的实现:
引入client:import sio from ‘socket.io-client’
在methods内添加connect()方法,方法内为具体实现如下:

connect:function(){
    let opts = {
        query: 'userName=test&appKey=test&roomId=rabbit'
    };
    // socketIo连接的服务器信息,就是我们后端配置的信息
    let socket = sio.connect('http://localhost:8083?',opts);
    socket.on('connect', function () {
        console.log('websocket连接成功');
    });
    let that = this;
    socket.on('ewsSocketMsg', function (data) {
        console.log(data);
        that.messageStatus = "消息状态:" + data.msgContent;
    });
    socket.on('disconnect', function () {
        console.log('websocket已经下线');
    });
    /*socket.on('connect_error', (error) => {
        socket.close();
    });*/
}

在mounted内执行connect方法:

mounted() {
   this.connect();
}

在启动了socketIo服务端的情况下,请求vue页面即可触发connect方法完成连接并监听,但是实际场景vue客户端会一直连接socketIo服务端,找了好久没定位到问题。最后发现很可能是因为socket.io的握手机制导致的。socket.io在进行握手的时候默认采用的是polling轮询机制进行的,当失败时会持续发送握手请求。

解决方案
在opts内添加transport参数的定义即可:

let opts = {
    query: 'userName=test&appKey=test&roomId=rabbit',
    transports:['websocket']
};

——————————————
JAVA面试知识点相关:JAVA面试知识点
SpringAOP原理使用相关:SpringAOP原理使用详解

  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰红茶不会渴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值