지난번에 인증서버 완성을 위해 앞으로 해야될것들을 정리해봤었다.

  • RedisConnectionPool
    Redis를 활용한 세션토큰 발급과 활용방법공부 필요.
  • SQL쿼리 바인더
    현재는 sql인젝션에 취약한 상태임
  • 비밀번호 해싱과 관리
  • 타이머 관리
    타이머를 만들긴 했는데 사용이 불편하고, 적절한 해제방법은 구현하지 않았음
  • 이메일 API 사용
  • Client/Server PacketHandler.h 코드 자동생성 기능 만들기
    게임서버강의에서 배운 Protocol.proto 파일을 파싱해서 패킷핸들러 헤더를 자동으로 만들어주는 프로그램을 만들자.

그중에서 가장 만만해보이는 코드 자동생성을 처리해보자.

사실 만만한건 아닌데 게임서버강의 들을때 만들어놓은게 있어서 그대로 가져오면 거의 품이 안들어서 먼저 쳐내려는 것.

게임서버 만들때는 어떻게 했는지는 아래 게시글을 참조하자.

 

67. 패킷 자동화 #1

지난시간까지 프로토버프를 이식했다.프로토버프 기반으로 패킷을 만들고 전송하고 수신해서 사용하는것까지 만들었다. 지금 이상태로 만들기엔 반복적이고 귀찮은 작업을 해야한다. 우선 프

dodontak.tistory.com

 

 

68. 패킷 자동화 #2

패킷 직렬화의 마지막서버패킷핸들러를 패킷이 하나 생길때마다 한땀한땀 만드는게 아닌 자동으로 만들어지게 해보자. proto파일을 파싱해서 패킷을 인지해서 코드를 자동으로 만들어지게 할거

dodontak.tistory.com


패킷 자동화

이번 목표는 인증서버에서 쓰는 ClientPacketHandler.h와 더미클라이언트에서 쓰는 ServerPacketHandler.h를 Protocol.proto 파일과 PacketHander.h (두 결과물의 베이스가 되는 템플릿) 기반으로 자동생성해주는 것이다.

 

먼저 몇가지 설치를 해야한다. Dockerfile의 install 목록에 추가하자.

apt install -y python3 python3-jinja2 python3-pyinstaller

 

 

proto파일과 PacketHandler.h 로 어떻게 두 파일을 생성시키는지 과정을 간략하게 설명하자면 이렇다.

  1. Protocol.proto파일을 파싱해서 message로 설정된 문구를 recv, send, total 배열에 넣는다. 넣을때 id값도 설정해서 넣음.
  2. 만들어둔 템플릿 파일을 jinja2의 템플릿파일로 설정한다.
  3. 템플릿 파일을 jinja2로 변환한다. 인자로 recv, send, total 배열을 넣어서 쓴다.

 

해야할 것

  • 템플릿 파일 만들기
  • 템플릿파일 변환할 파이썬 코드 짜기
  • 파이썬 코드로 실행파일 만들기
  • 쉘스크립트 만들어서 파이썬코드->실행파일, 탬플릿파일 -> XXXXPacketHander 자동화 하기

템플릿 파일 만들기

c++ 헤더파일 같지만, 중간중간 jinja2 문법들이 들어가있다.

변환할때 넣는 인자를 삽입시켜서 코드를 만들어낸다. 처음보는 문법이지만 대충 보면 무슨뜻인지 알 수 있다.

게임서버에서 쓴걸 거의 가져왔지만 epoll과 IOCP의 차이로 인해 변경된 부분은 직접 적용해줘야했다.

#pragma once

#include "Types.h"
#include "Session.h"
#include "Protocol.pb.h"
#include "WriteBuffer.h"
#include <memory>
#include <functional>

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

enum : uint16
{
{%- for pkt in parser.total_pkt %}
	PKT_{{pkt.name}} = {{pkt.id}},
{%- endfor %}
};

bool	Handle_INVALID(std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len);
{%- for pkt in parser.recv_pkt %}
void	Handle_{{pkt.name}}(const PacketSessionRef& session, const Protocol::{{pkt.name}}& pkt);
{%- endfor %}

