이전에 Protobuf를 사용한 패킷 핸들러를 자동화할 때와 동일하게,
Python
과 Jinja2
를 활용할 것이다.
1. XML 파서 작성
서버 소스가 아니라 Python
에서 사용할 XML 파서를 작성해야 한다.
import xml.etree.ElementTree as ET
class XmlDBParser:
def __init__(self):
self.tables = {}
self.procedures = []
def parse_xml(self, path):
tree = ET.parse(path)
root = tree.getroot()
for child in root:
if child.tag == 'Table':
self.tables[child.attrib['name']] = Table(child)
for child in root:
if child.tag == 'Procedure':
self.procedures.append(Procedure(child, self.tables))
# 적당히 필요한 정보만 뽑는다
# 테이블에 대해 모든 것을 알 필요는 없음!
class Table:
def __init__(self, node):
self.name = node.attrib['name']
self.columns = {}
for child in node:
if child.tag == 'Column':
self.columns[child.attrib['name']] = ReplaceType(child.attrib['type'])
class Procedure:
def __init__(self, node, tables):
name = node.attrib['name']
if name.startswith('sp'):
self.name = name[2:]
else:
self.name = name
self.params = []
for child in node:
if child.tag == 'Param':
self.params.append(Param(child))
elif child.tag == 'Body':
self.columns = ParseColumns(child, tables)
self.questions = MakeQuestions(self.params)
class Param: # ...
class Column: # ...
# SELECT에 뭐가 있는지, 그 개수는 몇개인지 등을 알아야 한다.
# 몇개의 정보를 가져와야 하고, 인자로는 어떤게 있으며 어떤 테이블이 대상인지...
# 이런 데이터를 관리하기 위함
def ParseColumns(node, tables):
columns = []
query = node.text
select_idx = max(query.rfind('SELECT'), query.rfind('select'))
from_idx = max(query.rfind('FROM'), query.rfind('from'))
if select_idx > 0 and from_idx > 0 and from_idx > select_idx:
table_name = query[from_idx+len('FROM') : -1].strip().split()[0]
table_name = table_name.replace('[', '').replace(']', '').replace('dbo.', '')
table = tables.get(table_name)
words = query[select_idx+len('SELECT') : from_idx].strip().split(",")
for word in words:
column_name = word.strip().split()[0]
columns.append(Column(column_name, table.columns[column_name]))
elif select_idx > 0:
word = query[select_idx+len('SELECT') : -1].strip().split()[0]
if word.startswith('@@ROWCOUNT') or word.startswith('@@rowcount'):
columns.append(Column('RowCount', 'int64'))
elif word.startswith('@@IDENTITY') or word.startswith('@@identity'):
columns.append(Column('Identity', 'int64'))
return columns
# 인자를 넣기 위한 ? 부분
def MakeQuestions(params): #...
def ReplaceType(type):
if type == 'bool':
return 'bool'
if type == 'int':
return 'int32'
if type == 'bigint':
return 'int64'
if type == 'datetime':
return 'TIMESTAMP_STRUCT'
if type.startswith('nvarchar'):
return 'nvarchar'
return type
Python
에서 XML
을 사용하기 위해 xml.etree.ElementTree
를 Import
해야 한다.
전체적인 흐름은 아래와 같다.
- 경로를 넘겨받음
parse_xml()
호출- 노드를 순회하며
Table
및Procedure
를 파싱해 저장
- 각Procedure
의Body
를 파싱 하기 위해
-ParseColumns()
및MakeQuestions()
호출 - 외부에서 XML로부터 파싱 된 데이터 사용
길이가 길어서 좀 쫄긴 했는데....
코드 자체는 파이썬 코드를 대충이라도 볼 줄 안다면 다 보이는 것들이긴 하다.
2. 템플릿
패킷 뽑던 때와는 조금 다르다.
#pragma once
#include "Types.h"
#include <windows.h>
#include "DBBind.h"
{%- macro gen_procedures() -%} {% include 'Procedure.h' %} {% endmacro %}
namespace SP
{
{{gen_procedures() | indent}}
};
템플릿 안에 또 다른 템플릿이 있는 구조다.
부분으로 나누어 작성의 효율성을 높이고 가독성의 향상을 노릴 수 있다.
실제로 내용물이 담기는 부분은 아래와 같다.
{%- macro lower_first(text) %}{{text[0]|lower}}{{text[1:]}}{% endmacro -%}
{%- for proc in procs %}
class {{proc.name}} : public DBBind<{{proc.params|length}},{{proc.columns|length}}>
{
public:
{{proc.name}}(DBConnection& conn) : DBBind(conn, L"{CALL dbo.sp{{ proc.name }}{{ proc.questions }}}") { }
{%- for param in proc.params %}
{%- if param.type == 'nvarchar' %}
template<int32 N> void In_{{param.name}}(WCHAR(&v)[N]) { BindParam({{loop.index - 1}}, v); };
template<int32 N> void In_{{param.name}}(const WCHAR(&v)[N]) { BindParam({{loop.index - 1}}, v); };
void In_{{param.name}}(WCHAR* v, int32 count) { BindParam({{loop.index - 1}}, v, count); };
void In_{{param.name}}(const WCHAR* v, int32 count) { BindParam({{loop.index - 1}}, v, count); };
{%- elif param.type == 'varbinary' %}
template<typename T, int32 N> void In_{{param.name}}(T(&v)[N]) { BindParam({{loop.index - 1}}, v); };
template<typename T> void In_{{param.name}}(T* v, int32 count) { BindParam({{loop.index - 1}}, v, count); };
{%- else %}
void In_{{param.name}}({{param.type}}& v) { BindParam({{loop.index - 1}}, v); };
void In_{{param.name}}({{param.type}}&& v) { _{{lower_first(param.name)}} = std::move(v); BindParam({{loop.index - 1}}, _{{lower_first(param.name)}}); };
{%- endif %}
{%- endfor %}
{%- for column in proc.columns %}
{%- if column.type == 'nvarchar' %}
template<int32 N> void Out_{{column.name}}(OUT WCHAR(&v)[N]) { BindCol({{loop.index - 1}}, v); };
{%- elif column.type == 'varbinary' %}
template<typename T, int32 N> void Out_{{column.name}}(OUT T(&v)[N]) { BindCol({{loop.index - 1}}, v); }
{%- else %}
void Out_{{column.name}}(OUT {{column.type}}& v) { BindCol({{loop.index - 1}}, v); };
{%- endif %}
{%- endfor %}
private:
{%- for param in proc.params %}
{%- if param.type == 'int32' or param.type == 'TIMESTAMP_STRUCT' %}
{{param.type}} _{{lower_first(param.name)}} = {};
{%- endif %}
{%- endfor %}
};
{% endfor %}
- 각
Procedure
마다 클래스를 작성한다. Params
및Columns
의 개수도 알아서 맞춰준다.- 파라미터의 타입에 따라 함수를 다르게 생성하도록 한다.
- 공통적으로 오른값을 다 받을 수 있다. private
영역엔 사용될 변수를 둔다.
코드의 양이 많으므로 강의 중 이런 코드들에 대한 상세한 분석이 없었으므로,
완성본을 보면서 코드의 동작을 유추해 나가는 것이 꽤 도움이 됐다.
3. Generator 작성
이 부분은 사실상 기존에 작성된 PacketGenerator
의 코드를 그대로 갖다가 쓸 수 있다.
import argparse
import jinja2
import XmlDBParser
def main():
arg_parser = argparse.ArgumentParser(description = 'StoredProcedure Generator')
arg_parser.add_argument('--path', type=str, default='H:/C++/Learning/Server/37_Procedure+Generator/GameServer/GameServer/GameDB.xml', help='Xml Path')
arg_parser.add_argument('--output', type=str, default='GenProcedures.h', help='Output File')
args = arg_parser.parse_args()
if args.path == None or args.output == None:
print('[Error] --path --output required')
return
parser = XmlDBParser.XmlDBParser()
parser.parse_xml(args.path)
file_loader = jinja2.FileSystemLoader('Templates')
env = jinja2.Environment(loader=file_loader)
template = env.get_template('GenProcedures.h')
output = template.render(procs=parser.procedures)
f = open(args.output, 'w+')
f.write(output)
f.close()
print(output)
if __name__ == '__main__':
main()
패킷 관련 내용이 제거되고 XML을 파싱 하는 내용이 들어가 있다 정도의 차이만을 가진다.
pushd %~dp0
GenProcs.exe --path=../../GameServer/GameDB.xml --output=GenProcedures.h
IF ERRORLEVEL 1 PAUSE
XCOPY /Y GenProcedures.h "../../GameServer"
DEL /Q /F *.h
PAUSE
배치 파일로 그렇게 다르진 않다.
옵션과 경로만 다시 잡아주면 끝.
마지막으로 스크립트를 pyinstaller
를 사용해 실행 가능한 파일로 만들면 끝.
4. 스크립트 실행 및 테스트
이렇게 완성된 스크립트를 실행하면 아래와 같은 결과가 나온다.
#pragma once
#include "Types.h"
#include <windows.h>
#include "DBBind.h"
namespace SP
{
class InsertGold : public DBBind<3,0>
{
public:
InsertGold(DBConnection& conn) : DBBind(conn, L"{CALL dbo.spInsertGold(?,?,?)}") { }
void In_Gold(int32& v) { BindParam(0, v); };
void In_Gold(int32&& v) { _gold = std::move(v); BindParam(0, _gold); };
template<int32 N> void In_Name(WCHAR(&v)[N]) { BindParam(1, v); };
template<int32 N> void In_Name(const WCHAR(&v)[N]) { BindParam(1, v); };
void In_Name(WCHAR* v, int32 count) { BindParam(1, v, count); };
void In_Name(const WCHAR* v, int32 count) { BindParam(1, v, count); };
void In_CreateDate(TIMESTAMP_STRUCT& v) { BindParam(2, v); };
void In_CreateDate(TIMESTAMP_STRUCT&& v) { _createDate = std::move(v); BindParam(2, _createDate); };
private:
int32 _gold = {};
TIMESTAMP_STRUCT _createDate = {};
};
class GetGold : public DBBind<1,4>
{
public:
GetGold(DBConnection& conn) : DBBind(conn, L"{CALL dbo.spGetGold(?)}") { }
void In_Gold(int32& v) { BindParam(0, v); };
void In_Gold(int32&& v) { _gold = std::move(v); BindParam(0, _gold); };
void Out_Id(OUT int32& v) { BindCol(0, v); };
void Out_Gold(OUT int32& v) { BindCol(1, v); };
template<int32 N> void Out_Name(OUT WCHAR(&v)[N]) { BindCol(2, v); };
void Out_CreateDate(OUT TIMESTAMP_STRUCT& v) { BindCol(3, v); };
private:
int32 _gold = {};
};
};
잘 되나 확인하기 위해 다시 서버로 돌아가자.
{
WCHAR name[] = L"Todd Howard";
SP::InsertGold insertGold(*dbConn);
insertGold.In_Gold(100);
insertGold.In_Name(name);
insertGold.In_CreateDate(TIMESTAMP_STRUCT{ 2023, 9, 1 });
insertGold.Execute();
}
{
SP::GetGold getGold(*dbConn);
getGold.In_Gold(100);
int32 id = 0;
int32 gold = 0;
WCHAR name[100];
TIMESTAMP_STRUCT date;
getGold.Out_Id(OUT id);
getGold.Out_Gold(OUT gold);
getGold.Out_Name(OUT name);
getGold.Out_CreateDate(OUT date);
getGold.Execute();
while (getGold.Fetch())
{
GConsoleLogger->WriteStdOut(Color::BLUE,
L"ID[%d] Gold[%d] Name[%s]\n", id, gold, name);
}
}
정말 코드가 간결 해졌다는 것이 느껴진다.
실행 결과는 아래와 같다.
문제는 한글이 출력되지 않는다.
이게 참 이상한 게...
메모리를 봤을 때 깨지는 것 없이 잘 왔다는 것은 출력에 문제가 있다는 것을 의미한다.
하지만 작성한 로그 클래스는 아래와 같이 출력을 처리한다.
va_list ap;
va_start(ap, format);
::vwprintf(format, ap);
va_end(ap);
vwprintf()
를 사용하기 때문에 와이드 캐릭터 출력에 이상이 없어야 한다.
이 상황을 이해할 수가 없어서 ChatGPT한테도 물어봤지만 영 소득이 없었다...
당장 이거만 판다고 해답이 나올 거 같지는 않아서 다음에 알아보는 게 좋을 것 같다.
여하튼 강의 자체는 마무리가 됐다.
게임 서버에 관해 이렇게 공부할 기회를 준 Rookiss 님에게 참으로 감사할 따름이다.
물론 모든 것을 이해했다고 볼 수는 없기 때문에 지속적인 복습이 필요할 것이다.
특히 모호하다 싶은 부분이 몇 가지 있으므로 이를 복습해 나갈 것 같다.
일부 사설 서버의 소스코드도 조금이나마 보이게 된 것이 그렇게 공부를 헛 하지는 않았나 보다.
안 보이던 코드가 보이게 되니 성취감을 느끼는 것 같아 동기부여에 매우 긍정적인 것 같아 만족스럽다.
'Study > C++ & C#' 카테고리의 다른 글
[C#] ###Clicker (0) | 2024.02.29 |
---|---|
[C#] 자동화 플러그인 수정 (0) | 2023.10.17 |
[C++ / DB] ORM (0) | 2023.09.01 |
[C++ / DB] XML Parser (0) | 2023.08.31 |
[C++ / DB] DBBind (0) | 2023.08.29 |