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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 10:20:25