2024-11-05 CH-5 팀 과제 (랜덤 타워 디펜스) 패킷 파싱룰 및 회원가입

MOON·2024년 11월 11일
0

내일배움캠프 과제

목록 보기
29/36

저희팀은 이번에 초반 셋팅 및 스켈레톤 코드를 다같이 vsCode의 Live Share 확장프로그램을 이용해서 해보기로 했습니다.

일단 먼저 전에 했던 개인과제의 파일들을 참고해서 이 프로젝트에 어울리게 변경하여 작성하였습니다.

디렉토리 구조

📦src
 ┣ 📂classes
 ┃ ┗ 📂models
 ┃ ┃ ┣ 📜game.class.js
 ┃ ┃ ┗ 📜user.class.js
 ┣ 📂config
 ┃ ┗ 📜config.js
 ┣ 📂constants
 ┃ ┣ 📜env.js
 ┃ ┣ 📜handlerId.js
 ┃ ┗ 📜header.js
 ┣ 📂db
 ┃ ┣ 📂migrations
 ┃ ┃ ┗ 📜createSchema.js
 ┃ ┣ 📂sql
 ┃ ┃ ┣ 📜games_db.sql
 ┃ ┃ ┗ 📜users_db.sql
 ┃ ┣ 📂users
 ┃ ┃ ┣ 📜user.db.js
 ┃ ┃ ┗ 📜user.queries.js
 ┃ ┣ 📜database.js
 ┃ ┗ 📜index.js
 ┣ 📂events
 ┃ ┣ 📜onConnection.js
 ┃ ┣ 📜onData.js
 ┃ ┣ 📜onEnd.js
 ┃ ┗ 📜onError.js
 ┣ 📂handlers
 ┃ ┣ 📂user
 ┃ ┃ ┣ 📜login.handler.js
 ┃ ┃ ┗ 📜register.handler.js
 ┃ ┗ 📜index.js
 ┣ 📂init
 ┃ ┣ 📜index.js
 ┃ ┗ 📜loadProto.js
 ┣ 📂protobuf
 ┃ ┣ 📜packetName.js
 ┃ ┗ 📜protobuf.proto
 ┣ 📂sessions
 ┃ ┣ 📜game.session.js
 ┃ ┣ 📜sessions.js
 ┃ ┗ 📜user.session.js
 ┣ 📂utils
 ┃ ┣ 📂error
 ┃ ┃ ┣ 📜customError.js
 ┃ ┃ ┗ 📜errorHandler.js
 ┃ ┣ 📂parser
 ┃ ┃ ┗ 📜packetParser.js
 ┃ ┣ 📂response
 ┃ ┃ ┣ 📜createHeader.js
 ┃ ┃ ┣ 📜createRespose.js
 ┃ ┃ ┗ 📜responseProto.js
 ┃ ┣ 📜dateFormatter.js
 ┃ ┗ 📜transformCase.js
 ┗ 📜server.js

onData

클라이언트에게 데이터를 받아서 처리하는 함수 부분입니다.
여기서 받아온 버전과 시퀀스를 체크하고 데이터(payload)를 확인합니다.

import { CLIENT_VERSION } from '../constants/env.js';
import {
  PACKET_TYPE_LENGTH,
  VERSION_LENGTH,
  SEQUENCE_LENGTH,
  PAYLOAD_LENGTH,
} from '../constants/header.js';
import { getHandlerById } from '../handlers/index.js';
import { getClientBySocket } from '../sessions/client.session.js';
import { packetParser } from '../utils/parser/packetParser.js';

