저희팀은 이번에 초반 셋팅 및 스켈레톤 코드를 다같이 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
클라이언트에게 데이터를 받아서 처리하는 함수 부분입니다.
여기서 받아온 버전과 시퀀스를 체크하고 데이터(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);
}
};
해당 받은 데이터(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를 구할 수 있었습니다. 어찌저찌 다 같이 하니깐 그래도 해답을 빠르게 찾을 수 있었던거 같습니다.
오늘도 화이팅