본문 바로가기
NestJS

[NestJS] nestJS 소켓통신 (part2. 채널마다 fetchSockets를 통한 메세지 보내주기)

by 개복취 2024. 2. 8.

 

 

0. 배경

1. 해결방법 


0. 배경

서로 구분되는 채널에다가 메세지를 보내줄 수 있도록 구현이 필요했었다.

 

https://socket.io/docs/v4/rooms/

 

처음에는 socket.io에서 지원하는 room 을 통해 채널id 내부에있는 소켓들에게 메세지를 보내주는 식으로 고안했는데.. 하나의 소켓이 다른 채널() 으로 참여할 때 기존의 룸에서 메세지를 보내도 다른 채널에서 메세지가 보이는(!) 문제가 발생했다.

 

그래서 아래와 같이 다른 채널에서 메세지를 보낼 때 구분하기 위해 channelId메세지 내용을 객체로 던져주는 방식을 생각했다.

const eventMessageEmitDto = {
    nickname: user.nickname,
    message,
    channelId,
};

 

이러한 과정중에 room으로 들어가기 위해 소켓을 가지고 와야하는 과정이 매끄럽지 않았다.

this.server.sockets.sockets 에서 get을 통해 소켓 전체의 객체를 가져오려고 했는데.. undefined으로 찍히고 소켓을 못가지고 오는 경우였다.

 

문제의 channels.gateway.ts

@SubscribeMessage('joinChannel')
async handlejoinChannel(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: { channelId: string; channelSocketId: string },) {
    const user = await this.authService.getUserFromSocket(client);

    if (!user || user.channelSocketId !== client.id) {
        throw WSBadRequestException('유저 정보가 일치하지 않습니다.');
    }

    const socket = this.server.sockets.sockets.get(data.channelSocketId);

    if (!socket) {
        throw WSBadRequestException('socket이 존재하지 않습니다.');
    }

    socket.join(data.channelId);
}

 

너무 궁금해서 this.server.sockets의 log를 찍어봤는데 아래와 같았다.