class {{output}}
{
public:
	static void	Init()
	{
		for (int i = 0; i < UINT16_MAX; ++i)
			GPacketHandler[i] = Handle_INVALID;
{%- for pkt in parser.recv_pkt %}
		GPacketHandler[PKT_{{pkt.name}}] = [](std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len) {
			return GetCallback<Protocol::{{pkt.name}}>(outFunc, Handle_{{pkt.name}}, session, buffer, len);
		};
{%- endfor %}
	}

	static bool	PacketHandler(std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len)
	{
		PacketHeader*	header = reinterpret_cast<PacketHeader*>(buffer);
		return GPacketHandler[header->id](outFunc, session, buffer, len);
	}

{%- for pkt in parser.send_pkt %}
	static WriteBufferRef MakeWriteBuffer(Protocol::{{pkt.name}}& pkt) { return MakeWriteBuffer(pkt, PKT_{{pkt.name}}); }
{%- endfor %}

private:
	template<typename PacketType, typename ProcessFunc>
	static bool	GetCallback(std::function<void()>& outFunc, ProcessFunc& func, PacketSessionRef& session, BYTE* buffer, int32 len)
	{
		PacketType	pkt;
		if (false == pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)))
			return false;
		outFunc = [func, session, pkt](){ func(session, pkt); };
		return true;
	}

	template<typename T>
	static WriteBufferRef	MakeWriteBuffer(T& pkt, uint16 pktId)
	{
		int	headerSize = sizeof(PacketHeader);
		int	pktSize = pkt.ByteSizeLong();
		WriteBufferRef	writeBuffer = std::make_shared<WriteBuffer>(headerSize + pktSize);

		PacketHeader	header;
		header.id = pktId;
		header.size = headerSize + pktSize;

		writeBuffer->AppendBuffer(reinterpret_cast<BYTE*>(&header), headerSize);
		pkt.SerializeToArray(writeBuffer->GetCopyBuffer(), pktSize);
		return writeBuffer;
	}
};

하는김에 MakeWriteBuffer도 업그레이드 했다. 기존엔 패킷Id를 넣어줘야했는데, 패킷별로 오버로딩 함수를 만들어서 패킷만 넣어도 되도록 만들어줬다.

그리고 쓸데없이 복사하는 부분들을 최대한 줄이기 위해 함수 파라미터에 &도 많이 넣었다. 어차피 람다함수에 넣을때는 복사해서 들어가기때문에 상관없다. 다만 람다함수 캡쳐가 const가 적용되기 때문에 캡쳐된 session의 값을 바꿀 수 없다. 그래서 상위 함수들에도 const를 다 붙여야했다. 람다함수에 mutable 키워드를 적어도 된다고 한다. 근데 const 가 붙어있던걸 떼버리는건 굉장히 꺼림찍한 일이기 때문에 지양해야 한다 생각해서 mutable은 사용하지 않았다.


템플릿파일 변환할 파이썬 코드 짜기

메인파일과 파싱담당 파일이 있다.

PacketGenerator.py

argparse라는걸 이용해서 나중에 실행파일의 인자로 넣어줘야하는 것들과 디폴트값을 설정할 수 있다.

ProtoParser로 message 이름 배열들을 받고, jinja2로 템플릿 파일과 배열을 넣어서 putput file 을 만든다.

import argparse
import jinja2
import ProtoParser

def main():

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

    return

if __name__ == '__main__':
    main()

 

ProtoParser.py

proto파일을 파싱해서 메시지를 recv, send, total 배열에 넣는 역할

recv_prefix와 send_prefix는 각각 _C _S로 넣으면 인증서버, _S _C로 넣으면 더미클라용이다.

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

파이썬 코드로 실행파일 만들기

쉘스크립트로 PyInstaller를 사용해 PacketGenerator를 만들어주고, 이동시킨다. 그리고 임시파일들을 다 제거해준다.

GenPacketMaker.sh

#!/bin/bash

cd "$(dirname "$0")"
python3 -m PyInstaller   --onefile PacketGenerator.py
mv ./dist/PacketGenerator ./GenPackets
rm -rf ./build
rm -rf ./dist
rm -f ./PacketGenerator.spec

 

잘 만들어졌다. 실행도 문제 없다.

만들어진 ClientPacketHandler.h

#pragma once

