Socket.io+Angular聊天应用切换房间后消息重复显示求助
实时聊天应用消息重复显示问题分析与解决
问题描述
使用Socket.io和Angular开发实时聊天应用,用户登录后建立连接、进入房间可正常收发消息,但切换房间后发送消息会重复显示,次数随切换次数递增(第一次切换后显示2次,第二次3次,以此类推),即使取消组件内所有订阅仍无法解决。
核心原因
- 服务端事件重复绑定:服务端在
join_room事件内部绑定send_message监听器,每次用户加入新房间时都会新增一个send_message的监听回调,发送消息时所有已绑定的回调都会触发,导致多次推送new_message事件。 - 房间未真正退出:服务端
leave_room事件仅打印日志,未执行socket.leave(room)操作,socket仍留在原房间,且之前绑定的send_message监听器未被移除。 - BehaviorSubject状态残留:SocketService中的
message$是单例BehaviorSubject,组件销毁后订阅取消,但BehaviorSubject的状态保留,切换房间后新订阅会接收历史推送,叠加新消息导致重复。
解决方案
1. 修复服务端Socket事件逻辑
将send_message事件移出join_room内部避免重复绑定,同时完善leave_room操作让socket真正退出房间:
const axios = require("axios"); const socketFunctions = (io, socket) => { console.log("new connection " + socket.id); // 将send_message事件移到外部,仅绑定一次 socket.on("send_message", (data) => { // 推送到目标房间内的其他用户 socket.to(data.room).emit("new_message", { poster: data.poster, content: data.content, room: data.room, date: Date.now(), }); // 给自己推送消息(如果需要) socket.emit("new_message", { poster: data.poster, content: data.content, room: data.room, date: Date.now(), }); }); socket.on("login", data => { const { email, password } = data; axios .post("http://localhost:3001/api/v1/chatApp/auth/login", { email, password, }) .then(res => { loadEvents(socket); }) .catch(err => console.log(err)); }); }; const loadEvents = (socket) => { socket.on("join_room", room => { socket.join(room); console.log("user joined the room" + room); }); socket.on("leave_room", room => { socket.leave(room); // 执行真正的退出房间操作 console.log("user left the room" + room); }); socket.on("connect_error", err => { console.log(`connect_error due to ${err.message}`); }); socket.on("end", function () { socket.disconnect(0); }); socket.on("disconnect", () => { console.log("Disconnected"); }); }; module.exports = socketFunctions;
2. 优化SocketService切换房间逻辑
新增切换房间方法,确保先离开原房间再加入新房间,可选重置BehaviorSubject状态:
export class SocketService { URL = environment.BASE_SERVER_URL API_MESSAGE = environment.MESSAGE_URL; API_ROOMS = environment.GET_SINGLE_ROOM_BY_ID; socket!: any; currentUserID!:string; message$ = new BehaviorSubject<any>(null); // 初始值设为null constructor( private http: HttpClient, private userService: UserService) { } ioConnect(email:string,password:string){ this.socket = io(this.URL); this.socket.emit('login', {email,password}) this.socket.on('new_message', (data:Message) => { this.message$.next(data) }) } // 新增切换房间方法 switchRoom(oldRoom: string | undefined, newRoom: string | undefined){ if(oldRoom){ this.onLeaveRoom(oldRoom); } if(newRoom){ this.onJoinRoom(newRoom); } } // 重置消息主题,避免历史数据干扰 resetMessageSubject(){ this.message$.next(null); } // 其他原有方法保持不变 getCurrentUserID(){ this.currentUserID = localStorage.getItem('userID') || ''; } onSendMessage(msg:Message):void{ this.socket.emit('send_message', msg); } onJoinRoom(room:string|undefined){ if(!room) return; this.socket.emit('join_room', room) } onLeaveRoom(room:string){ this.socket.emit('leave_room', room) } getMessageObservable(): Observable<Message> { return this.message$.asObservable() } SendMessageToDB(data:Message){ return this.http.post<Message>(this.API_MESSAGE, data) .pipe( tap((data:any)=> { const { _id, room } = data.newMessage this.sendMessageToRoom(_id, room) })) } sendMessageToRoom(msgID:string, msgRoom:string){ return this.http.patch<Message>(this.API_ROOMS + msgRoom,{ "message": msgID }) .subscribe() } socketOut(){ this.socket.emit('end'); } }
3. 组件中完善房间切换流程
在路由参数变化时执行房间切换操作,销毁组件时清理资源:
export class ChatDetailComponent implements OnInit, OnDestroy { @ViewChild('formMessage') input!: ElementRef; room!:Room; roomID!:string; currentUser!:User; penFriend!:User; penFriendID!:any; messages!:Message[] | undefined; formMessage!: string; msgObject!: Message; socketSub!:Subscription; currentUserSub!:Subscription; chatSub!: Subscription; penFriendSub!:Subscription; sendDBSub!:Subscription; private previousRoomID: string | undefined; constructor( private chatService: ChatService, private route:ActivatedRoute, private userService: UserService, private router:Router, private formBuilder: FormBuilder, private socketService: SocketService ) { } ngOnInit(): void { this.initRoom() this.socketSub = this.socketService.getMessageObservable() .subscribe((data:Message) => { if(!data || !this.messages)return this.messages.push(data) }) } initRoom(){ this.route.params .subscribe((params: Params)=>{ if(!params['id']) return; // 切换房间时先离开之前的房间 this.socketService.switchRoom(this.previousRoomID, params['id']); this.previousRoomID = params['id']; this.roomID = params['id'] this.initUser() }) } // 其他原有方法保持不变 initUser(){ this.currentUserSub = this.userService.getUserFromLocalStorage() .subscribe((user:any) => { this.currentUser = user.user; this.getRoom() }) } getRoom(){ this.chatSub = this.chatService.getSingleRoom(this.roomID) .subscribe((room:any)=>{ this.room = room.room this.getPenFriend() }) } getPenFriend(){ this.penFriendID = this.room.users?.find((userID:any) => userID !== this.currentUser._id); this.penFriendSub = this.userService.getUserByID(this.penFriendID) .subscribe((data:any) => { this.penFriend = data.user; this.initMsg() }) } initMsg(){ this.messages = this.room.chat; } onSend(msg:string){ if(!msg || !this.roomID || !this.currentUser._id)return ; this.msgObject = {content: msg, room: this.roomID, poster:this.currentUser._id}; this.socketService.onSendMessage(this.msgObject); this.socketService.SendMessageToDB(this.msgObject) .subscribe(); this.input.nativeElement.value = ''; } onExit(){ this.router.navigateByUrl('/chats'); this.socketService.onLeaveRoom(this.roomID); } ngOnDestroy(): void { this.socketSub.unsubscribe(); this.currentUserSub.unsubscribe(); this.chatSub.unsubscribe(); this.penFriendSub.unsubscribe(); // 清理当前房间订阅与消息主题 this.socketService.onLeaveRoom(this.roomID); this.socketService.resetMessageSubject(); } }
额外说明
- 服务端推送消息时,使用
socket.to(room).emit给房间内其他用户发消息,单独用socket.emit给自己发消息,避免重复推送; - 每次进入新房间时,确保
messages数组从room.chat重新初始化,避免残留旧房间的消息数据。
内容的提问来源于stack exchange,提问作者TEISSIER Nathan
相关产品推荐
相关产品推荐

