12. Redis에 설문 참여자 추가 및 Room join

Hyeseong·2023년 3월 3일
1

실시간설문조사앱

목록 보기
2/6

🏛 구현 로직


🔖 들어가기 앞서

  • polls service와 polls repository에 대한 추가 수정과 구현을 통해 polls 모듈의 비즈니스로직과 REDIS에 데이터 저장이 어떻게 이루어지는지 구체적으로 알아볼게요.
  • poll(설문조사)의 기능중 중요한 특징 중 하나는 participants(참여자)들이 해당 poll에 속하게되어 설문조사가 이루어 진다는 점이에요.
    • 특히 웹소켓을 통해서 poll에 참여하는 것이 실시간으로 이루어지고 이러한 참여가 어떻게 이루어 지는지 살펴 볼게요.
    • 반대로 poll(설문조사)에 removed되는 로직 과정 또한 볼게요.(disconnted를 요청하거나 혹은 disconnted 되는 경우) 로직 처리
  • socket.io를 이용하여 특정 poll(room)에 어떻게 데이터를 전송하는지도 살펴볼게요.

🚀들어가기

1️⃣ polls.repository.ts

이 코드는 NestJS를 사용하여 구현된 polls repository의 일부분입니다.

  • PollsRepository 클래스는 ConfigService 및 Redis를 주입 받습니다.
  • Redis는 Redis 모듈에서 가져온 것이며, ConfigService는 응용 프로그램에서 환경 변수를 사용하는 데 사용됩니다.
  • PollsRepository 클래스에는 addParticipant 메서드가 있습니다. 이 메서드는 pollID, userID 및 name과 같은 데이터를 가져와 Redis에 저장하고 poll 객체를 반환합니다.
  • Redis 클라이언트를 사용하여 Redis에 JSON 데이터를 저장하고 검색합니다.
  • 이 코드는 추가 수정 및 구현을 통해 설문조사 모듈의 비즈니스 로직과 REDIS에 데이터 저장이 어떻게 이루어지는지 구체적으로 설명합니다. 특히, 설문조사에 참여하는 것이 어떻게 이루어지는지와 설문조사에서 제거되는 경우에 대한 로직 처리 방법도 설명합니다. 또한, 특정 설문조사에 데이터를 전송하는 방법도 설명합니다.

기존 코드

import {
  Inject,
  Injectable,
  InternalServerErrorException,
  Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Redis } from 'ioredis';
import { IORedisKey } from 'src/redis.module';
import { AddParticipantData, CreatePollData } from './types';
import { Poll } from 'shared';
import { error } from 'console';

@Injectable()
export class PollsRepository {
  // to use time-to-live from configuration
  private readonly ttl: string;
  private readonly logger = new Logger(PollsRepository.name);

  constructor(
    configService: ConfigService,
    @Inject(IORedisKey) private readonly redisClient: Redis,
  ) {
    this.ttl = configService.get('POLL_DURATION');
  }

  /**
   * Create a new poll in Redis and returns the newly created poll.
   * @param {CreatePollData} data - The data required to create a poll.
   * @param {number} data.votesPerVoter - The number of votes each voter is allowed to cast in the poll.
   * @param {string} data.topic - The topic of the poll.
   * @param {string} data.pollID - The ID of the poll.
   * @param {string} data.userID - The ID of the user creating the poll.
   * @returns {Promise<Poll>} - The newly created poll.
   * @throws {InternalServerErrorException} - Throws an error if creating the poll in Redis fails.
   */
  async createPoll({
    votesPerVoter, // The number of votes each voter is allowed.
    topic, // The topic of the poll.
    pollID, // The ID of the poll.
    userID, // The ID of the user creating the poll.
  }: CreatePollData): Promise<Poll> {
    // Create the initial poll object with the given data.
    const initialPoll = {
      id: pollID,
      topic,
      votesPerVoter,
      participants: {},
      adminID: userID,
    };

    // Log a message indicating that a new poll is being created with the given data.
    this.logger.log(
      `Creating new poll: ${JSON.stringify(initialPoll, null, 2)} with TTL ${
        this.ttl
      }`,
    );

    // Create a Redis key for the poll.
    const key = `polls:${pollID}`;

    try {
      // Use Redis MULTI/EXEC to perform multiple commands atomically.
      await this.redisClient
        .multi([
          // Set the value of the Redis key to the initial poll object.
          ['send_command', 'JSON.SET', key, '.', JSON.stringify(initialPoll)],
          // Set the TTL for the Redis key to the configured value.
          ['expire', key, this.ttl],
        ])
        .exec();
      // Return the initial poll object.
      return initialPoll;
    } catch (error) {
      // Log an error message if the poll could not be added to Redis.
      this.logger.error(
        `Failed to add poll ${JSON.stringify(initialPoll)}\n${error}`,
      );
      // Throw an internal server error exception to the calling function.
      throw new InternalServerErrorException();
    }
  }

