dh_0e

[Node.js] 강의 내용 정리(4) (서버 로직 개발) + 개인 과제 본문

내일배움캠프/Node.js[심화]

[Node.js] 강의 내용 정리(4) (서버 로직 개발) + 개인 과제

dh_0e 2024. 6. 14. 16:47

1. 데이터 테이블 로드

  • 파일 시스템을 사용하여 서버에서 필요한 데이터 테이블을 메모리에 로드할 수 있음
  • 파일 시스템(file system) - Node.js의 fs 모듈은 파일 시스템에 접근하고, 파일을 읽고 쓰는 기능을 제공함
  • 동기적 및 비동기적 방식 모두로 파일 I/O 작업을 수행하며 CRUD 작업을 할 수 있음
  • 다양한 형태의 파일 기반 작업을 가능하게 해줌
  • DB(DataBase), CDN(Cloud Delivery Network), File 등으로 테이블을 관리하며 file이 가장 간편함

 

2. 유저 접속 관리

  • 유저가 서버에 웹소켓 프로토콜을 통해 접속을 하면 소켓 아이디가 발급됨
  • 현재 유저가 서버에 접속해있다는 상태를 저장하기 위해서 웹소켓에서 데이터를 주고받기 위해 존재함
  • 소켓 id는 임시적이어서 접속이 끊기면 사라지므로 이를 통해 유저를 특정할 수는 없으며 uuid로 유저를 특정해야함

 

3. 커넥션 핸들러

  • 게임 기획 단계에서 설정한 내용을 토대로 커넥션에 관한 핸들러를 기획, 완성함
// helper.js

export const handleDisconnect = (socket, uuid) => {
  removeUser(uuid);
  clearStage(uuid);
  clearItem(uuid);
  console.log('User disconnected: ' + uuid);
  console.log('Current users: ', getUser());
};

export const handleConnection = (socket, uuid) => {
  console.log(`New user connected: ${uuid} with socket ID ${socket.id}`);
  console.log('Current users: ', getUser());

  createStage(uuid);
  createItem(uuid);
  // 본인의 소켓에 보내는 것
  socket.emit('connection', { uuid });
};
// register.handler.js

import { addUser } from '../models/user.model.js';
import { v4 as uuidv4 } from 'uuid';
import { handleConnection, handleDisconnect, handleEvent } from './helper.js';

const registerHandler = (io) => {
  // 서버에 접속한 모든 유저를 대상한 이벤트
  io.on('connection', (socket) => {
    const userUUID = uuidv4();
    addUser({ uuid: userUUID, socketId: socket.id });

    handleConnection(socket, userUUID);
    socket.on('event', (data) => handleEvent(io, socket, data));

    // 하나의 유저를 대상으로 한 이벤트
    socket.on('disconnect', (socket) => {
      handleDisconnect(socket, userUUID);
    });
  });
};

export default registerHandler;
  • 서버에서 유저를 위한 데이터를 생성해 저장할 수 있어야 함

 

4. 이벤트 핸들러

  • 게임 기획 단계에서 구상한 이벤트들을 핸들러로 구성

event handlers

game.handler.js

  • 게임 시작, 게임 종료 시 초기화 및 최고점수 갱신이 있으며 이전에 데이터 검증이 포함되어 있음
import { getGameAssets, highScoreRenewal } from '../init/assets.js';
import { clearItem, getItem } from '../models/item.model.js';
import { clearStage, getStage, setStage } from '../models/stage.model.js';

export const gameStart = (uuid, payload) => {
  const { stages } = getGameAssets();
  clearStage(uuid);
  clearItem(uuid);
  setStage(uuid, stages.data[0].id, payload.timestamp);
  console.log('Stage: ', getStage(uuid));

  return { status: 'success' };
};

export const gameEnd = (uuid, payload) => {
  const { items, stages, highScore } = getGameAssets();
  console.log(uuid, payload);
  const { timestamp: gameEndTime, score } = payload;
  const myStages = getStage(uuid);
  if (!myStages.length) {
    return { status: 'fail', message: 'No stage found for user' };
  }

  let totalScore = 0;

  // 스테이지 시간별 점수 추가
  myStages.forEach((stage, index) => {
    console.log('sdf', stage, index);
    let stageEndTime;
    if (index === myStages.length - 1) {
      stageEndTime = gameEndTime;
    } else {
      stageEndTime = myStages[index + 1].timestamp;
    }
    const scorePerSecond = stages.data.find(function (val) {
      return val.id === stage.id;
    }).scorePerSecond;
    const statgeDuration = ((stageEndTime - stage.timestamp) / 1000) * scorePerSecond;
    totalScore += statgeDuration; // *(stage.id%10+1); 1초당 1점 stage 모두 동일
  });
  // 아이템 점수 추가
  const ateItems = getItem(uuid);
  for (let item of ateItems) {
    totalScore += items.data.find(function (val) {
      return val.id === item.itemId;
    }).score;
  }
  // 점수와 타임스탬프 검증
  // 오차범위 5
  console.log(score, totalScore);
  if (Math.abs(score - totalScore) > 5) {
    return { status: 'fail', message: 'Score verification failed' };
  }

  //DB에 저장한다고 가정한다면 여기서 저장
  //setResult(userId, score, timestamp)
  if (highScore.highScore < totalScore) {
    highScoreRenewal(totalScore);
    return {
      status: 'success',
      message: `Game ended`,
      broadcast: `${uuid} has renew the highest score`,
      highScore: totalScore,
    };
  }
  return { status: 'success', message: `Game ended` };
};

 

