본문 바로가기

부트캠프

멀티플레이 게임 파일 구조 이해

server.js ⇒ 서버 띄우는 용

constants ⇒ 상수 ,패킷타입 정의 폴더

  1. env.js ⇒ 선언한 환경변수를 한번에 관리하는 파일
//환경변수 관리
import dotenv from "dotenv";

dotenv.config();
//서버정보

//왼쪽은 .env데이터 오른쪽은 defalut값
export const HOST = process.env.HOST || "localhost";
export const PORT = process.env.PORT || 5555;
export const CLIENT_VERSION = process.env.CLIENT_VERSION || "1.0.0";

//DB정보
export const DB_NAME = process.env.DB_NAME || "user_db";
export const DB_USER = process.env.DB_USER || "root";
export const DB_PASSWORD = process.env.DB_PASSWORD || "1234";
export const DB_HOST = process.env.DB_HOST || "127.0.0.1";
export const DB_PORT = process.env.DB_PORT || "3306";

/**********************************************************/
//.env에서 환경변수 선언

HOST =127.0.0.1
PORT=5555
CLIENT_VERSION = 1.0.0

##DB정보

DB_NAME = user_db
DB_USER = root
DB_PASSWORD=1234
DB_HOST=127.0.0.1
DB_PORT=3306

2. header.js ⇒ 상수와 패킷타입 정의

//상수 선언
export const TOTAL_LENGTH = 4;//패킷의 전체 길이를 측정하는 패킷의 길이
export const PACKET_TYPE_LENGTH = 1;//패킷타입의 길이

//패킷타입 정의
export const PACKET_TYPE = {
  PING: 0, //왕복 레이턴시를 측정하기 위한 패킷타입 
  NORMAL: 1, //데이터 처리가 필요한 패킷타입
  LOCATION: 3,//위치에 관련한 업데이트에 필요한 패킷타입
};

3. handlerIds.js ⇒ 클라이언트에서 온 요청을 처리할 핸들러ID를 관리하는 파일 클라이언트에서 요청한 핸들러ID에따라 어떤 작업을 수행할지 결정한다.

export const RESPONSE_SUCCESS_CODE = 0;//요청이 성공했다면 클라이언트에 0을 보내준다.

//특별한 요청을 식별하기 위한 상수들이다.
//각 ID는 클라와 서버간의 통신에서 어떤 작업을 수행할지를 명확히한다.
export const HANDLER_IDS = {
  INITIAL: 0,
  LOCATION_UPDATE: 2,
};

events ⇒ 모든 소켓 이벤트를 관리하는 파일

  1. onConnection.js ⇒ server.js에서 서버를 생성할경우 실행되는 함수이다. 여러가지 이벤트를 가지고 있으며 서버 실행시 그 이벤트들을 대기시키는 함수
import { onData } from "./onData.js";
import { onEnd } from "./onEnd.js";
import { onError } from "./onError.js";

//server.js에 createServer로 서버가 실행되면 가장 최상위에 보내준다.
export const onConnection = (socket) => {
  console.log(`Client connected from : ${socket.remoteAddress} : ${socket.remotePort}`);
  //소켓에는 클라이언트의 정보가 들어있음
  socket.buffer = Buffer.alloc(0); //아무 크기가 없는 버퍼 객체를 각 클라이언트 소켓에 넣어줌 여기에 데이터를 넣었다 뻇다

  //대기중인 소켓 이벤트
  socket.on("data", onData(socket));

  socket.on("end", onEnd(socket));

  socket.on("error", onError(socket));
};

//서버 실행시 모든 이벤트들 무한정 대기

//server.js

const server = net.createServer(onConnection);
  1. onDate.js ⇒ 클라이언트에서 받아온 데이터를 처리하는 파일


init ⇒ 서버가 켜짐과 동시에 로드가 되는 내용들을 담은파일

  1. index.js ⇒ 서버가 켜지기전 메모리에 모든 파일을 로드하는 함수를 작성한 파일
//서버 실행시 불러올 데이터를 여기 추가

import { addGameSession } from "../sessions/game.session.js";
import { testConnection } from "../utils/db/testConnection.js";
import { loadProtos } from "./loadProto.js";
import { v4 as uuidv4 } from "uuid";

//이 함수가 실행된 후에 서버가 실행되야 하므로 비동기 처리를 해준다.
const initServer = async () => {
  try {
    // 프로토타입 파일을 로드한다.
    await loadProtos();
    //게임생성시 인자로 넣어줄 gameId를 UUID로 생성
    const gameId = uuidv4(); 
    // 생성된 게임 ID를 사용하여 새로운 게임 세션을 추가
    const gameSession = addGameSession(gameId); 
    // 데이터베이스 연결을 테스트
    await testConnection();
  } catch (err) {
    console.error(err);
    //오류 발생시 프로세스를 종료
    process.exit(1);
  }
};

