패킷 직렬화의 마지막

서버패킷핸들러를 패킷이 하나 생길때마다 한땀한땀 만드는게 아닌 자동으로 만들어지게 해보자.

 

proto파일을 파싱해서 패킷을 인지해서 코드를 자동으로 만들어지게 할거라 한다.

우선 어느 부분을 자동화할지 구체적으로 알아보자.

#pragma once
#include "Protocol.pb.h"

using PacketHandlerFunc = std::function<bool(PacketSessionRef&, BYTE*, int32)>;
extern PacketHandlerFunc GPacketHandler[UINT16_MAX];

enum : uint16
{
	// TODO 자동화
	PKT_S_TEST = 1,
	PKT_S_LOGIN = 2
};

//Custom Handlers
bool Handle_INVALID(PacketSessionRef& session, BYTE* buffer, int32 len);
// TODO 자동화
bool Handle_S_TEST(PacketSessionRef& session, Protocol::S_TEST& pkt);

class ServerPacketHandler
{
public:
	static void Init()
	{
		for (int32 i = 0; i < UINT16_MAX; ++i)
			GPacketHandler[i] = Handle_INVALID;

		// TODO 자동화
		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;
	}
};

 

  1. 각패킷 enum
  2. 각 패킷에 대응하는 핸들 함수 정의
  3. 패킷 핸들 함수 배열에 할당
  4. MakeSendBuffer 오버로딩 (첫 항에 어떤 패킷구조체 쓰는지에 따른 오버로딩)

그리고 지금은 서버쪽의 서버패킷핸들러 라는 이름으로 쓰고있는데, 나중에 클라->서버 뿐 아니라 서버->서버 통신도 있을 것이기 때문에 이름이 너무 광범위하니 "누가" 나한테 패킷을 보내느냐를 기준으로 이름을 짓도록 하자

예를 들어 클라->서버로 데이터를 보내서 서버측에서 그걸 처리할 때 클라이언트 패킷 핸들러가 그걸 처리하는 식으로.

 

그건 그렇고 일단 툴을 만들어볼것이다.

보통 회사에서는 툴은 c#으로 만들수도 c++로 만들수도 있지만 파이썬을 만드는게 대부분이다. 파이썬이 툴 만들기에 편리하게 되어있다고 한다.


우선 vs인스톨러를 이용해 파이썬을 추가한다. (나는 좀 다르길래 여기에 더해서 웹사이트 가서 파이썬을 설치함)

새 솔루션 폴더 추가 Tools

서버 디렉토리에도 Tools 추가 (더미클라, 서버, 서버코어 폴더 있는곳)

새 파이썬 프로젝트 PacketGenrator 추가 위치는 Tools에

 

그리고 윈도우 설정 - 환경변수 변경 - 사용자 환경변수에서 path에 파이썬 폴더를 추가해주자. 아래 처럼 탐색기로 파이썬을 폴더를 열면 바로 알 수 있다. 맨끝에 \ 넣어주는걸 잊지 말자

C:\Users\~~~~~\AppData\Local\Programs\Python\Python313\
C:\Users\~~~~~\AppData\Local\Programs\Python\Python313\Scripts

을 추가한다.

그런데 난 강의자와 달리 cmd에서 py라고 써야 파이썬이 켜져서 gpt의 도움을 받아 python.exe의 심볼릭링크를 만들어주는 식으로 python으로 실행시킬 수 있게 만들었다(명령어만 조금 다른거라 굳이 안해도 되는거긴 한데)

mklink C:\Windows\System32\python.exe "C:\Users\사용자명\AppData\Local\Programs\Python\Python312\python.exe"

 

위와같이 해주고 pip install jinja2 를 클릭해주자.

같은 방식으로 pyinstaller도 설치해주자.

 


- 파이썬으로 Protocol.proto 읽고 패킷 이름 추출해서 S_ 붙은 앤지 C_ 붙은 앤지 구분하고, id 발급하기

import argparse
import jinja2
import ProtoParser

def main():

    arg_parser = argparse.ArgumentParser(description = 'PacketGenerator')
    arg_parser.add_argument('--path', type=str, default='D:/Projects/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)

    # jinja2


    return

if __name__ == '__main__':
    main()
class ProtoParser(object):
    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

 


 

이제 이것과 jinja2를 이용해서 우리가 c++에서 자동화 하고싶었던 부분을 바꿔치기 하는 것.

이것을 위해서 먼저 바꿔치기의 대상이 될 파일

"D:\Projects\CPP_Server\Server\Tools\PacketGenerator\Templates\PacketHandler.h" 을 만들어준다.

원본은 이전 serverpackerhandler.h 이다.

중간중간 자동화 하려 했던 부분엔 jinja2 만의 문법으로 "이 부분을 이렇게 대체할것이다" 라는 코드가 들어가있다.

#pragma once
#include "Protocol.pb.h"

using PacketHandlerFunc = std::function<bool(PacketSessionRef&, BYTE*, int32)>;
extern PacketHandlerFunc GPacketHandler[UINT16_MAX];

// TODO 자동화
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);
	}

	//TODO 자동화
{%- 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 (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;
	}
};

 

import argparse
import jinja2
import ProtoParser

def main():

    arg_parser = argparse.ArgumentParser(description = 'PacketGenerator')
    arg_parser.add_argument('--path', type=str, default='D:/Projects/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', encoding='cp949')
    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()

 

 

일단 print해서 결과물이 어떻게 나왔는지 볼 수 있다.

 

파이썬을 이용해 자동으로 프토로파일을 기반으로 h파일을 만드는데 성공했다.

 


그런데 이걸 매번 빌드해서 실행할 수 없으니까 편하게 exe 실행파일로 만들고싶다.

하지만 파이썬에서는 기본적으로 vs상에서 실행파일을 만들수 없다.

별도의 라이브러리로 만들 수 있는데, 아까 받은 pyinstaller이다.

 

먼저 Tools\PacketGenerator에 MakeExe.bat를 만들자.

pushd %~dp0
pyinstaller --onefile PacketGenerator.py

그리고 bat을 실행하자.

dist 안에 PacketGenerator.exe가 만들어졌다.

우리가 필요한건 exe파일 뿐이니 다른쓰레기들은 지워주자.

pushd %~dp0
pyinstaller --onefile PacketGenerator.py
MOVE .\dist\PacketGenerator.exe .\GenPacket.exe
@RD /S /Q .\build
@RD /S /Q .\dist
DEL /S /F /Q .\PacketGenerator.spec
PAUSE

다시 실행해보면 깔끔하게 이렇게 될것이다.

templates폴더와 genpacket.exe는 common - protobuf - bin으로 옮겨놓자.

 

이제 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

GenPacket.exe --path=./Protocol.proto --output=ClientPacketHandler --recv=C_ --send=S_
GenPacket.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 *.ph.h
DEL /Q /F *.pb.cc
DEL /Q /F *.h

더미클라에서는 서버패킷핸들러를 가지고있음

서버에서는 클라패킷핸들러를 가지고있음

 

이제 클라, 서버 프로젝트에 각각 들어간 패킷핸들러의 파일이름, 클래스이름이 바뀌었고, HandlePacket 함수같은 경우 들어가는 인자도 바뀌었으니 그것들을 싹 잘 돌아가게끔 바꿔주자.

 

'강의 수강 > 게임서버(1)' 카테고리의 다른 글

70. JobQueue #1  (0) 2025.08.19
69. 채팅 실습  (0) 2025.08.18
67. 패킷 자동화 #1  (0) 2025.08.17
66. Protobuf  (0) 2025.08.16
54.5 중간 정리  (0) 2025.07.28

+ Recent posts