  /**
   * Get a poll from Redis and returns the poll object.
   * @param {string} pollID - The ID of the poll to retrieve.
   * @returns {Promise<Poll>} - The poll object corresponding to the given ID.
   * @throws {Error} - Throws an error if retrieving the poll from Redis fails.
   */
  async getPoll(pollID: string): Promise<Poll> {
    // Log a message indicating that we are attempting to get the poll with the given ID.
    this.logger.log(`Attempting to get poll with: ${pollID}`);
    // Create a Redis key for the poll.
    const key = `polls:${pollID}`;

    try {
      // Use Redis JSON.GET to retrieve the value of the Redis key as a JSON string.
      const currentPoll = await this.redisClient.send_command(
        'JSON.GET',
        key,
        '.',
      );

      // Log the retrieved poll object in verbose mode.
      this.logger.verbose(currentPoll);

      // Parse the retrieved poll object from JSON and return it.
      return JSON.parse(currentPoll);
    } catch (error) {
      // Log an error message if the poll could not be retrieved from Redis.
      this.logger.error(`Failed to get ${pollID}`);
      // Throw the error to the calling function.
      throw error;
    }
  }

  /**
   * Adds a participant to a poll in Redis and returns the updated poll.
   * @param {AddParticipantData} data - The data required to add a participant to a poll.
   * @param {string} data.pollID - The ID of the poll.
   * @param {string} data.userID - The ID of the user to add as a participant.
   * @param {string} data.name - The name of the user to add as a participant.
   * @returns {Promise<Poll>} - The updated poll with the new participant added.
   * @throws {Error} - Throws an error if adding the participant to the poll fails.
   */
  async addParticipant({
    pollID,
    userID,
    name,
  }: AddParticipantData): Promise<Poll> {
    this.logger.log(
      `Attempting to add a participant with userID/name: ${userID}/${name} to pollID: ${pollID}`,
    );

    const key = `polls:${pollID}`;
    const participantPath = `.participants.${userID}`;

    try {
      // Use the JSON.SET command to set the value of a specific JSON path within a Redis key.
      // This command sets the name of the participant at the path ".participants.<userID>" within the poll object.
      await this.redisClient.send_command(
        'JSON.SET',
        key,
        participantPath,
        JSON.stringify(name),
      );

      const pollJSON = await this.redisClient.send_command(
        'JSON.GET',
        key,
        '.',
      );

      const poll = JSON.parse(pollJSON) as Poll;

      this.logger.debug(
        `Current Participants for pollID: ${pollID};)`,
        poll.participants,
      );
      return poll;
    } catch (error) {
      this.logger.error(
        `Failed to add a participant with userID/name: ${userID}/${name} to pollID: ${pollID}`,
      );
      throw error;
    }
  }

}

🛀🏻 변경 후

import {
  Inject,
  Injectable,
  InternalServerErrorException,
  Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Redis } from 'ioredis';
import { IORedisKey } from 'src/redis.module';
import {
  AddParticipantData,
  CreatePollData,
  RemoveParticipantData,
} from './types';
import { Poll } from 'shared';
import { error } from 'console';

@Injectable()
export class PollsRepository {
  // to use time-to-live from configuration
  private readonly ttl: string;
  private readonly logger = new Logger(PollsRepository.name);

  constructor(
    configService: ConfigService,
    @Inject(IORedisKey) private readonly redisClient: Redis,
  ) {
    this.ttl = configService.get('POLL_DURATION');
  }