export default initServer;

//server.js에서 호출

//서버실행시 필요한 데이터들이 전부 들어왔다면 그 이후 서버를 실행한다.
initServer()
  .then(() => {
    //포트 , 호스트주소 ,백로그
    server.listen(PORT, HOST, () => {
      console.log(`서버 켜짐${HOST}  : ${PORT}`);
    });
  })
  .catch((err) => {
    console.error(err);
    process.exit(1);
  });


protobuf ⇒ 여러가지 proto파일들을 담아놓은 폴더

  1. proto 파일 ⇒ 프로토콜 버퍼 파일, 즉 .proto파일은 데이터의 구조와 형식을 정의하는데 사용되는 파일이다. 간단하게 말해 클라이언트에서 받아올 데이터의 형식을 작성하는 파일이다.
  2. 데이터 구조 정의 .proto 파일에서는 메시지 형식을 정의하며 ,각 메시지의 필드와 타입을 지정한다.
  3. 타입 안정성 정의된 메시지 타입을 사용하여 클라이언트와 서버간의 데이터 교환 시 타입 안정성을 보장한다.
  4. 자동 코드 생성 프로토콜 버퍼 컴파일러를 사용하여 .proto파일로부터 다양한 프로그래밍 언어에 맞는 코드 (JS,Python,Java등)를 자동으로 생성할 수 있다. 이를 통해 서버와 클라이언트 간의 데이터 처리 방식을 일관되게 유지할 수 있다.
  5. 버전관리 프로토콜 버퍼는 데이터 구조의 버전 관리를 용이하게 하여 이전 버전과의 호환성을 유지하면서 새로운 필드를 추가하거나 기존 필드를 변경할 수 있다.
syntax ="proto3";

package common;

message Packet {
    uint32 handlerId = 1;
    string userId = 2;
    string version =3;
    bytes payload =4;
}

  1. packetNames.js ⇒ 패킷을 어떤 패킷으로 설정했는지 작성하는 파일 proto 파일에서 작성한값을 편하게 맵핑해주는 용도의 파일이다.
//앞에 선언한 common,Packet등등은 맵핑용
export const packetNames = {
  common: {
    Packet: "common.Packet", //common.Packet:protobuf를 사용할때 사용할 이름
    Ping: "common.Ping",
  },
  initial: {
    InitialPayload: "initial.InitialPayload",
  },
  game: {
    LocationUpdatePayload: "game.LocationUpdatePayload",
  },
  gameNotification: {
    LocationUpdate: "gameNotification.LocationUpdate",
  },
  response: {
    Response: "response.Response",
  },
};

/*예시
common = 패킷이름
Packet = 패킷타입
common.Pakcet = 타입이름
*/
  1. loadProto.js ⇒ .proto파일을 읽어와서 메모리에 로드하는 로직을 작성한 파일 프로토타입 파일을 동적으로 찾고,로드하며,이를 통해 생성된 메시지 타입을 관리한다.
  2. 모듈 임포트
import fs from "fs"; // 파일 시스템 모듈
import path from "path"; // 경로 관련 모듈
import { fileURLToPath } from "url"; // URL 모듈의 함수
import protobuf from "protobufjs"; // protobufjs 라이브러리
import { packetNames } from "../protobuf/packetNames.js"; // 패킷 이름 정의
  • 설명

fs: 파일 시스템에 접근하여 파일과 디렉토리를 읽기 위한 모듈입니다. path: 파일 경로를 조작하기 위한 모듈입니다. fileURLToPath: ES 모듈에서 현재 모듈의 파일 경로를 가져오는 데 사용됩니다. protobuf: 프로토콜 버퍼를 다루기 위한 라이브러리입니다. packetNames: 프로토타입 파일에서 정의한 패킷 이름을 가져옵니다.

  1. 파일 경로 설정
const __filename = fileURLToPath(import.meta.url); //nodejs에서 ES모듈을 사용할때 현재 모듈의 파일 경로를 가져오는 코드
const __dirname = path.dirname(__filename); //filename으로 찾은 경로에있는 디렉토리의 이름을 반환함
const protoDir = path.join(__dirname, "../protobuf"); //현재위치:init 폴더 이므로 ../로 상위폴더의 protobuf파일을 찾아 가져온다.

현재 파일의 경로를 가져와서 __dirname을 설정한다.

protoDir = 프로토타입 파일이 위치한 경로를 넣어준다.

  1. 프로토파일 검색 함수