export const onData = (socket) => async (data) => {
  try {
    socket.buffer = Buffer.concat([socket.buffer, data]);
    const client = getClientBySocket(socket);
    client.updateSequence(); // 시퀀스 업데이트

    while (socket.buffer.length >= PACKET_TYPE_LENGTH + VERSION_LENGTH) {
      // 패킷 타입 읽어오기
      const packetType = socket.buffer.readUInt16BE(0);
      const versionLength = socket.buffer.readUInt8(PACKET_TYPE_LENGTH);
      const totalHeaderLength =
        PACKET_TYPE_LENGTH + VERSION_LENGTH + versionLength + SEQUENCE_LENGTH + PAYLOAD_LENGTH;

      if (socket.buffer.length < totalHeaderLength) {
        break;
      }

      // 버전 불러오기
      const version = socket.buffer.toString(
        'utf8',
        PACKET_TYPE_LENGTH + VERSION_LENGTH,
        PACKET_TYPE_LENGTH + VERSION_LENGTH + versionLength,
      );
      // 버전 체크
      if (version !== CLIENT_VERSION) {
        throw new Error('클라이언트 버전이 일치하지 않습니다.');
      }

      // 시퀀스
      const sequence = socket.buffer.readUInt32BE(
        PACKET_TYPE_LENGTH + VERSION_LENGTH + versionLength,
      );
      // 시퀀스 체크
      if (sequence !== client.getSequence()) {
        throw new Error('시퀀스가 일치하지 않습니다.');
      }

      // 페이로드 길이
      const payloadLength = socket.buffer.readUInt32BE(
        PACKET_TYPE_LENGTH + VERSION_LENGTH + versionLength + SEQUENCE_LENGTH,
      );
      const totalPacketLength = totalHeaderLength + payloadLength;

      // 패킷 전체 길이보다 짧으면 처음으로
      if (socket.buffer.length < totalPacketLength) {
        break;
      }

      // console.log('packetType', packetType);
      // console.log('version: ', version);
      // console.log('sequence', sequence);
      // console.log('payload Length: ', payloadLength);

      // payload
      const payload = socket.buffer.subarray(totalHeaderLength, totalPacketLength);
      // console.log('payload: ', payload);
      // 이후의 데이터 다시 저장
      socket.buffer = socket.buffer.subarray(totalPacketLength);

      const data = packetParser(packetType, payload);

      const handler = getHandlerById(packetType);
      // console.log(handler);
      await handler({ packetType, data, socket });
    }
  } catch (e) {
    console.error('onData error: ', e);
  }
};

packetParser

해당 받은 데이터(payload)를 파싱하여 해당 값을 추출하는 함수입니다.

import { getProtoTypeNameByPacketType } from '../../handlers/index.js';
import { getProtoMessages } from '../../init/loadProto.js';

export const packetParser = (packetType, data) => {
  try {
    const protoMessages = getProtoMessages();

    const GamePacket = protoMessages['protoPacket']['GamePacket'];
    const gamePacket = GamePacket.decode(data);

    const payloadType = gamePacket.payload;
    if (!payloadType) {
      throw new Error('No payload found in gamePacket');
    }

    const payloadField = GamePacket.oneofs['payload'].oneof.find(
      (field) => gamePacket[field] != null,
    );
    if (!payloadField) {
      throw new Error('No valid payload field found in GamePacket');
    }

    const protoTypeName = getProtoTypeNameByPacketType(packetType);
    const payload = gamePacket[payloadField];

    const [namespace, typeName] = protoTypeName.split('.');
    const expectedProto = protoMessages[namespace][typeName];
    const expectedFields = Object.keys(expectedProto.fields);
    const actualFields = Object.keys(payload);
    const missingField = expectedFields.filter((field) => !actualFields.includes(field));
    if (missingField > 0) {
      throw Error();
    }

    // return { packetType, userId, payload };
    return payload;
  } catch (err) {
    console.error('error in packet Parsing: ', err.message);
  }
};

오늘의 회고
처음에 제대로 파싱이 안되서 고생하다가 프로토 파일에 GamePacket구조로 한번더 묶어서 보내주는 것을 알아 팀원분 한분이 '설마...' 하면서 먼저 GamePacket구조로 한번 decode하고 또 다시 oneof에서 해당 필드의 이름을 찾아 알맞는 payload를 구할 수 있었습니다. 어찌저찌 다 같이 하니깐 그래도 해답을 빠르게 찾을 수 있었던거 같습니다.

오늘도 화이팅

profile
안녕하세요

0개의 댓글