  /**

  Create a new poll in Redis with the specified topic, votes per voter, poll ID, and admin ID.
  @param {CreatePollData} data - The data required to create a poll.
  @param {number} data.votesPerVoter - The number of votes allowed per voter.
  @param {string} data.topic - The topic of the poll.
  @param {string} data.pollID - The ID of the poll.
  @param {string} data.userID - The ID of the user creating the poll.
  @returns {Promise<Poll>} - The newly created poll.
  @throws {InternalServerErrorException} - Throws an error if creating the poll fails.
  */
  async createPoll({
    votesPerVoter,
    topic,
    pollID,
    userID,
  }: CreatePollData): Promise<Poll> {
    // Create initial poll object with specified details
    const initialPoll = {
      id: pollID,
      topic,
      votesPerVoter,
      participants: {},
      adminID: userID,
      hasStarted: false,
    };

    // Log creation of poll object
    this.logger.log(
      `Creating new poll: ${JSON.stringify(initialPoll, null, 2)} with TTL ${
        this.ttl
      }`,
    );

    // Set Redis key-value pair for the new poll with a specified time to live (TTL)
    const key = `polls:${pollID}`;
    try {
      await this.redisClient
        .multi([
          ['send_command', 'JSON.SET', key, '.', JSON.stringify(initialPoll)],
          ['expire', key, this.ttl],
        ])
        .exec();
      return initialPoll;
    } catch (error) {
      // Log and throw error if creation of poll fails
      this.logger.error(
        `Failed to add poll ${JSON.stringify(initialPoll)}\n${error}`,
      );
      throw new InternalServerErrorException();
    }
  }

  /**

  Get an existing poll from Redis.
  @param {string} pollID - The ID of the poll.
  @returns {Promise<Poll>} - The poll retrieved from Redis.
  @throws {Error} - Throws an error if getting the poll from Redis fails.
  */
  async getPoll(pollID: string): Promise<Poll> {
    // Log attempt to retrieve poll
    this.logger.log(`Attempting to get poll with: ${pollID}`);

    // Retrieve poll from Redis using pollID
    const key = `polls:${pollID}`;
    try {
      const currentPoll = await this.redisClient.send_command(
        'JSON.GET',
        key,
        '.',
      );
      this.logger.verbose(currentPoll);
      return JSON.parse(currentPoll);
    } catch (error) {
      // Log and throw error if retrieval of poll fails
      this.logger.error(`Failed to get ${pollID}`);
      throw error;
    }
  }

  /**
   * Add a participant to a poll.
   *
   * @param {AddParticipantData} data - Object containing the pollID, userID, and name of the participant to add.
   * @returns {Promise<Poll>} The updated poll object with the added participant.
   */
  async addParticipant({
    pollID,
    userID,
    name,
  }: AddParticipantData): Promise<Poll> {
    // Log that we are attempting to add a participant to the poll with the provided userID and name.
    this.logger.log(
      `Attempting to add a participant with userID/name: ${userID}/${name} to pollID: ${pollID}`,
    );

    // Generate the Redis key and participant path strings for the poll and participant, respectively.
    const key = `polls:${pollID}`;
    const participantPath = `.participants.${userID}`;

    try {
      // Use the JSON.SET command to set the value of a specific JSON path within a Redis key.
      // This command sets the name of the participant at the path ".participants.<userID>" within the poll object.
      await this.redisClient.send_command(
        'JSON.SET',
        key,
        participantPath,
        JSON.stringify(name),
      );
      // Get an existing poll from Redis
      return this.getPoll(pollID);
    } catch (error) {
      // If an error occurred while adding the participant, log the error and re-throw it.
      this.logger.error(
        `Failed to add a participant with userID/name: ${userID}/${name} to pollID: ${pollID}`,
      );
      throw error;
    }
  }

  /**
  Remove a participant from a poll in Redis and returns the updated poll.
  @param {RemoveParticipantData} data - The data required to remove a participant from a poll.
  @param {string} data.pollID - The ID of the poll.
  @param {string} data.userID - The ID of the user to remove from the poll participants.
  @returns {Promise<Poll>} - The updated poll with the participant removed.
  @throws {Error} - Throws an error if removing the participant from the poll fails.
  */
  async removeParticipant({
    pollID,
    userID,
  }: RemoveParticipantData): Promise<Poll> {
    // Log the removal of the participant.
    this.logger.log(`removing userID: ${userID} from poll: ${pollID}`);

    // Construct the Redis key and JSON path for the participant.
    const key = `polls:${pollID}`;
    const participantPath = `.participants.${userID}`;

    try {
      // Remove the participant from the poll using the Redis JSON.DEL command.
      await this.redisClient.send_command('JSON.DEL', key, participantPath);

      // Return the updated poll without the removed participant.
      return this.getPoll(pollID);
    } catch (error) {
      // Log an error if the participant removal fails.
      this.logger.error(
        `Failed to remove userID: ${userID} from poll: ${pollID}`,
        error,
      );
    }
  }

}