//최초 실행시 빈 배열을 넘겨줌
const getAllProtoFiles = (dir, fileList = []) => {
  const files = fs.readdirSync(dir); //주어진 디렉토리의 현재 경로를 읽어오는 부분

  files.forEach((file) => {
    const filePath = path.join(dir, file); //파일의 전체 경로를 구함
    const stat = fs.statSync(filePath); //현재 파일 상태 확인

    //현재 파일의 상태가 디렉토리일 경우 getAllProtoFiles를 재귀호출
    if (stat.isDirectory()) {
      //현재 파일의 상태가 디렉토리 일 경우 배열을 다시 넘겨줌
      getAllProtoFiles(filePath, fileList);
    } else if (path.extname(file) === ".proto") {
      //현재 file의 확장자명이 .proto일 경우 fileList에 파일을 추가한다.
      fileList.push(filePath);
    }
  });
  //마지막 값을 반환
  return fileList;
};

주어진 디렉토리 내의 모든 .proto파일을 재귀적으로 찾아서 그 경로를 배열에 저장하는 함수다.

디렉토리 내의 파일을 읽고 각 파일의 상태를 확인하여 디렉토리일 경우 재귀적으로 호출하며 .proto파일을 발견하면 fileList에 추가한다.

간단하게 말해서 최상위 프로토파일 디렉토리 로들어가 그파일안에 있는게 .proto파일이라면 배열에 저장하고 파일이아닌 디렉토리가 들어있다면 그 디렉토리안으로 들어가 .proto파일을 찾는다.

 

함수 실행시 예상도

📦protobuf =  디렉토리
 ┣ 📂notification = getAllProtoFiles = 파일로 들어감
 ┃ ┗ 📜game.notification.proto = .proto 이므로 배열에 저장
 ┣ 📂request = getAllProtoFiles = 파일로 들어감
 ┃ ┣ 📜common.proto = .proto 이므로 배열에 저장
 ┃ ┣ 📜game.proto = .proto 이므로 배열에 저장
 ┃ ┗ 📜Initial.proto = .proto 이므로 배열에 저장
 ┣ 📂response =  getAllProtoFiles = 파일로 들어감
 ┃ ┗ 📜response.proto = .proto 이므로 배열에 저장
 ┗ 📜packetNames.js = .proto 이므로 배열에 저장
  1. 프로토파일 로드 및 메시지 타입 정의 loadProtos 함수는 프로토파일을 비동기적으로 로드한다. protobuf.Root()를 생성하여 프로토타입 메시지의 루트 객체를 만든다. Promise.all을 사용하여 모든 프로토파일을 동시에 로드한다. 로드가 완료되면 packetNames에 정의된 각 패킷 이름에 대해 protoMessages 객체에 메시지타입 저장
//protoFIles배열은 .proto파일들의 전체 경로가 담겨있는 배열이다.
const protoFiles = getAllProtoFiles(protoDir);

//완성된 파일이 들어갈 객체
const protoMessages = {}; //읽어온 파일들을 저장할 객체 생성

export const loadProtos = async () => {
  try {
    /*protobufjs에서 제공하는 클래스이다. 새로운Protocol Buffers의 루트 객체를 생성하는 코드이다. 
      이 루트 객체는 메시지 타입을 정의하고 관리할 수 있는 최상위 컨테이너이다.*/
    const root = new protobuf.Root(); //row한 파일을 Root메서드를 사용해서 읽는다.

    //Promise.all을 사용하여 portoFiles의 배열의 각 파일경로에 대해 비동기적으로 root.load(file)를 호출하여 모든 파일을 동시에 로드한다.
    //이 작업이 끝나면 root객체는 모든 .proto파일에 정의된 메시지 타입과 구조체를 메모리에 올린상태가 된다.
    await Promise.all(
      protoFiles.map(
        (file) => root.load(file), //각 파일을 읽고 파싱하여 root객체에 포함시키는 함수다.
      ),
    );

    for (const [packetName, types] of Object.entries(packetNames)) {
      //packetName: packetNames.js에서 작성해둔 이름
      protoMessages[packetName] = {};
      for (const [type, typeName] of Object.entries(types))
        protoMessages[packetName][type] = root.lookupType(typeName);
    }

    console.log(`Protobuf파일 로드 성공`);
  } catch (err) {
    console.error(`Protobuf파일 로드 중 오류 발생`, err);
  }
};
  1. 메시지 타입 접근 함수 getProtoMessages 함수는 protoMessages객체의 얕은 복사본을 반환하여 원본 객체의 변조를 방지한다.
export const getProtoMessages = () => {
  return { ...protoMessages };
};

 

'부트캠프' 카테고리의 다른 글

타워 디펜스 온라인 1  (2) 2024.11.08
멀티플레이  (3) 2024.11.07
프로토콜 , 로드밸런싱 , 헬스체크  (1) 2024.10.31
protobufjs 사용법  (0) 2024.10.30
리눅스 기본 명령어 / PM2명령어  (1) 2024.10.24