handlerMapping.js

  • moveStageHandler - 스테이지 이동
  • gameStart - 게임 시작
  • gameEnd - 게임 종료
  • eatItem - 아이템 획득
import { moveStageHandler } from './stage.handler.js';
import { gameStart, gameEnd } from './game.handler.js';
import { eatItem } from './item.handler.js';

const handlerMappings = {
  11: moveStageHandler,
  2: gameStart,
  3: gameEnd,
  4: eatItem,
};

export default handlerMappings;

helper.js

  • broadcast는 광역적으로 모든 사용자에게 이벤트를 보낸다.
// helper.js

export const handleEvent = (io, socket, data) => {
  if (!CLIENT_VERSION.includes(data.clientVersion)) {
    socket.emit('response', { status: 'fail', message: 'Client version mismatched' });
    return;
  }

  const handler = handlerMappings[data.handlerId];
  if (!handler) {
    socket.emit('response', { status: 'fail', message: 'Handler not found' });
    return;
  }

  const response = handler(data.userId, data.payload);

  if (response.broadcast) {
    io.emit('response', response);
    return;
  }

  socket.emit('response', response);
};

 

item.handler.js

  • 아이템을 획득시 획득 정보를 uuid를 key값으로 저장하고 아이템이 현재 스테이지에 나올 수 있는지 검증함
  • + 아이템 획득 패킷을 보내서 어뷰징으로 점수를 얻는 행위 또한 검증
import { getGameAssets } from '../init/assets.js';
import { addItem, getItem } from '../models/item.model.js';
import { getStage } from '../models/stage.model.js';

export const eatItem = async (userId, payload) => {
  const { itemUnlocks } = getGameAssets();
  const serverTime = Date.now();

  // 검증
  // 아이템Id가 현재 스테이지에 나올 수 있는지
  const stages = await getStage(userId);
  console.log(stages);
  const currentStage = stages[stages.length - 1].id;
  for (let i = 0; i < itemUnlocks.data.length; i++) {
    if (itemUnlocks.data[i].stage_id === currentStage) {
      if (!itemUnlocks.data[i].item_id.includes(payload.itemId)) {
        return { status: 'false', message: "This item can't be in your stage" };
      }
      break;
    }
  }

  // 아이템 획득 패킷만 전송하는 어뷰징 행위인지 (아이템 생성 간격을 잘 지켰는지)
  const items = getItem(userId);
  if (items.length > 0) {
    const elapsedTime = (serverTime - items[items.length - 1].timestamp) / 1000;
    // 5~10초 사이로 랜덤하게 생성되는데 5보다 작으면 false, 오차범위 10%
    if (elapsedTime < 4.5) {
      return { status: 'false', message: 'Item creation interval Error' };
    }
  }

  // 검증 완료시 데이터 저장
  addItem(userId, payload.itemId, serverTime);
  console.log(getItem(userId));

  return { status: 'success' };
};

 

 

stage.handler.js

  • 점수에 따라 스테이지 이동을 해주는 함수
  • 클라이언트와 서버 비교, 점수 비교를 통해 검증을 함
import { getGameAssets } from '../init/assets.js';
import { getStage, setStage } from '../models/stage.model.js';

export const moveStageHandler = (userId, payload) => {
  let currentStages = getStage(userId);
  if (!currentStages.length) {
    return { status: 'fail', message: 'No stages found for user' };
  }

  // 오름차순으로 정렬하여 현재 스테이지 확인
  currentStages.sort((a, b) => a.id - b.id);
  const currentStage = currentStages[currentStages.length - 1];

  // 클라이언트 vs 서버 비교
  if (currentStage.id !== payload.currentStage) {
    return { status: 'fail', message: 'Current Stage mismatched', currentStage: currentStage.id };
  }

  // 다음 stage로 갈 수 있는지 score 검사
  const serverTime = Date.now();
  const elapsedTime = (serverTime - currentStage.timestamp) / 1000;
  // console.log(serverTime, currentStage.timestamp, elapsedTime);

  // 5 => 임의로 정한 오차범위 클라이언트 -> 서버까지 딜레이가 너무 길어 에러 처리
  // if (elapsedTime < 95 || elapsedTime > 105) {
  //   return { status: 'fail', message: 'Invalid elapsed time' };
  // }

  // targetStage 검증 <- 게임에셋에 존재하는지
  const { stages } = getGameAssets();
  // some - 배열을 구성하는 것들 중에 조건문이 하나라도 맞으면 true 반환
  if (!stages.data.some((stage) => stage.id === payload.targetStage)) {
    return { status: 'fail', message: 'Target stage not found' };
  }

  setStage(userId, payload.targetStage, serverTime);
  console.log(getStage(userId));
  return { status: 'success' };
};

 

5. 변경된 패킷구조 정리

  • 구현을 하며 이벤트나 로직에 의해 변경된 패킷구조를 다시 정리

 

 

+ TCP vs UDP