KoreanFoodie's Study
[C++ 게임 서버] 5-9. 패킷 자동화 #2 본문
[C++ 게임 서버] 5-9. 패킷 자동화 #2
핵심 :
1. 파이썬과 jinja2 를 이용해, 패킷 핸들러 코드를 자동으로 생성해 보자.
2. 이 포스팅에서 만든 ProtoParser 는 Protocol.proto 의 패킷 명세를 읽어 PacketHandler 의 코드를 자동으로 생성한다.
저번 시간에 패킷 자동화를 위한 밑작업(?) 을 보여준 바 있다.
사실 제대로 패킷 자동화를 구축하기 위해서는, data-driven 하게 작업이 이루어져야 할 것이다.
간단한 Flow 를 생각하면 다음과 같다 :
- Protocol.proto 에 우리가 원하는 패킷(메시지)을 선언한다.
- 자동화 툴이 돌아가서 Packet 과 PacketHandler 클래스를 생성한다.
- 프로그래머는 각 패킷에 대한 커스텀 동작만 직접 구현한다.
이전에 만든 Protocol.proto 의 경우 다음과 같이 생겼는데...
syntax = "proto3";
package Protocol;
import "Enum.proto";
import "Struct.proto";
message C_TEST
{
uint64 id = 1;
}
message C_MOVE
{
uint64 id = 1;
uint32 pos = 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 { NONE = 0; PACKET_ID = 1;}
}
우리는 이 proto 파일을 읽어, 적절한 패킷을 형식에 맞게 만들고, 자동으로 일부 코드도 생성하도록 만들 것이다.
음... 그런데 어떻게 하면 좋을까? 🤔
사실 툴을 만들 때 가장 많이 사용하는 언어가 바로 Python 인데, 여기서도 python 프로젝트를 이용해 툴 프로젝트를 하나 추가해 줄 것이다!
대충 우리의 솔루션 아래에, Tools 라는 폴더를 만들고 파이썬 프로젝트를 추가해 주자.
위처럼, PacketGenerator 와 ProtoParser 두 녀석을 추가한다. ProtoParser 는 .proto 파일을 읽어서 파싱을 해 주는 녀석으로, 파싱한 결과는 PacketGenerator 에 전달되어 PacketHandler.h 라는 샘플 코드와 같은 느낌으로 결과물이 만들어질 것이다. 거두절미하고, 바로 코드를 보자.
ProtoParser.py
class ProtoParser():
def __init__(self, start_id, recv_prefix, send_prefix):
self.recv_pkt = [] # 수신 패킷 목록
self.send_pkt = [] # 송신 패킷 목록
self.total_pkt = [] # 모든 패킷 목록
self.start_id = start_id
self.id = start_id
self.recv_prefix = recv_prefix
self.send_prefix = send_prefix
def parse_proto(self, path):
f = open(path, 'r')
lines = f.readlines()
for line in lines:
if line.startswith('message') == False:
continue
pkt_name = line.split()[1].upper()
if pkt_name.startswith(self.recv_prefix):
self.recv_pkt.append(Packet(pkt_name, self.id))
elif pkt_name.startswith(self.send_prefix):
self.send_pkt.append(Packet(pkt_name, self.id))
else:
continue
self.total_pkt.append(Packet(pkt_name, self.id))
self.id += 1
f.close()
class Packet:
def __init__(self, name, id):
self.name = name
self.id = id
파이썬 코드는 좀 오랜만인데... 일단 parse_proto 쪽을 보면, 'message' 로 시작하는 줄을 읽어서 pkt_name 과 id(계속 1 씩 증가함) 를 이용해 패킷 목록을 각각 recv_pkt/send_pkt/total_pkt 에 담아 주는 것을 알 수 있다.
PacketGenerator.py
import argparse
import jinja2
import ProtoParser
def main():
arg_parser = argparse.ArgumentParser(description = 'PacketGenerator')
arg_parser.add_argument('--path', type=str, default='C:/Rookiss/CPP_Server/Server/Common/Protobuf/bin/Protocol.proto', help='proto path')
arg_parser.add_argument('--output', type=str, default='TestPacketHandler', help='output file')
arg_parser.add_argument('--recv', type=str, default='C_', help='recv convention')
arg_parser.add_argument('--send', type=str, default='S_', help='send convention')
args = arg_parser.parse_args()
parser = ProtoParser.ProtoParser(1000, args.recv, args.send)
parser.parse_proto(args.path)
file_loader = jinja2.FileSystemLoader('Templates')
env = jinja2.Environment(loader=file_loader)
template = env.get_template('PacketHandler.h')
output = template.render(parser=parser, output=args.output)
f = open(args.output+'.h', 'w+')
f.write(output)
f.close()
print(output)
return
if __name__ == '__main__':
main()
그리고 PacketGenerator 는, 파싱한 결과를 읽어서 jinja2 라이브러리를 이용해 렌더링을 해 주는데... 이때 PacketHandler.h 를 템플릿으로 사용하고 있다.
이제 대망의 PacketHandler.h 를 보자.
PacketHandler.h
#pragma once
#include "Protocol.pb.h"
using PacketHandlerFunc = std::function<bool(PacketSessionRef&, BYTE*, int32)>;
extern PacketHandlerFunc GPacketHandler[UINT16_MAX];
enum : uint16
{
{%- for pkt in parser.total_pkt %}
PKT_{{pkt.name}} = {{pkt.id}},
{%- endfor %}
};
// Custom Handlers
bool Handle_INVALID(PacketSessionRef& session, BYTE* buffer, int32 len);
{%- for pkt in parser.recv_pkt %}
bool Handle_{{pkt.name}}(PacketSessionRef& session, Protocol::{{pkt.name}}& pkt);
{%- endfor %}
class {{output}}
{
public:
static void Init()
{
for (int32 i = 0; i < UINT16_MAX; i++)
GPacketHandler[i] = Handle_INVALID;
{%- for pkt in parser.recv_pkt %}
GPacketHandler[PKT_{{pkt.name}}] = [](PacketSessionRef& session, BYTE* buffer, int32 len) { return HandlePacket<Protocol::{{pkt.name}}>(Handle_{{pkt.name}}, session, buffer, len); };
{%- endfor %}
}
static bool HandlePacket(PacketSessionRef& session, BYTE* buffer, int32 len)
{
PacketHeader* header = reinterpret_cast<PacketHeader*>(buffer);
return GPacketHandler[header->id](session, buffer, len);
}
{%- for pkt in parser.send_pkt %}
static SendBufferRef MakeSendBuffer(Protocol::{{pkt.name}}& pkt) { return MakeSendBuffer(pkt, PKT_{{pkt.name}}); }
{%- endfor %}
private:
template<typename PacketType, typename ProcessFunc>
static bool HandlePacket(ProcessFunc func, PacketSessionRef& session, BYTE* buffer, int32 len)
{
PacketType pkt;
if (pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)) == false)
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;
}
};
음... 뭔가 엄청 화려해 보이는데, 사실 이 부분만 잘 보면 된다 :
enum : uint16
{
{%- for pkt in parser.total_pkt %}
PKT_{{pkt.name}} = {{pkt.id}},
{%- endfor %}
};
결국 위 코드 스니펫을 보면... parser 라는, 우리가 아까 파싱한 녀석으로부터 인자를 꺼내와서 루프를 돌면서 코드를 작성하는 것이기 때문이다! {{ }} 로 감싸서 변수를 조회하고, {% %} 로 루프를 표현하는 것을 확인할 수 있다.
그리고 {%- ... 에서 '-' 는 줄바꿈 관련 옵션인데... 자세한 건 직접 찾아보도록 하자! 🤣
어쨌든, 위와 같이 사용하면 파이썬 코드를 이용해(jinja2 라이브러리) cpp 코드를 자동으로 생성해 줄 수 있다! 😁
참, GenPackets.bat 파일의 경우는 아래와 같이 조금 수정해 주도록 하자 :
pushd %~dp0
protoc.exe -I=./ --cpp_out=./ ./Enum.proto
protoc.exe -I=./ --cpp_out=./ ./Struct.proto
protoc.exe -I=./ --cpp_out=./ ./Protocol.proto
GenPackets.exe --path=./Protocol.proto --output=ClientPacketHandler --recv=C_ --send=S_
GenPackets.exe --path=./Protocol.proto --output=ServerPacketHandler --recv=S_ --send=C_
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 ClientPacketHandler.h "../../../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"
XCOPY /Y ServerPacketHandler.h "../../../DummyClient"
DEL /Q /F *.pb.h
DEL /Q /F *.pb.cc
DEL /Q /F *.h
PAUSE
이제 자동화 스크립트가 만들어주는 Handler 부분을 보자. 참고로, 이번 글부터 ServerPacketHandler 와 ClientPacketHandler 의 이름을 뒤바꾸어 주었다. 왜냐면... 이름 그대로, ClientPacketHandler 는 GameServer 에서, ClientPacket 받았을 때 어떻게 행동하면 좋은지 정의하면 이름의 정의에 좀 더 부합하기 때문이다!
어쨌든, 대표적인 예시로 ClientPacketHandler 를 보자 :
ClientPacketHandler.h
#pragma once
#include "Protocol.pb.h"
using PacketHandlerFunc = std::function<bool(PacketSessionRef&, BYTE*, int32)>;
extern PacketHandlerFunc GPacketHandler[UINT16_MAX];
enum : uint16
{
PKT_C_TEST = 1000,
PKT_C_MOVE = 1001,
PKT_S_TEST = 1002,
PKT_S_LOGIN = 1003,
};
// Custom Handlers
bool Handle_INVALID(PacketSessionRef& session, BYTE* buffer, int32 len);
bool Handle_C_TEST(PacketSessionRef& session, Protocol::C_TEST& pkt);
bool Handle_C_MOVE(PacketSessionRef& session, Protocol::C_MOVE& pkt);
class ClientPacketHandler
{
public:
static void Init()
{
for (int32 i = 0; i < UINT16_MAX; i++)
GPacketHandler[i] = Handle_INVALID;
GPacketHandler[PKT_C_TEST] = [](PacketSessionRef& session, BYTE* buffer, int32 len) { return HandlePacket<Protocol::C_TEST>(Handle_C_TEST, session, buffer, len); };
GPacketHandler[PKT_C_MOVE] = [](PacketSessionRef& session, BYTE* buffer, int32 len) { return HandlePacket<Protocol::C_MOVE>(Handle_C_MOVE, 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);
}
static SendBufferRef MakeSendBuffer(Protocol::S_TEST& pkt) { return MakeSendBuffer(pkt, PKT_S_TEST); }
static SendBufferRef MakeSendBuffer(Protocol::S_LOGIN& pkt) { return MakeSendBuffer(pkt, PKT_S_LOGIN); }
private:
template<typename PacketType, typename ProcessFunc>
static bool HandlePacket(ProcessFunc func, PacketSessionRef& session, BYTE* buffer, int32 len)
{
PacketType pkt;
if (pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)) == false)
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;
}
};
위 코드가 자동 생성 되었다는 것이 새삼 놀라울 뿐이다.. 😎
자동화 스크립트가 구축되었으면, 프로그래머는 이제 각 패킷에 대한 로직(예를 들어 Handle_C_MOVE 함수)만 작성해 주면 된다!
#include "ClientPacketHandler.h"
PacketHandlerFunc GPacketHandler[UINT16_MAX];
// 직접 컨텐츠 작업자
bool Handle_INVALID(PacketSessionRef& session, BYTE* buffer, int32 len)
{
PacketHeader* header = reinterpret_cast<PacketHeader*>(buffer);
// TODO : Log
return false;
}
bool Handle_C_TEST(PacketSessionRef& session, Protocol::C_TEST& pkt)
{
// TODO
return true;
}
bool Handle_C_MOVE(PacketSessionRef& session, Protocol::C_MOVE& pkt)
{
// TODO
return true;
}
이것으로, 대략적인 패킷 자동화에 대한 설명을 마치도록 한다.
사실 Protobuf 와 패킷 자동화 부분에 대한 설명은 세팅에 대한 부분을 포스팅에 그대로 적진 않았다. 좀 복잡하기도 하고, 실제로 한 번 따라해 보면 되지, 이걸 암기할 필요는 없기 때문이다. 😂
그러니 관심이 생긴다면, Rookiss 님 강의를 직접 들어보면 어떨까 싶다! 😊
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 6-2. JobQueue #1 (1) | 2023.12.19 |
---|---|
[C++ 게임 서버] 6-1. 채팅 실습 (0) | 2023.12.19 |
[C++ 게임 서버] 5-8. 패킷 자동화 #1 (0) | 2023.12.18 |
[C++ 게임 서버] 5-7. Protobuf (0) | 2023.12.16 |
[C++ 게임 서버] 5-6. 패킷 직렬화 #3 (0) | 2023.12.16 |