You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring调用io()时onConnected()重复触发问题排查

Spring Boot Socket.IO 服务器连接事件重复触发问题

我用Spring Boot搭建了同时支持API和Socket.IO的服务器,前端搭配Angular。当前遇到的问题是:调用前端SocketService的connect方法时,Spring后端的onConnected方法会被触发两次。我尝试移除client.joinRoom(sessionCode)这类操作,但日志仍显示重复触发。

我怀疑问题出在客户端实现或者Socket.IO服务器配置上,试过给客户端添加重连事件、移除connect事件;也试用过Node.js服务器,功能正常但多应用通信过于复杂,因此希望在Spring环境中解决该问题。


相关代码

前端代码

Component 代码

import { Component, HostBinding, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { GameSessionService } from "../../services/game-session.service";
import { SocketService } from "../../services/socket.service";
import { GameSessionI } from "../../interfaces/GameSessionI";

@Component({
  selector: 'app-create-game',
  templateUrl: './create-game.page.html',
  styleUrl: './create-game.page.scss',
})
export class CreateGamePage implements OnInit {
  createGameForm: FormGroup;

  @HostBinding('class') class = 'tw-my-auto';
  colorPrimaryString: string = 'primary';
  colorGreyString: string = 'grey';
  username: string = '';

  constructor(fb: FormBuilder, private readonly gameSessionService : GameSessionService, private readonly socketService: SocketService) {
    this.createGameForm = fb.group({
      username: ['', Validators.required],
    });
  }

  ngOnInit(): void {}

  onSubmit() {
    this.gameSessionService.createGameSession(
      this.createGameForm.value.username,
    ).subscribe(
      (gameSession: GameSessionI) => {
        console.log(gameSession);
        this.socketService.connect(gameSession.code, gameSession.owner.id, gameSession.owner.name);
      },
    );
  }

  backButton(){
    this.socketService.disconnect();
  }
}

Service 代码

import { Injectable } from '@angular/core';
import { io } from 'socket.io-client';
import { LaunchSessionDTO } from '../model/socket/LaunchSessionDTO';
import { GameSessionService } from './game-session.service';

@Injectable({
  providedIn: 'root',
})
export class SocketService {
  socket: any;

  constructor(private readonly gameSessionService: GameSessionService) {
    
  }

  connect(sessionCode: string, userId: number, userName: string) {
    console.log(this.socket);
    if (!this.socket) {
      this.socket = io('http://localhost:9092', {
        transports: ['websocket'], // 指定传输方式以兼容
        query: { sessionCode, userId, userName }, // 握手数据
      });

      this.socket.on('connect', () => {
        console.log('Connected to socket server');
      });
      
      this.socket.on('disconnect', () => {
        console.log('Disconnected from socket server');
      });
    }
  }

  public launchSession(launchSessionDTO: LaunchSessionDTO) {
    this.socket.emit('launch_session', launchSessionDTO);
  }

  disconnect() {
    if (this.socket) {
      this.socket.disconnect();
    }
  }
}

Spring 后端代码

package com.miage.beeramideback.socket.gamesession;

// imports

@Component
public final class GameSessionSocketModule {
    private final GameSessionBasicEventSocketService gameSessionSocketService;
    private final GameSessionService gameSessionService;
    private final GameSessionManagementService gameSessionManagementService;
    private final SocketIOServer server;
    Logger log = LoggerFactory.getLogger(GameSessionManagementServiceImpl.class);

    // GameSessionSocketModule构造函数
    public GameSessionSocketModule(SocketIOServer server, GameSessionSocketServiceImpl gameSessionSocketServiceImpl, GameSessionManagementServiceImpl gameSessionManagementServiceImpl, GameSessionService gameSessionService) {
        this.server = server;
        this.gameSessionSocketService = gameSessionSocketServiceImpl;
        this.gameSessionManagementService = gameSessionManagementServiceImpl;
        this.gameSessionService = gameSessionService;
        // 添加会话事件监听器(响应客户端通过socket连接服务器后的操作)
        server.addConnectListener(this.onConnected());
        server.addDisconnectListener(this.onDisconnected());
    }

    // 玩家连接会话事件监听器
    private ConnectListener onConnected() {
        return (client) -> {
            final var params = client.getHandshakeData().getUrlParams();
            final String sessionCode = params.get("sessionCode").stream().collect(Collectors.joining());
            final Long userId = Long.parseLong(params.get("userId").stream().collect(Collectors.joining()));
            final String userName = params.get("userName").stream().collect(Collectors.joining());
            log.info("Socket connecting '{}'  ", client.getSessionId().toString());
        };
    }

    // 玩家断开会话事件监听器
    private DisconnectListener onDisconnected() {
        return client -> {
            final var params = client.getHandshakeData().getUrlParams();
            final Long userId = Long.parseLong(params.get("userId").stream().collect(Collectors.joining()));
            final String sessionCode = client.getHandshakeData().getSingleUrlParam("sessionCode");
            final String userName = params.get("userName").stream().collect(Collectors.joining());
            log.info("Client '{}'' - disconnected from socket", client.getSessionId().toString());
        };
    }
}

后端日志情况

每次调用前端connect方法时,后端日志会输出两条相同会话ID的"Socket connecting"记录。


排查方向与解决方案

1. 检查Spring组件是否重复注册

如果GameSessionSocketModule被Spring多次实例化,会导致ConnectListener被重复添加,触发两次连接事件:

  • 确认Spring Boot启动类的扫描范围,避免重复扫描同一个包;
  • 检查@ComponentScan注解是否存在重复的包路径配置;
  • 可以在GameSessionSocketModule构造函数中添加日志,验证是否被多次实例化:
public GameSessionSocketModule(...) {
    log.info("GameSessionSocketModule 实例化");
    // 原有代码
}

2. 修复客户端Socket实例管理问题

当前disconnect方法没有重置socket引用,可能导致旧实例残留触发重连:

disconnect() {
  if (this.socket) {
    this.socket.disconnect();
    this.socket = null; // 断开连接后重置引用
  }
}

3. 强制后端仅使用WebSocket传输

部分环境下Socket.IO会先尝试轮询再升级,导致两次连接触发,可在后端配置中禁用轮询:

@Configuration
public class SocketConfig {
    @Bean
    public SocketIOServer socketIOServer() {
        Configuration config = new Configuration();
        config.setHostname("localhost");
        config.setPort(9092);
        // 仅允许WebSocket传输
        config.setTransports(Transport.WEBSOCKET);
        return new SocketIOServer(config);
    }
}

4. 验证监听器是否重复添加

确保server.addConnectListener仅被调用一次,可在添加监听器前打印日志,确认调用次数。


内容的提问来源于stack exchange,提问作者elioteuteu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:14:56