#include "Types.h"
#include "Session.h"
#include "Protocol.pb.h"
#include "WriteBuffer.h"
#include <memory>
#include <functional>

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

enum : uint16
{
	PKT_C_SIGNUP = 1000,
	PKT_S_SIGNUP = 1001,
	PKT_C_VERIFY_EMAIL = 1002,
	PKT_S_VERIFY_EMAIL = 1003,
	PKT_C_LOGIN = 1004,
	PKT_S_LOGIN = 1005,
};

bool	Handle_INVALID(std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len);
void	Handle_C_SIGNUP(PacketSessionRef& session, const Protocol::C_SIGNUP& pkt);
void	Handle_C_VERIFY_EMAIL(PacketSessionRef& session, const Protocol::C_VERIFY_EMAIL& pkt);
void	Handle_C_LOGIN(PacketSessionRef& session, const Protocol::C_LOGIN& pkt);

class ClientPacketHandler
{
public:
	static void	Init()
	{
		for (int i = 0; i < UINT16_MAX; ++i)
			GPacketHandler[i] = Handle_INVALID;
		GPacketHandler[PKT_C_SIGNUP] = [](std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len) {
			return GetCallback<Protocol::C_SIGNUP>(outFunc, Handle_C_SIGNUP, session, buffer, len);
		};
		GPacketHandler[PKT_C_VERIFY_EMAIL] = [](std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len) {
			return GetCallback<Protocol::C_VERIFY_EMAIL>(outFunc, Handle_C_VERIFY_EMAIL, session, buffer, len);
		};
		GPacketHandler[PKT_C_LOGIN] = [](std::function<void()>& outFunc, PacketSessionRef& session, BYTE* buffer, int32 len) {
			return GetCallback<Protocol::C_LOGIN>(outFunc, Handle_C_LOGIN, session, buffer, len);
		};
	}

	static bool	PacketHandler(std::function<void()>& outFunc, PacketSessionRef session, BYTE* buffer, int32 len)
	{
		PacketHeader*	header = reinterpret_cast<PacketHeader*>(buffer);
		return GPacketHandler[header->id](outFunc, session, buffer, len);
	}
	static WriteBufferRef MakeWriteBuffer(Protocol::S_SIGNUP& pkt) { return MakeWriteBuffer(pkt, PKT_S_SIGNUP); }
	static WriteBufferRef MakeWriteBuffer(Protocol::S_VERIFY_EMAIL& pkt) { return MakeWriteBuffer(pkt, PKT_S_VERIFY_EMAIL); }
	static WriteBufferRef MakeWriteBuffer(Protocol::S_LOGIN& pkt) { return MakeWriteBuffer(pkt, PKT_S_LOGIN); }

private:
	template<typename PacketType, typename ProcessFunc>
	static bool	GetCallback(std::function<void()>& outFunc, ProcessFunc func, PacketSessionRef session, BYTE* buffer, int32 len)
	{
		PacketType	pkt;
		if (false == pkt.ParseFromArray(buffer + sizeof(PacketHeader), len - sizeof(PacketHeader)))
			return false;
		outFunc = [func, session, pkt](){ func(session, pkt); };
		return true;
	}

	template<typename T>
	static WriteBufferRef	MakeWriteBuffer(T& pkt, uint16 pktId)
	{
		int	headerSize = sizeof(PacketHeader);
		int	pktSize = pkt.ByteSizeLong();
		WriteBufferRef	writeBuffer = std::make_shared<WriteBuffer>(headerSize + pktSize);

		PacketHeader	header;
		header.id = pktId;
		header.size = headerSize + pktSize;

		writeBuffer->AppendBuffer(reinterpret_cast<BYTE*>(&header), headerSize);
		pkt.SerializeToArray(writeBuffer->GetCopyBuffer(), pktSize);
		return writeBuffer;
	}
};

의도대로 잘 만들어졌다.

 

'프로젝트 > Project_Island' 카테고리의 다른 글

16. RedisConnection  (0) 2026.03.08
15. 패킷 자동화 (2)  (0) 2026.03.08
13. DummyClient <-> AuthServer 통신  (0) 2026.03.07
12. DummyClient (2)  (0) 2026.03.06
11. DummyClient (1)  (0) 2026.03.06

+ Recent posts