backend      |   <ref *1> Map(1) {
backend      |   'ej06MNkjv1F8qxnOAAAB' => Socket {
backend      |     _events: [Object: null prototype] {
backend      |       error: [Function: noop],
backend      |       disconnect: [Array],
backend      |       message: [Function: handler],
backend      |       ClientToServer: [Function: handler]
backend      |     },
backend      |     _eventsCount: 4,
backend      |     _maxListeners: undefined,
backend      |     nsp: Namespace {
backend      |       _events: [Object: null prototype],
backend      |       _eventsCount: 1,
backend      |       _maxListeners: undefined,
backend      |       sockets: [Circular *1],
backend      |       _fns: [],
backend      |       _ids: 0,
backend      |       server: [Server],
backend      |       name: '/channels',
backend      |       adapter: [Adapter],
backend      |       [Symbol(shapeMode)]: false,
backend      |       [Symbol(kCapture)]: false
backend      |     },
backend      |     client: Client {
backend      |       sockets: [Map],
backend      |       nsps: [Map],
backend      |       server: [Server],
backend      |       conn: [Socket],
backend      |       encoder: [Encoder],
backend      |       decoder: [Decoder],
backend      |       id: 'zmDDJmUHg6txOshvAAAA',
backend      |       onclose: [Function: bound onclose],
backend      |       ondata: [Function: bound ondata],
backend      |       onerror: [Function: bound onerror],
backend      |       ondecoded: [Function: bound ondecoded],
backend      |       connectTimeout: undefined
backend      |     },
backend      |     recovered: false,
backend      |     data: {},
backend      |     connected: true,
backend      |     acks: Map(0) {},
backend      |     fns: [],
backend      |     flags: {},
backend      |     server: Server {
backend      |       _events: [Object: null prototype] {},
backend      |       _eventsCount: 0,
backend      |       _maxListeners: undefined,
backend      |       _nsps: [Map],
backend      |       parentNsps: Map(0) {},
backend      |       parentNamespacesFromRegExp: Map(0) {},
backend      |       _path: '/socket.io',
backend      |       clientPathRegex: /^\\/socket\\.io\\/socket\\.io(\\.msgpack|\\.esm)?(\\.min)?\\.js(\\.map)?(?:\\?|$)/,
backend      |       _connectTimeout: 45000,
backend      |       _serveClient: true,
backend      |       _parser: [Object],
backend      |       encoder: [Encoder],
backend      |       opts: [Object],
backend      |       _adapter: [class Adapter extends EventEmitter],
backend      |       sockets: [Namespace],
backend      |       eio: [Server],
backend      |       httpServer: [Server],
backend      |       engine: [Server],
backend      |       [Symbol(shapeMode)]: false,
backend      |       [Symbol(kCapture)]: false
backend      |     },
backend      |     adapter: Adapter {
backend      |       _events: [Object: null prototype] {},
backend      |       _eventsCount: 0,
backend      |       _maxListeners: undefined,
backend      |       nsp: [Namespace],
backend      |       rooms: [Map],
backend      |       sids: [Map],
backend      |       encoder: [Encoder],
backend      |       [Symbol(shapeMode)]: false,
backend      |       [Symbol(kCapture)]: false
backend      |     },
backend      |     id: 'ej06MNkjv1F8qxnOAAAB',
backend      |     handshake: {
backend      |       headers: [Object],
backend      |       time: 'Fri Dec 08 2023 02:33:38 GMT+0000 (Coordinated Universal Time)',
backend      |       address: '::ffff:192.168.65.1',
backend      |       xdomain: false,
backend      |       secure: true,
backend      |       issued: 1702002818896,
backend      |       url: '/socket.io/?EIO=4&transport=websocket',
backend      |       query: [Object: null prototype],
backend      |       auth: {}
backend      |     },
backend      |     [Symbol(shapeMode)]: false,
backend      |     [Symbol(kCapture)]: false
backend      |   }
backend      | }

 

map 레퍼런스인 것 같은데, this.server.sockets.sockets.get() 이나 this.server.sockets.sockets.keys() 등 자바스크립트의 맵 자료구조 함수로 값을 가져오는게 불가능했다.

아마, this.server.sockets.sockets 자체가 undefined로 나오는 것을 보면, socket.io가 map 을 private으로 접근 불가하게 해놓은 것 같다.

socket.io의 버전마다 socket instance를 가져오는 방식이 다른 것 같다. ver3 이상에서는 this.server.sockets.sockets.get(socketId) 로 소켓을 가져오라고 한다.

 

버전은 "[socket.io](<http://socket.io/>)": "4.6.2"인데 TypeError: Cannot read properties of undefined (reading 'get') 에러가 나는 것으로 보아 이 방법은 안되는 것 같다.

 

해결

  • fetchSockets()를 이용한다.
  • socket.io의 fetchSockets 메서드는 서버에서 현재 연결된 모든 소켓(Socket)의 목록을 가져오는 기능을 제공한다. 이 메서드는 Socket.IO 서버 인스턴스에서 호출되며, 연결된 모든 소켓을 반환하는 Promise를 반환한다.
async joinChannelRoom(channelRoomName: string, channelSocketId: string) {
    this.logger.log(
        `joinChannelRoom: ${channelRoomName}, ${channelSocketId}`,
    );

    const sockets = await this.server.fetchSockets();

    const socket = sockets.find((s) => s.id === channelSocketId);

    if (!socket) {
        return WSBadRequestException('socket이 존재하지 않습니다.');
    }

    this.logger.log(`socket.id: `, socket.id);
    socket.join(channelRoomName);
}

 


https://docs.nestjs.com/websockets/gateways

https://velog.io/@fejigu/Socket.IO-client

https://socket.io/docs/v4/rooms/