1) protobufjs 간단 사용법
1. 프로토콜 버퍼 정의 파일 작성하기
먼저, 최상위 폴더에 protobuf 파일을 만들고, request와 response 폴더를 만듭니다.
request 폴더에는 common.proto 파일을 생성해주고,
syntax = 'proto3';
package common;
message Package{
uint32 handlerId =1;
string userId = 2;
string clientVersion =3;
uint32 sequence = 4;
byte payload = 5;
}
위의 내용은 클라이언트가 서버에 내용을 보내기 위한 내용들로
어떠한 이벤트가 "어떠한 handlerId로 유저가 clientVersion과 sequence를 가지고 요청을 보낼 것인데
이 handerId에서 payload를 처리하기 위해서는 위와 같은 데이터가 필요하다." 라는 내용입니다.
response 폴더에는 response.proto 파일을 생성해주고,
syntax = 'proto3'
package response;
message Response{
uint32 handlerId = 1;
uint32 responseCode =2;
int64 timestamp = 3;
byte data = 4;
uint32 sequence = 5;
}
위의 내용으로 서버가 클라이언트에게 내용을 보내기 위한 내용들을 작성해줍니다.
위와 같이 작성하는 이유는 서버와 클라이언트가 packet을 주고 받을 때 어떤 내용들이 올 것이라는 걸 미리 알아서 혼선이 없도록 하기 위해서 입니다.
이제 두 파일을 JavaScript 상에서 편하게 읽기 위해 packetName.js라는 매핑 파일을 만들겠습니다.
packetName.js 파일 내부에는 아래와 같이
export const packetNames = {
common: {
Packet: 'common.Packet',
},
response: {
Response: 'response.Response',
},
};
위에 작성된 'common' 패키지의 message인 Packet을 나타내는 'common.Package'와 'response' 패키지의 message인 Response를 나타내는 'response.Response'를 작성해줍니다. 만약 어떤 다른 패킷들이 추가 된다면, 위와 같이 파일을 만들어주고, packetName.js 파일에 넣어주면 됩니다.
그 후, init 폴더에 loadProtos.js 파일을 만들어주고,
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const protoDir = path.join(__dirname, '../protobuf');
protobuf가 있는 폴더를 경로로 지정해줍니다. 그후,
const getAllProtoFiles = (dir, fileList = []) => {
//../protobuf 폴더 내에 .proto로 끝나는 파일들을 읽어오기 위한 함수
// 매개변수로는 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') {
// 만약 읽어온 무언가가 파일이고, .proto 파일이면
fileList.push(filePath);
// 리스트에 추가해줍니다.
}
});
return fileList;
};
const protoFiles = getAllProtoFiles(protoDir);
const protoMessages = {};
로 ".proto" 파일로 된 파일들을 가져와 protoFiles 변수 안에 저장해줍니다.
파일들을 읽어는 왔으니 외부에서 사용할 수 있도록 해보겠습니다.
export const loadProts = async () => {
try {
const root = new protobuf.Root();
await Promise.all(protoFiles.map((file) => root.load(file)));
for (const [packageName, types] of Object.entries(packetNames)) {
console.log(packageName);
console.log(types);
}
console.log('Protobuf 파일이 로드 됐습니다.');
} catch (error) {
console.error('Protobuf 파일 로드 중 오류가 발생했습니다 : ', error);
}
};
우선,
import protobuf from 'protobufjs';
protobufjs 패키지를 import 받아주고,
그 다음,
const protoMessages = {};
export const loadProts = async () => {
};
로 외부에서 사용할 수 있도록 함수를 만든 후,
export const loadProts = async () => {
try {
const root = new protobuf.Root();
// protobuf에서 new 연산자로 Root() 객체를 만들어줍니다.
console.log('Protobuf 파일이 로드 됐습니다.');
} catch (error) {
console.error('Protobuf 파일 로드 중 오류가 발생했습니다 : ', error);
}
};
try-catch 문으로 protobuf.Root() 객체를 생성하고,
await Promise.all(protoFiles.map((file) => root.load(file)));
// Promise.all로 파일들을 한 번에 읽어옵니다.
비동기 방식으로 파일을 모두 읽어 들입니다.
// protoMessages를 사용하기 위해서 protoMessages에 내용물을 맵핑하기 위해 packetNames를 가져옵니다.
for (const [packageName, types] of Object.entries(packetNames)) {
protoMessages[packageName] = {};
for (const [type, typeName] of Object.entries(types)) {
protoMessages[packageName][type] = root.lookup(typeName);
}
}
그리고, protoMessages객체에 packageName이라는 key에 따른 객체를 생성해준 후,
for (const [type, typeName] of Object.entries(types)) {
protoMessages[packageName][type] = root.lookupType(typeName);
}
그 객체에 type 이라는 key에 따른 root.lookupType으로 타입 이름을 받아와줍니다.
파일을 사용하는 위치는 index.js의 initServer 함수로
import { loadProts } from './loadProtos.js';
를 import 해주고,
const initServer = async () => {
try {
await loadGameAssets();
await loadProts();
} catch (e) {
console.error(e);
process.exit(1);
}
};
게임 에셋 load와 함께 Prots도 load해줍니다.
하지만, 이럴 경우 파일 원본 데이터를 조작할 위험이 있기 때문에 loadProtos.js에서
protoMessages를 얕은 복사한 함수를 사용하게 만들겠습니다.
export const getProtoMessages = ()=>{
return {...protoMessages}
}
위와 같이 protoMessages는 객체인데 spread연산자와 다시 {}객체를 만들어줘
얕은 복사를 수행합니다.
이유 : 얕은 복사를 통해 원본 객체를 복사한 객체를 반환할 경우 객체 내부의 값이 바뀌더라도 원본 객체는 바뀌지 않기 때문이다.