지난시간까지 프로토버프를 이식했다.
프로토버프 기반으로 패킷을 만들고 전송하고 수신해서 사용하는것까지 만들었다.
지금 이상태로 만들기엔 반복적이고 귀찮은 작업을 해야한다.
우선 프로토버프 파일을 만들고, 배치파일을 실행하는것도 귀찮고, 실핼된걸 복사해서 우리프로젝트로 옮겨오는것도 귀찮다.
그리고 서버 패킷 핸들러를 보면 프로토콜 구조체마다 id를 부여해서 1:1 대응으로 연동한 부분도 불편하다 (makesendbuffer 함수에서 S_TEST를 직접 타이핑해서 넣는 부분)
그리고 HandleBacket에 와서 id로 switch case 노가다를 하는데 많아지면 작업하기도 힘들고 실수의 여지도 많다.
중요한건 자동화 하기 전에 코드를 한번 만들어보고 뭘 자동화 하는게 좋을지 생각해보는 그 과정이다.
디렉토리 구조 약간 변경 common 폴더 만들고 거기에 protoc 폴더 넣고 이름도 바꿈
proto파일이 지금 있는데, S_TEST는 패킷용도인데 BuffData는 일반클래스이다.
나중엔 이것저것 늘어날텐데 하나의 proto파일에서 관리하는건 좋지 않다. 그래서 용도에따라 proto파일을 구분해서 만드는게 좋다. 심지어 proto파일이 너무 거대해지면 빌드하는데도 오래걸릴 수 있으니 컨텐츠별로 구분하는것도 좋다. 나중에 해보자.
오늘은 Struct와 Enum proto파일을 만들어 따로 관리해보자.

일단 protoc-버전어쩌구 폴더를 common 폴더를 만들어 여기다 넣어주고

Enum과 Struct 파일을 만들어주자
그리고 이 두 파일과 배치파일을 vs프로젝트에도 끌어다 넣자. (이전과 다르게 서버와 클라에 각각 복붙해서 처리한게 아니라 그냥 폴더에서 바로 끌어다가 Protocol 필터에 넣었다. - 알고보니 Protocol.proto 도 그렇게 했었음)

이제 배치파일의 내용을 바꿔서 실행해보자. 새 프로토파일을 추가함.
protoc.exe -I=./ --cpp_out=./ ./Enum.proto
protoc.exe -I=./ --cpp_out=./ ./Struct.proto
protoc.exe -I=./ --cpp_out=./ ./Protocol.proto
IF ERRORLEVEL 1 PAUSE

이렇게 생성한 파일을 더미클라이언트와 서버에 복사하고 vs에도 넣어주자.
그런데 이 과정도 배치파일로 자동화 할 수 있다. (처음 vs에 넣어주는거 한번은 직접 해줘야 하는듯 함)
pushd %~dp0
protoc.exe -I=./ --cpp_out=./ ./Enum.proto
protoc.exe -I=./ --cpp_out=./ ./Struct.proto
protoc.exe -I=./ --cpp_out=./ ./Protocol.proto
IF ERRORLEVEL 1 PAUSE
XCOPY /Y Enum.pb.h "..\..\..\GameServer\"
XCOPY /Y Enum.pb.cc "..\..\..\GameServer\"
XCOPY /Y Struct.pb.h "..\..\..\GameServer\"
XCOPY /Y Struct.pb.cc "..\..\..\GameServer\"
XCOPY /Y Protocol.pb.h "..\..\..\GameServer\"
XCOPY /Y Protocol.pb.cc "..\..\..\GameServer\"
XCOPY /Y Enum.pb.h "..\..\..\DummyClient\"
XCOPY /Y Enum.pb.cc "..\..\..\DummyClient\"
XCOPY /Y Struct.pb.h "..\..\..\DummyClient\"
XCOPY /Y Struct.pb.cc "..\..\..\DummyClient\"
XCOPY /Y Protocol.pb.h "..\..\..\DummyClient\"
XCOPY /Y Protocol.pb.cc "..\..\..\DummyClient\"
첫줄은 "이 배치파일이 있는 디렉터리로 이동해서 작업을 실행한다" 는 의미이다.
기존 코드에서 이 배치파일을 실행하면 Binary - Debug/Release 디렉터리에서 실행되어서 적어놓은 조치인듯.
컴파일 하면 에러가 날건데 매번 해주던 vs에서 cc파일들을 파일속성에서 미리컴파일된 헤더 사용안함 설정하자.
이제 이 배치파일도 프로젝트를 빌드할 때 자동으로 실행되도록 만들어보자.

