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

时间:2024-11-07 21:34:34

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

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

<dependency>
  	<groupId></groupId>
    <artifactId>netty-socketio</artifactId>
    <version>1.7.17</version>
</dependency>

这儿使用的是netty-socketIo,netty-socketio是一个开源的服务器端的一个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 = ();
    @Value("${}")
    private String host;

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

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

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

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

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

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

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

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

    /**
     * SocketIOServer配置
     */
    @Bean("socketIOServer")
    public SocketIOServer socketIOServer() {
         config = new ();
        //配置host
//        (host);
        //配置端口
        (port);
        //开启Socket端口复用
         socketConfig = new ();
        (true);
        (socketConfig);
        //连接数大小
        (workCount);
        //允许客户请求
        (allowCustomRequests);
        //协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
        (upgradeTimeout);
        //Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
        (pingTimeout);
        //Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
        (pingInterval);
        //设置HTTP交互最大内容长度
        (maxHttpContentLength);
        //设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
        (maxFramePayloadLength);
        (, );
        /*("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 = ();
    /**存已连接的客户端*/
    private Map<String, List<UUID>> clientMap = new ConcurrentHashMap<>(16);
    private final SocketIOServer socketIOServer;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    public SocketHandler(SocketIOServer socketIOServer) {
         = socketIOServer;
    }

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

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

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

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

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

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

最后是socketIoServer启动类:

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

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

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

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

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

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

在mounted内执行connect方法:

mounted() {
   ();
}

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

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

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

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