ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TIL] 프로토콜 버퍼 (protobuf) - 1
    TIL 2024. 6. 29. 21:43

     

    학습 키워드: Node.js, net, socket, protobuf

     

    1. protobuf 기본 세팅

    1) What is it?:

    이전 TIL(링크)에서 protobuf가 Google에서 개발한 오픈소스 직렬화(serialization) 라이브러리라고 언급한 바 있다. 이번에는 스파르타 강의에서 제공된 예시코드를 통해 protobufjs 라이브러리로 proto 파일들을 읽어들이고 컴파일하여 사용 가능한 상태로 만드는 법을 먼저 알아보았다.

     

     

    2) How does it work?:

    npm i protobufjs

     시작에 앞서 우선 protobufjs 라이브러리를 설치해준다.

     

    // src/protobuf/request/common.proto
    
    syntax = 'proto3';
    
    package common;
    
    message Packet {
        uint32 handlerId = 1;
        string userId = 2;
        string clientVersion = 3;
        uint32 sequence = 4;
        bytes payload = 5;
    }

     .proto 파일을 정의해준다. 이전에 웹 소켓 프로젝트에서 패킷에 들어갈 정보(key, value)들을 정의하는 패킷 명세서라는 것을 작성했었는데, 해당 정보들을 schema 형태로 정의해주는 것이 proto 파일이 하는 역할이다. 이렇게 정의된 proto 파일들을 protobufjs 라이브러리를 통해 컴파일하여 사용가능한 상태로 만들어준다.

     

    // src/init/loadProtos.js
    
    import fs from "fs";
    import path from "path";
    import { fileURLToPath } from "url";
    import protobuf from "protobufjs";
    import { packetNames } from "../protobuf/packetNames.js";
    
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    const protoDir = path.join(__dirname, "../protobuf");
    
    const protoMessages = {};
    
    const getAllProtoFiles = (dir, fileList = []) => {
      const files = fs.readdirSync(dir);
    
      files.forEach((file) => {
        const filePath = path.join(dir, file);
        if (fs.statSync(filePath).isDirectory()) {
          getAllProtoFiles(filePath, fileList);
        } else if (path.extname(file) === ".proto") {
          fileList.push(filePath);
        }
      });
    
      return fileList;
    };
    
    const protoFiles = getAllProtoFiles(protoDir);
    
    export const loadProtos = async () => {
      try {
        const root = new protobuf.Root();
        await Promise.all(protoFiles.map((file) => root.load(file)));
    
        for (const [packageName, types] of Object.entries(packetNames)) {
          protoMessages[packageName] = {};
          for (const [type, typeName] of Object.entries(types)) {
            protoMessages[packageName][type] = root.lookupType(typeName);
          }
        }
    
        // console.log(protoMessages);
    
        console.log("Protobuf 파일이 성공적으로 로드 되었습니다.");
      } catch (err) {
        console.error("Protobuf 파일 로드 중 오류가 발생했습니다: ", err);
      }
    };
    
    export const getProtoMessages = () => {
      return { ...protoMessages }; // shallow copy
    };

     강의에서 실제로 protobufjs 라이브러리가 사용되는 파일은 이 파일 하나 뿐이다. NPM 문서에 나와있는 방식과는 조금 다르게 사용했다. 프로젝트의 src/protobuf 디렉토리에 있는 proto 파일들을 모두 찾아 그 path들을 리스트로 반환하는 getAllProtoFiles 함수, getAllProtoFiles 함수를 통해 반환된 파일 목록에서 proto를 load하고 이를 protoMessages 라는 객체에 매핑해주는 loadProtos 함수, 그리고 protoMessages 객체를 복사하여 반환하는 getProtoMessages 함수로 이루어져있다.

     

    const root = new protobuf.Root();

      protobufjs 라이브러리는 타입스크립트로 작성되어 있는데, export as namespace protobuf 구문을 통해 export를 하기 때문에 import protobuf from 'protobufjs' 로 import를 하면 protobuf를 통해 위 예시처럼 Root 클래스의 생성자를 사용할 수 있다. Protobuf에는 namespace라는 것(export에서 사용한 namespace랑은 다름)이 있는데, root는 모든 type, enum, services, sub-namespaces를 모두 아우르는 최상위 namespace다.

     

    await Promise.all(protoFiles.map((file) => root.load(file)));

     생성자를 통해 만들어진 root 객체에 있는 load 라는 method를 이용하면 .proto 확장자를 가진 파일을 컴파일하여 사용 가능한 상태로 만들어 root에 저장하게 된다.

     

    protoMessages[packageName][type] = root.lookupType(typeName);

     root 객체를 직접 access하여 lookupType 라는 method를 통해 찾아올 수도 있겠지만, 강의에서는 미리 생성해둔 protoMessages라는 객체에 proto의 package 이름을 key로 가지는 sub-object를 만들고, 그 object에 다시 type이름을 가지는 key, 그리고 그 key에 root.lookupType() 으로 찾아낸 proto type을 묶어둔다. typeName은 packageName.messageTypeName 구조이며, 맨 위에서 정의한 common.proto 파일을 예로들면 common.Packet 이 된다.

     

     아래는 참고용으로 올려둔 강의에서 작성된 proto 파일 구조를 매핑한 js 파일이다.

     

    // src/protobuf/packetNames.js
    
    export const packetNames = {
      common: { // packageName
        Packet: "common.Packet", // type : typeName
      },
      initial: {
        InitialPacket: "initial.InitialPacket",
      },
      response: {
        Response: "response.Response",
      },
    };

     

     

     

    --

     

    REFERENCES:

     

    https://teamsparta.notion.site/2-7b03e94d6bd346cfa23d5514d9d15139#df5c8c7791a94ae4bd1661967d22ddfe

     > 강의 노트, "[게임서버개발 플러스] 실전 게임 서버만들기 (2)" by 조호영 튜터님, accessed: 2024-06-29

    https://www.npmjs.com/package/protobufjs

     > protobufjs NPM 페이지

    728x90
Designed by Tistory.