서버와 클라에 모두 해주자.
이렇게 하면 빌드할 때마다 배치파일을 실행해 생성, 복사해준다.
단점은 추적되고있는 소스코드에 변화가 없으면 안해준다. proto 파일만 변경한다고 해서 수정해주지 않는 것.
이걸 해결해보자.
vs를 끈 상태에서 GameServer.vcxproj 파일을 열어보자 (vs말고 다른걸로)
<ItemGroup>
<None Include="..\Common\Protobuf\bin\Enum.proto" />
<None Include="..\Common\Protobuf\bin\GenPackets.bat" />
<None Include="..\Common\Protobuf\bin\Protocol.proto" />
<None Include="..\Common\Protobuf\bin\Struct.proto" />
</ItemGroup>
이 부분을 바로 아래 하나 더 만들어서 아래와 같이 추가해주자.
<ItemGroup>
<UpToDateCheckInput Include="..\Common\Protobuf\bin\Enum.proto" />
<UpToDateCheckInput Include="..\Common\Protobuf\bin\Protocol.proto" />
<UpToDateCheckInput Include="..\Common\Protobuf\bin\Struct.proto" />
</ItemGroup>
위 파일들에 변화가 있으면 이벤트를 실행시켜주는 것. 더미클라이언트에서도 해주자.
이제 proto 파일에 변화가 생기면 배치파일을 실행시켜서 새로 만들고 복붙해준 후에 빌드를 한다.
그다음에 생각해볼 문제는
서버패킷핸들러에서 보면
enum 부분이 지저분하다(?)
우리가 원하는건 proto로 패킷을 설계할 때 enum을 지정할 수 있으면 좋겠다. S_TEST의 id값을 1로 설정하는 것 처럼.
그나마 비슷하게 할 수 있는건 아래와 같이 해주는 방법이다.
이러면 S_TEST 구조체 안에 PACKET_ID가 1, 2 이렇게 있다.
message S_TEST
{
uint64 id = 1;
uint32 hp = 2;
uint32 attack = 3;
repeated BuffData buffs = 4;
enum PacketId {None = 0; PACKET_ID = 1;}
}
message S_LOGIN
{
enum PacketId {Node = 0; PACKET_ID = 2;}
}
그런데 이렇게하면 사람이 실수할 요소가 있다는 것이 문제다.
그리고 이런식으로 고정해서 만들면 문제가 공격자들이 하는 것 중 하나가 패킷id를 조정하는것이다.
그래서 이 패킷id를 주기적으로 교체되는게 중요한 것. 그런데 위와 같은 방법으로 하드코딩하면 그 부분에서 취약해진다.
즉 비추
그럼 어떻게 할까? 자동화 툴이 필요하다. 그건 다음시간에 해보자.
이번엔 다음시간에 자동화 해봐야할것들이 뭔지 생각해보기 위해 서버패킷핸들러를 수정해보자.
#pragma once
#include "Protocol.pb.h"
// TODO 자동화
enum : uint16
{
PKT_S_TEST = 1,
PKT_S_LOGIN = 1
};
class ServerPacketHandler
{
public:
static void HandlePacket(BYTE* buffer, int32 len);
//TODO 자동화
static SendBufferRef MakeSendBuffer(Protocol::S_TEST& pkt) { return MakeSendBuffer(pkt, PKT_S_TEST); }
private:
template<typename T>
static SendBufferRef MakeSendBuffer(T& pkt, uint16 pktid)
{
const uint16 dataSize = static_cast<uint16>(pkt.ByteSizeLong());
const uint16 packetSize = dataSize + sizeof(PacketHeader);
SendBufferRef sendBuffer = GSendBufferManager->Open(packetSize);
PacketHeader* header = reinterpret_cast<PacketHeader*>(sendBuffer->Buffer());
header->size = packetSize;
header->id = pktid;
ASSERT_CRASH(pkt.SerializeToArray(&header[1], dataSize));
sendBuffer->Close(packetSize);
return sendBuffer;
}
};
받는 부분도 수정해보자. 스위치 케이스문 말고 배열로 관리해보자.
//서버패킷핸들러.h
using PacketHandlerFunc = std::function<bool(PacketSessionRef&, BYTE*, int32)>;
extern PacketHandlerFunc GPacketHandler[UINT16_MAX];
//서버패킷핸들러.cpp
PacketHandlerFunc GPacketHandler[UINT16_MAX];
int16max를 쓰는 이유는 패킷 종류가 6만개가 넘어갈 일은 없을거라는 가정하에 메모리를 좀 손해보더라도 속도에서 큰 이득을 보기 위해 이렇게 사용하는 것.
init으로 패킷id별로 불러와야할 함수를 할당해주도록 만들어보자.
이렇게 해주면 HandlePacket을 불러왔을 때 switch case가 아니라 바로 적절한 Handle함수를 불러올 수 있다.
아래 코드도 지금은 이것저것 기능들을 그냥 다 한군데 넣어놨지만 나중엔 다 분리할거라고 한다.
#pragma once
#include "Protocol.pb.h"
using PacketHandlerFunc = std::function<bool(PacketSessionRef&, BYTE*, int32)>;
extern PacketHandlerFunc GPacketHandler[UINT16_MAX];
// TODO 자동화
enum : uint16
{
PKT_S_TEST = 1,
PKT_S_LOGIN = 1
};
//Custom Handlers
bool Handle_INVALID(PacketSessionRef& session, BYTE* buffer, int32 len);
bool Handle_S_TEST(PacketSessionRef& session, Protocol::S_TEST& pkt);
class ServerPacketHandler
{
public:
//TODO 자동화
static void Init()
{
for (int32 i = 0; i < UINT16_MAX; ++i)
GPacketHandler[i] = Handle_INVALID;
GPacketHandler[PKT_S_TEST] = [](PacketSessionRef& session, BYTE* buffer, int32 len) {return HandlePacket<Protocol::S_TEST>(Handle_S_TEST, session, buffer, len); };
}
static bool HandlePacket(PacketSessionRef& session, BYTE* buffer, int32 len)
{
PacketHeader* header = reinterpret_cast<PacketHeader*>(buffer);
return GPacketHandler[header->id](session, buffer, len);
}
//TODO 자동화
static SendBufferRef MakeSendBuffer(Protocol::S_TEST& pkt) { return MakeSendBuffer(pkt, PKT_S_TEST); }
private:
template<typename PacketType, typename ProcessFunc>
static bool HandlePacket(ProcessFunc func, PacketSessionRef& session, BYTE* buffer, int32 len)
{
PacketType pkt;
if (false == pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)))
return false;
return func(session, pkt);
}
template<typename T>
static SendBufferRef MakeSendBuffer(T& pkt, uint16 pktid)
{
const uint16 dataSize = static_cast<uint16>(pkt.ByteSizeLong());
const uint16 packetSize = dataSize + sizeof(PacketHeader);
SendBufferRef sendBuffer = GSendBufferManager->Open(packetSize);
PacketHeader* header = reinterpret_cast<PacketHeader*>(sendBuffer->Buffer());
header->size = packetSize;
header->id = pktid;
ASSERT_CRASH(pkt.SerializeToArray(&header[1], dataSize));
sendBuffer->Close(packetSize);
return sendBuffer;
}
};
이렇게 패킷 자동화를 잘 해놓으면 Custom Handler 에 해당하는
bool Handle_INVALID(PacketSessionRef& session, BYTE* buffer, int32 len);
bool Handle_S_TEST(PacketSessionRef& session, Protocol::S_TEST& pkt);
이러한 함수의 내용물만 수정해서 이런 패킷이 오면 어떻게 실행될지를 구현하면 될 것.
'강의 수강 > 게임서버(1)' 카테고리의 다른 글
| 69. 채팅 실습 (0) | 2025.08.18 |
|---|---|
| 68. 패킷 자동화 #2 (0) | 2025.08.18 |
| 66. Protobuf (0) | 2025.08.16 |
| 54.5 중간 정리 (0) | 2025.07.28 |
| 게임서버 공부 시작 (0) | 2025.03.27 |