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
相关产品推荐
相关产品推荐