removeParticipant

1) 특정 poll의 특정 유저를 remove하기 위한 repository의 메서드입니다.
2) 파라미터는 객체로 넘겨 받으며 이를 object destructring을 통해 각각 pollID, userID로 처리하며 Promise Poll 객체를 반환하게 됩니다.
3) redisClient의 send_command 메서드를 이용하여 특정 poll의 키에 접근하고 더불어 특정 참가자를 제거 하기 위한 값을 seond_command메서드의 매개변수로 각각 넘겨주어 호출합니다.
4) 정상적으로 로직이 수행 완료되면 PollsRepository의 getPoll 메서드에 pollID를 호출하여 반환값인 Poll 객체를 반환하여줍니다.
5) 만약 오류가 발생하면 catch 구문에서 error로그로 남기도록 합니다.

  /**
  Remove a participant from a poll in Redis and returns the updated poll.
  @param {RemoveParticipantData} data - The data required to remove a participant from a poll.
  @param {string} data.pollID - The ID of the poll.
  @param {string} data.userID - The ID of the user to remove from the poll participants.
  @returns {Promise<Poll>} - The updated poll with the participant removed.
  @throws {Error} - Throws an error if removing the participant from the poll fails.
  */
  async removeParticipant({
    pollID,
    userID,
  }: RemoveParticipantData): Promise<Poll> {
    // Log the removal of the participant.
    this.logger.log(`removing userID: ${userID} from poll: ${pollID}`);

    // Construct the Redis key and JSON path for the participant.
    const key = `polls:${pollID}`;
    const participantPath = `.participants.${userID}`;

    try {
      // Remove the participant from the poll using the Redis JSON.DEL command.
      await this.redisClient.send_command('JSON.DEL', key, participantPath);

      // Return the updated poll without the removed participant.
      return this.getPoll(pollID);
    } catch (error) {
      // Log an error if the participant removal fails.
      this.logger.error(
        `Failed to remove userID: ${userID} from poll: ${pollID}`,
        error,
      );
    }
  }

addParticipant 메소드 수정

이미 구현된 getPoll 메서드를 사용하여 약 10줄의 코드를 1줄로 대체 할 수 있습니다.

async addParticipant
//생략

//변경전 --------------------
   const pollJSON = await this.redisClient.send_command(
        'JSON.GET',
        key,
        '.',
      );

      const poll = JSON.parse(pollJSON) as Poll;

      this.logger.debug(
        `Current Participants for pollID: ${pollID};)`,
        poll.participants,
      );
      return poll;
// 변경 후 ---------------------
      // Get an existing poll from Redis
      return this.getPoll(pollID);
