카테고리 없음
TIL20250131
foreiner852
2025. 1. 31. 21:00
# 패킷 분해/헤더 및 상태 동기화
## 패킷 분해/헤더
### 받는 경우
- on data 이벤트에서 변환하는 내용을 만들었다.
```
export const onData = (socket) => async (data) => {
try {
socket.buffer = Buffer.concat([socket.buffer, data]);
console.log('=== 새로운 패킷 수신 ===');
console.log('수신된 데이터:', data);
// 패킷의 버전 길이까지의의 헤더 길이 (패킷 길이 정보 + 버전 길이 정보)
const leastHeaderLength = config.packet.packetTypeLength + config.packet.versionLengthLength;
let totalHeaderLength =
config.packet.packetTypeLength +
config.packet.versionLengthLength +
config.packet.sequenceLength +
config.packet.payloadLengthLength;
let versionLength = 0;
if (socket.buffer.length >= leastHeaderLength) {
versionLength = socket.buffer.readUInt8(config.packet.packetTypeLength);
}
totalHeaderLength += versionLength;
// 버퍼에 최소한 전체 헤더가 있을 때만 패킷을 처리
while (socket.buffer.length >= totalHeaderLength) {
let readHeadBuffer = 0;
//패킷타입정보 수신(2바이트)
const packetType = socket.buffer.readUInt16BE(readHeadBuffer);
readHeadBuffer += config.packet.packetTypeLength + config.packet.versionLengthLength;
//버전 수신
const version = socket.buffer
.slice(readHeadBuffer, readHeadBuffer + versionLength)
.toString('utf-8');
readHeadBuffer += versionLength;
// clientVersion 검증
if (version !== config.client.version) {
throw new CustomError(
ErrorCodes.CLIENT_VERSION_MISMATCH,
'클라이언트 버전이 일치하지 않습니다.',
);
}
// 시퀀스 수신(4바이트)
const sequence = socket.buffer.readUInt32BE(readHeadBuffer);
readHeadBuffer += config.packet.sequenceLength;
// 패이로드 길이 수신(4바이트)
const payloadLength = socket.buffer.readUInt32BE(readHeadBuffer);
readHeadBuffer += config.packet.payloadLengthLength;
// 3. 패킷 전체 길이 확인 후 데이터 수신
if (socket.buffer.length >= payloadLength + readHeadBuffer) {
// 패킷 데이터를 자르고 버퍼에서 제거
const packet = socket.buffer.slice(readHeadBuffer, readHeadBuffer + payloadLength);
socket.buffer = socket.buffer.slice(readHeadBuffer + payloadLength);
try {
const user = getUserBySocket(socket);
let game;
// 유저가 접속해 있는 상황에서 시퀀스 검증
if (user && user.getNextSequence() !== sequence) {
console.log(`USER SEQUENCE => ${user.sequence} / SEQUENCE => ${sequence}`);
throw new CustomError(ErrorCodes.INVALID_SEQUENCE, '잘못된 호출 값입니다. ');
}
if(user !== undefined){
game = getGameByUser(user);
}
const payload = packetParser(packetType, packet);
const handler = getHandlerById(packetType);
await handler({
socket,
userId: user !== undefined ? user.id : null,
payload,
user,
game,
});
} catch (error) {
handleError(socket, error);
}
}
}
} catch (error) {
console.error('onData 처리 중 오류:', error);
handleError(socket, error);
}
};
```
헤더 부분을 때어내서 해석을 하는 내용이다.
버전 길이가 따로 클라이언트에서 보내주기 때문에 헤더의 길이를 유동적으로 처리한다.
- packetParser 데이터 디코딩(handlerId == packetType )
handlerId 를 protoType과 같은 의미로 사용했다.
```
export const packetParser = (handlerId, rawPayload) => {
const protoMessages = getProtoMessages();
// 핸들러 ID에 따라 적절한 payload 구조를 디코딩
const protoTypeName = getProtoTypeNameByHandlerId(handlerId);
if (!protoTypeName) {
throw new CustomError(ErrorCodes.UNKNOWN_HANDLER_ID, `알 수 없는 핸들러 ID: ${handlerId}`);
}
const [namespace, typeName] = protoTypeName.split('.');
const expectedPayloadType = protoMessages[namespace][typeName];
const PayloadType = protoMessages['test']['GamePacket'];
testLog(0, `protoTypeName: ${protoTypeName}`, 'yellow');
testLog(0, `namespace: ${namespace}, typeName: ${typeName}`, 'green');
testLog(0,`expectedPayloadType: ${expectedPayloadType}`,'green',);
let payload;
try {
payload = PayloadType.decode(rawPayload);
testLog(
0,
`Namespace: ${protoMessages['test']['C2SRegisterRequest']} \n
protoTypeName: ${protoTypeName}\n
handlerId: ${handlerId}\n
namespace: ${namespace} / typeName: ${typeName}\n`,
'yellow'
);
testLog(
0,
`PayloadType: ${PayloadType} / ${JSON.stringify(PayloadType)} / rawPayload: ${rawPayload}\n`,
'yellow',
false
);
} catch (error) {
throw new CustomError(ErrorCodes.PACKET_STRUCTURE_MISMATCH, '패킷 구조가 일치하지 않습니다.');
}
// 필드가 비어 있거나, 필수 필드가 누락된 경우 처리
const expectedFields = Object.keys(expectedPayloadType.fields);
testLog(0,`expectedFields: ${expectedFields}`,'red');
testLog(0,`payload: ${JSON.stringify(payload)}`,'blue');
const actualFields = Object.keys(Object.values(payload)[0]);
testLog(0,`actualFields: ${actualFields}`,'green');
const missingFields = expectedFields.filter((field) => !actualFields.includes(field));
testLog(0, `missingFields: ${missingFields} / length: ${missingFields.length}`);
if (missingFields.length > 0) {
throw new CustomError(ErrorCodes.INVALID_PACKET, '지원하지 않는 패킷 타입입니다.');
}
let returnPayload = {};
for (const [key, value] of Object.entries(Object.values(payload)[0])) {
returnPayload[key] = value;
}
console.log(returnPayload);
return returnPayload;
};
```
프로토 버퍼에 oneof 로 해서 알아서 패킷을 찾아서 분해한다. 처음에 이걸 이해 못해서 꽤 고생했다.
### 주는 경우 (헤더 추가)
- payloadParser 함수로 전해받은 내용을 합치는 내용이다.
```
// 패이로드에 헤더를 붙여서 클라이언트에 보낼 패킷으로 변환
export const payloadParser = (packetType, user, Payload) => {
// 버전 문자열 준비
const version = config.client.version;
const versionBuffer = Buffer.from(version, 'utf8');
// 1. 패킷 타입 정보를 포함한 버퍼 생성 (2바이트)
const packetTypeBuffer = Buffer.alloc(config.packet.packetTypeLength);
packetTypeBuffer.writeUint16BE(packetType, 0);
// 2. 버전 길이 (1바이트)
const versionLengthBuffer = Buffer.alloc(config.packet.versionLengthLength);
versionLengthBuffer.writeUInt8(versionBuffer.length, 0);
// 3. 시퀀스 (4바이트, big endian)
const sequenceBuffer = Buffer.alloc(config.packet.sequenceLength);
sequenceBuffer.writeInt32BE(user.sequence);
// 4. 페이로드 길이 (4바이트, big endian)
const payloadLengthBuffer = Buffer.alloc(config.packet.payloadLengthLength);
payloadLengthBuffer.writeInt32BE(Payload.length);
// 5. 최종 패킷 데이터 생성
return Buffer.concat([
packetTypeBuffer,
versionLengthBuffer,
versionBuffer,
sequenceBuffer,
payloadLengthBuffer,
Payload,
]);
};
```
사실상 상수값을 순서대로 붙여서 보냈다.
### 상태 동기화
프로토 버퍼 : S2CStateSyncNotification stateSyncNotification = 7;
stateSyncNotificationHandler 헨들러로 처리
- 패이로드 생성
```
export const createS2CStateSyncNotificationPacket = (user) => {
const protoMessages = getProtoMessages();
const userStateData = protoMessages.test.GamePacket;
const userTowers = user.towers.map((value) => {
return { towerId: value.id, x: value.x, y: value.y };
});
const userMonsters = user.monsters.map((value) => {
return { monsterId: value.id, monsterNumber: value.num, level: value.level };
});
const payload = {
stateSyncNotification: {
userGold: user.gold,
baseHp: user.baseHp,
monsterLevel: user.monsterLevel,
score: user.score,
towers: userTowers,
monsters: userMonsters,
},
};
const message = userStateData.create(payload);
const userStateDataPacket = userStateData.encode(message).finish();
return payloadParser(PACKET_TYPE.STATE_SYNC_NOTIFICATION, user, userStateDataPacket);
};
```
유저 정보를 패이로드에 담아 보내는 내용이다.
### 기지 HP 업데이트 및 게임 오버
C2SMonsterAttackBaseRequest monsterAttackBaseRequest = 16;
- 몬스터 공격 처리 헨들러
```
const monsterAttackBaseRequestHandler = ({socket, userId, payload, user, game}) => {
try {
const {damage} = payload;
user.substractBaseHp(damage);
} catch (error) {
handleError(user.socket, error);
}
};
```
유저 클래스에서 기지 체력을 감소시킨다. -> 유저 기지 체력 변경으로 updateBaseHpNotification 호출
S2CUpdateBaseHPNotification updateBaseHpNotification = 17;
- 기지 체력 업데이트
```
const updateBaseHpNotificationHandler = ({ user }) => {
try {
const gameSession = getGameByUser(user);
if (gameSession) {
const users = gameSession.users;
users[0].socket.write(
createS2CUpdateBaseHPNotificationPacket(user, users[1].id === user.id),
);
users[1].socket.write(
createS2CUpdateBaseHPNotificationPacket(user, users[0].id === user.id),
);
}
} catch (error) {
handleError(user.socket, error);
}
};
```
두 유저에게 현재 체력을 전달한다.
```
export const createS2CUpdateBaseHPNotificationPacket = (user, isOpponent = true) => {
const protoMessages = getProtoMessages();
const baseHpData = protoMessages.test.GamePacket;
testLog(0, `userid ${user.id} userhp ${user.baseHp} isOpponent ${isOpponent}`);
const payload = {
updateBaseHpNotification: {
isOpponent,
baseHp: user.baseHp,
},
};
const message = baseHpData.create(payload);
const baseHpDataPacket = baseHpData.encode(message).finish();
testLog(0, `baseHpDataPacket ${baseHpDataPacket.toString('hex')}`);
return payloadParser(PACKET_TYPE.UPDATE_BASE_HP_NOTIFICATION, user, baseHpDataPacket);
};
```
패킷 생성 함수
S2CGameOverNotification gameOverNotification = 18;
- 게임 종료
```
substractBaseHp(baseHp) {
if (this.baseHp <= baseHp) {
const handler = getHandlerById(18);
handler({
user: this,
});
return -1;
}
this.baseHp -= baseHp;
updateBaseHp(this);
return this.baseHp;
}
setDatabaseId(databaseId) {
return (this.databaseId = databaseId);
}
```
기지 체력이 0 혹은 그 이하가 된 사용자가 호출한다.
```
const updategameOverNotificationHandler = ({ user }) => {
try {
const gameSession = getGameByUser(user);
const users = gameSession.users;
users[0].socket.write(createS2CGameOverNotificationPacket(users[0], users[0].id !== user.id));
users[1].socket.write(createS2CGameOverNotificationPacket(users[1], users[0].id === user.id));
removeGameSession(gameSession.id);
} catch (error) {
handleError(user.socket, error);
}
};
```
각 유저에게 내용에 맞게 전송
```
export const createS2CGameOverNotificationPacket = (user, isWin) => {
const protoMessages = getProtoMessages();
const gameOverData = protoMessages.test.GamePacket;
const payload = {
gameOverNotification: {
isWin,
},
};
const message = gameOverData.create(payload);
const gameOverDataPacket = gameOverData.encode(message).finish();
return payloadParser(PACKET_TYPE.GAME_OVER_NOTIFICATION, user, gameOverDataPacket);
};
```
패킷 생성 함수