이번 프로젝트에서 맡게된 부분은 타워 공격 부분이다.
타워 공격 부분에서 처리할 수 있는 일은 많다 => 저번에 했던 프로젝트에서는 몬스터의 정보, 타워의 정보와 함께 타워의 공격력, 몬스터의 공격력 등과 같은 것들을 실제로 서버에서 처리하여 클라이언트로 보내주는 방식을 채용하기도 했었다.
그렇지만 이번에 TCP를 사용하게 되면서 정해진 패킷 구조가 있었고 해당하는 패킷 구조에서는 실제로 처리할 수 있는게 거의 없다는 것이 현실이었다.
이렇게 C2S(클라이언트 -> 서버)와 S2C(서버 -> 클라이언트) 패킷이 정해져 있다.
특히나 S2C,C2S는 실제로도 자주 사용한다고하니 꼭 기억해두자.
다음으로 이런 파트를 맡게되었음으로 로직을 생각해야만 한다.
- 클라이언트에서 서버로 towerId와 monsterId를 보내온다.
- 그 정보를 받아 해당하는 정보가 존재하는지 서버에서 검증한다.
- 검증이 끝났다면 타워 공격이 성공했음으로 이를 연결된 다른 소켓에 보내준다.
패킷 구조를 확인하고 이런 계획을 세웠다.
이를 직접 구현한 코드이다.
export const towerAttackHandler = async ({ socket, payload }) => {
try {
const fieldName = Object.keys(payload)[0];
const { towerId, monsterId } = payload[fieldName];
// 타워, 몬스터 유무 검증
await towerAttackVerifiy(towerId, monsterId, socket.id);
// 모든 유저 정보 가져옴
const users = await redis.getUsers(socket.gameId);
// 유저 정보중에 socket.id랑 같지 않은 => 다른 유저의 소켓을 찾음
const socketId = users.find((user) => {
return user !== socket.id;
});
// 찾은 socketId로 connectedSockets에 조회하여 찾음
const enemySocket = connectedSockets.get(socketId);
const towerAttackPacket = {
enemyTowerAttackNotification: { towerId: towerId, monsterId: monsterId },
};
enemySocket.write(
createResponse(PacketType.ENEMY_TOWER_ATTACK_NOTIFICATION, 0, towerAttackPacket),
);
} catch (error) {
console.error(`타워 공격 정보 처리중 에러 발생: ${error}`);
}
};
다른 분들이 만들어둔 기능으로 찾는게 수월했어서 오랜 시간이 걸렸던 것 같지는 않다.
그렇기에 이를 테스트할 수 있는 테스트 코드를 만들어 보고자 했다.
import net from 'net';
import Config from '../src/config/config.js';
import HANDLER_IDS from '../src/constants/handlerIds.js';
import { deserialize, serialize } from '../src/utils/serializer/serialize.js';
import { getProtoMessages, loadProtos } from '../src/init/loadProtos.js';
const client = new net.Socket();
const version = '1.0.0';
client.connect(Config.SERVER.PORT, Config.SERVER.HOST, async () => {
console.log(`클라이언트 테스트가 연결되었습니다.`);
await loadProtos();
const packetType = HANDLER_IDS.TOWER_ATTACK_REQUEST;
//const version = Config.CLIENT.VERSION;
const sequence = 0;
const payload = { towerAttackRequest: { towerId: 1, monsterId: 1 } };
const protoMessages = getProtoMessages();
const towerAttackProto = protoMessages.packets.GamePacket;
const message = towerAttackProto.create(payload);
const towerAttackpacket = towerAttackProto.encode(message).finish();
const buffer = serialize(packetType, sequence, towerAttackpacket);
client.write(buffer);
});
client.on('data', (socket) => async (data) => {
socket.buffer = Buffer.concat([socket.buffer, data]);
while (socket.buffer.length >= Config.PACKETS.TOTAL_HEADER_LENGTH) {
const deserializeData = await deserialize(socket);
if (deserializeData.version !== version) {
throw new Error(`서버에서 보내준 버전이 다릅니다.`);
}
const requiredLength = deserializeData.offset + deserializeData.payloadLength;
if (socket.buffer.length >= requiredLength) {
const packet = socket.buffer.subarray(deserializeData.offset, requiredLength);
try {
const protoMessages = getProtoMessages();
const gamePacketProto = protoMessages.packets.GamePacket;
const decodedPacket = gamePacketProto.decode(packet);
console.log(`서버에서 온 패킷`, decodedPacket);
} catch (error) {
throw new Error(`S2C 페이로드 파싱중 에러 발생`, error);
}
}
}
});
기존에 있었던 클라이언트 코드를 이용하여 만들었지만,
기존과 패킷구조가 달라서 새롭게 인코딩하여 보내기도 했고 이를 통해서 towerAttackHandler로 잘 들어가고 정보가 잘 들어가있다는 것을 확인하였다.
실제로 테스트 코드를 직접 작성해보면서 동작방식, 흐름에 대해서 직접 이해하는데 도움이 많이 되었고
실제로 들어가기 전에 발생하는 에러들을 미연에 방지할 수 있어서 좋았다.