// ----------------------------
    } catch (error) {
// 생략

2️⃣ poll-type.ts

  • hasStarted 키는 웹소켓을 통해서 실시간으로 poll에 참여한 모두에게 poll의 상태를 알려줍니다.
  • adminID는 해당 키를 갖고 있는 관리자만이 다른 참여자를 강제로 poll에서 제거 할 수 있는 권한을 갖고 이를 행사 할 수 있게 합니다.
// 이 인터페이스는 참가자 객체의 구조를 정의합니다. 참가자 객체는
// 각 키가 참가자 ID이고 각 값이 참가자의 이름인 객체입니다.
export interface Participants {
  [participantID: string]: string;
}

// 이 인터페이스는 poll 객체의 구조를 정의합니다. poll 객체는 여러 속성을 갖습니다:
// - id: poll의 ID를 나타내는 문자열
// - topic: poll의 주제를 나타내는 문자열
// - votesPerVoter: 각 투표자가 행사할 수 있는 투표 수를 나타내는 숫자
// - participants: poll에 참가하는 참가자를 포함하는 객체
// - adminID: poll 관리자의 ID를 나타내는 문자열
// - hasStarted: poll이 시작되었는지 여부를 나타내는 bool 값
export interface Poll {
  id: string;ㄹㄹ
  topic: string;
  votesPerVoter: number;
  participants: Participants;
  adminID: string;
  hasStarted: boolean;
  // 아래 속성은 현재 주석 처리되어 있지만, 나중에 추가될 수 있습니다:
  // - nominations: poll의 후보자를 포함하는 객체
  // - rankings: poll의 순위를 포함하는 객체
  // - results: poll 결과를 포함하는 객체
}
  • 위 poll interface 변경에 따라 해당 타입을 사용하는 메서드를 수정해야 합니다.

polls.repository.ts

  • cretePoll 메서드의 내의 initialPoll 객체에 hasStarted 키를 추가하고 값은 false로 지정합니다. 즉, 처음 poll이 생성 되었을때는 당연히 시작 상태가 아닌 것으로 객체가 만들어 지는 겁니다.
  • 더불어 poll-type.ts 모듈에서 명세한 Poll 인터페이스를 이용하여 createPoll메서드의 반환값을 Promise로 하여 initialPoll이 반환 될 수 있습니다.
  async createPoll({
    votesPerVoter,
    topic,
    pollID,
    userID,
  }: CreatePollData): Promise<Poll> {
    // Create initial poll object with specified details
    const initialPoll = {
      id: pollID,
      topic,
      votesPerVoter,
      participants: {},
      adminID: userID,
      hasStarted: false,
    };

3️⃣ polls -> types.ts & service & repository

  • polls.repository.ts 모듈과 polls.service.ts 모듈의 addParticipant, removeParticipant 메서드의 매개변수의 타입 지정을 위해서 아래와 같이 명세하여 줍니다.
  • 아래는 types.ts 모듈의 추가 명세 내역입니다.
// 생략 
  
export interface AddParticipantFields {
  pollID: string;
  userID: string;
  name: string;
}
export interface RemoveParticipantFields {
  pollID: string;
  userID: string;
}
// 생략 

polls.service.ts

  • addParticipant메서드의 매개변수 객체의 타입으로 지정합니다
  • removeParticipant 메서드의 매개변수 객체의 타입으로 동일하게 지정합니다.
    • 여기서 poll객체의 hasStarted 속성이 true라면 해당 poll이 이미 시작된 상태라면 void를 반환하도록 합니다.
  // 생략 
  
  async addParticipant(addParticipant: AddParticipantFields): Promise<Poll> {
    return this.pollsRepository.addParticipant(addParticipant);
  }

  async removeParticipant(
    removeParticipant: RemoveParticipantFields,
  ): Promise<Poll | void> {
    const poll = await this.pollsRepository.getPoll(removeParticipant.pollID);

    if (!poll.hasStarted) {
      const updatedPoll = await this.pollsRepository.removeParticipant(
        removeParticipant,
      );
      return updatedPoll;
    }
  }
  
  // 생략 

4️⃣ polls -> gateway.ts

변경 전

아래 코드 내용 요약

  • 이 코드는 @nestjs/websockets 모듈을 사용하여 WebSocket 게이트웨이를 구현한 TypeScript 코드입니다. PollsGateway 클래스는 OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect 인터페이스를 구현하고 있으며, @WebSocketGateway 데코레이터를 사용하여 "polls" 네임스페이스로 WebSocket 서버를 생성합니다.

  • ValidationPipe과 WsCatchAllFilter를 사용하여 WebSocket 요청을 유효성 검사하고 예외 처리합니다. PollsService를 주입받아 생성자에서 사용합니다.

  • handleConnection 메서드는 클라이언트가 WebSocket 서버에 연결되면 호출되며, 연결된 클라이언트의 ID, poll ID 및 이름을 로깅합니다. 이 메서드는 또한 연결된 모든 클라이언트에게 "hello" 메시지를 보냅니다.

  • handleDisconnect 메서드는 클라이언트가 WebSocket 서버에서 연결을 해제하면 호출됩니다. 이 메서드는 연결이 해제된 클라이언트의 ID, poll ID 및 이름을 로깅하고, 남아 있는 모든 클라이언트에게 "participants_updated" 메시지를 보냅니다.

  • test 메서드는 예외 처리를 위해 BadRequestException을 던지는 테스트용 WebSocket 메시지 핸들러입니다.

import { BadRequestException, Logger, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer, Namespace, UseFilters, UsePipes, } from '@nestjs/websockets';
import { ValidationPipe } from '@nestjs/common';
import { WsCatchAllFilter } from './ws-catch-all.filter';
import { PollsService } from './polls.service';
import { SocketWithAuth } from './interfaces/socket-with-auth.interface';

// Use the ValidationPipe for validating incoming messages
@UsePipes(new ValidationPipe())
// Use the WsCatchAllFilter to catch all WebSocket exceptions
@UseFilters(new WsCatchAllFilter())
// Declare this class as a WebSocket Gateway with the namespace "polls"
@WebSocketGateway({
  namespace: 'polls',
})
export class PollsGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  // Create a Logger instance for this class
  private readonly logger = new Logger(PollsGateway.name);

  constructor(private readonly pollsService: PollsService) {}

  // Declare that the "io" property is a WebSocketServer instance
  @WebSocketServer() io: Namespace;

  /**
   * Called when the gateway is initialized.
   * Provides logging to indicate that the WebSocket Gateway has been initialized.
   */
  afterInit(): void {
    this.logger.log(`Websocket Gateway initialized.`);
  }

  /**
   * Called when a client connects to the WebSocket Gateway.
   * Logs the user ID, poll ID, and name of the connected socket, as well as the socket ID.
   * Sends a "hello" message to all connected clients.
   * @param client The connected socket client
   */
  handleConnection(client: SocketWithAuth) {
    const sockets = this.io.sockets;
    this.logger.debug(
      `Socket connected with userID: ${client.userID}, pollID: ${client.pollID}, and name: "${client.name}"`,
    );
    this.logger.log(`WS Client with id: ${client.id} connected!`);
    this.logger.debug(`Number of connected sockets: ${sockets.size}`);

    // Send a "hello" message to all connected clients
    this.io.emit('hello', `from ${client.id}`);
  }

  /**
   * Called when a client disconnects from the WebSocket Gateway.
   * Logs the user ID, poll ID, and name of the disconnected socket, as well as the socket ID.
   * Sends a "participants_updated" message to all remaining clients.
   * @param client The disconnected socket client
   */
  handleDisconnect(client: SocketWithAuth) {
    const sockets = this.io.sockets;
    this.logger.debug(
      `Socket connected with userID: ${client.userID}, pollID: ${client.pollID}, and name: "${client.name}"`,
    );
    this.logger.log(`Disconnected socket id: ${client.id}`);
    this.logger.debug(`Number of connected sockets: ${sockets.size}`);

    // TODO - remove client from poll and send `participants_updated` event to remaining clients
  }

  /**
   * A test WebSocket message handler that throws a BadRequestException.
   * Used to test exception handling with the WsCatchAllFilter.
   * @returns {Promise<void>}
   */
  @SubscribeMessage('test')
  async test() {
    throw new BadRequestException('plain ol');
  }
}  

변경 후

  • handleConnection 메서드는 클라이언트가 WebSocket에 연결될 때 호출됩니다. pollID를 기반으로 클라이언트를 방에 참여시키고, 클라이언트를 투표 참가자로 추가하고, 방의 모든 클라이언트에게 poll_updated 이벤트를 발생시킵니다.

  • handleDisconnect 메서드는 클라이언트가 WebSocket에서 연결을 해제 할 때 호출됩니다. 클라이언트를 투표에서 제거하고 방의 모든 클라이언트에게 poll_updated 이벤트를 발생시킵니다.

  • SubscribeMessage 데코레이터를 사용하여 'test' 메시지를 처리하고 BadRequestException을 throw합니다.

5️⃣ Testing with postman

1. POST 요청을 통하여 poll 하나를 생성하여 줍니다.

2. {{base_url}}/polls/join API를 통하여 여러 유저를 만들어 줍니다.

3. socket.io를 통하여 연결합니다.

clinet1~3 각각 연결을 시도합니다.

  • 클라이언트2 연결이 끊길 경우 다른 클라이언트는 해당 poll의 참여자의 접속 여부를 실시간으로 확인할 수 있습니다.
profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글