KoreanFoodie's Study

[C++ 게임 서버] 7-3. XML Parser 본문

Game Dev/Game Server

[C++ 게임 서버] 7-3. XML Parser

GoldGiver 2023. 12. 21. 19:13

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 7-3. XML Parser

핵심 :

1. DB 도 버전 관리가 필요하며, 따라서 테이블과 쿼리는 버전에 맞게 존재해야 한다. 이를 위한 XML Parser 를 만들어 본다.

프로젝트를 진행하다 보면 자연스럽게 버전 관리가 필요하다. 이는 사실 DB 에도 마찬가지인데, 어떤 버전에서는 특정 테이블에 특정 컬럼이 없다던지, 추후 버전에서 테이블이 추가된다던지 하는 경우가 생길 것이다.

따라서 쿼리도 그런 DB 의 변동에 맞게 내용물이 바뀌어야 하는데... 보통 이를 관리해 주는 직군이 DBA 이다(물론 서버 담당자가 하기도 한다).

 

우리는 앞으로 이런 DB 쿼리를 XML 로 관리한 다음, 실제로 GameServer 에서 쿼리를 날려줄 때 XML Parser 를 이용해 해당 쿼리가 자동으로 생성되도록 만들 것이다! 😁

일단 그러려면 RapidXML 라이브러리를 다운 받고, 이것 저것 세팅을 해야 하는데... 사실 자세한 내용은 굳이 암기까지 할 필요는 없으므로, 이번 글에서는 내용을 소개만 하고 넘어가겠다.

 

먼저, File 입출력을 위한 FileUtils 클래스가 필요하다.

/*-----------------
	FileUtils
------------------*/

class FileUtils
{
public:
	static Vector<BYTE>		ReadFile(const WCHAR* path);
	static String			Convert(string str);
};

//////////////////////////////////////////////////////////////////////////

namespace fs = std::filesystem;

Vector<BYTE> FileUtils::ReadFile(const WCHAR* path)
{
	Vector<BYTE> ret;

	fs::path filePath{ path };

	const uint32 fileSize = static_cast<uint32>(fs::file_size(filePath));
	ret.resize(fileSize);

	basic_ifstream<BYTE> inputStream{ filePath };
	inputStream.read(&ret[0], fileSize);

	return ret;
}

String FileUtils::Convert(string str)
{
	const int32 srcLen = static_cast<int32>(str.size());

	String ret;
	if (srcLen == 0)
		return ret;

	const int32 retLen = ::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<char*>(&str[0]), srcLen, NULL, 0);
	ret.resize(retLen);
	::MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<char*>(&str[0]), srcLen, &ret[0], retLen);

	return ret;
}

 

그리고 대망의 XMLParser 를 만들어 줄 것이다. 아참, 우리가 만들 DB 에 대한 구조 및 쿼리가 다음과 같다고 하겠다 :

GameDB.xml

<?xml version="1.0" encoding="utf-8"?>
<GameDB>
	<Table name="Gold" desc="골드 테이블">
		<Column name="id" type="int" notnull="true" />
		<Column name="gold" type="int" notnull="false" />
		<Column name="name" type="nvarchar(50)" notnull="false" />
		<Column name="createDate" type="datetime" notnull="false" />
		<Index type="clustered">
			<PrimaryKey/>
			<Column name="id" />
		</Index>
	</Table>

	<Procedure name="spInsertGold">
		<Param name="@gold" type="int"/>
		<Param name="@name" type="nvarchar(50)"/>
		<Param name="@createDate" type="datetime"/>
		<Body>
			<![CDATA[
			INSERT INTO [dbo].[Gold]([gold], [name], [createDate]) VALUES(@gold, @name, @createDate);
			]]>
		</Body>		
	</Procedure>
	
	<Procedure name="spGetGold">
		<Param name="@gold" type="int"/>
		<Body>
			<![CDATA[
			SELECT id, gold, name, createDate FROM [dbo].[Gold] WHERE gold = (@gold)
			]]>
		</Body>
	</Procedure>
	
</GameDB>

이때, Node 라는 개념이 있는데... 대충 여기서는 첫 노드가 GameDB 가 된다는 것을 알아두자(이따가 쓸 거임).

 

XMLParser 를 만들기 위해, XML 로부터 값들을 편하게 가져오는 Wrapper 클래스 XmlNode 를 다음과 같이 만들 것이다.

using XmlNodeType = xml_node<WCHAR>;
using XmlDocumentType = xml_document<WCHAR>;
using XmlAttributeType = xml_attribute<WCHAR>;

class XmlNode
{
public:
	XmlNode(XmlNodeType* node = nullptr) : _node(node) { }

	bool				IsValid() { return _node != nullptr; }

	bool				GetBoolAttr(const WCHAR* key, bool defaultValue = false);
	int8				GetInt8Attr(const WCHAR* key, int8 defaultValue = 0);
	int16				GetInt16Attr(const WCHAR* key, int16 defaultValue = 0);
	int32				GetInt32Attr(const WCHAR* key, int32 defaultValue = 0);
	int64				GetInt64Attr(const WCHAR* key, int64 defaultValue = 0);
	float				GetFloatAttr(const WCHAR* key, float defaultValue = 0.0f);
	double				GetDoubleAttr(const WCHAR* key, double defaultValue = 0.0);
	const WCHAR*		GetStringAttr(const WCHAR* key, const WCHAR* defaultValue = L"");

	bool				GetBoolValue(bool defaultValue = false);
	int8				GetInt8Value(int8 defaultValue = 0);
	int16				GetInt16Value(int16 defaultValue = 0);
	int32				GetInt32Value(int32 defaultValue = 0);
	int64				GetInt64Value(int64 defaultValue = 0);
	float				GetFloatValue(float defaultValue = 0.0f);
	double				GetDoubleValue(double defaultValue = 0.0);
	const WCHAR*		GetStringValue(const WCHAR* defaultValue = L"");

	XmlNode				FindChild(const WCHAR* key);
	Vector<XmlNode>		FindChildren(const WCHAR* key);

private:
	XmlNodeType*		_node = nullptr;
};

////////////////////////////////////////////////////////////////////////////////////

_locale_t kr = _create_locale(LC_NUMERIC, "kor");

bool XmlNode::GetBoolAttr(const WCHAR* key, bool defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return ::_wcsicmp(attr->value(), L"true") == 0;

	return defaultValue;
}

int8 XmlNode::GetInt8Attr(const WCHAR* key, int8 defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return static_cast<int8>(::_wtoi(attr->value()));

	return defaultValue;
}

int16 XmlNode::GetInt16Attr(const WCHAR* key, int16 defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return static_cast<int16>(::_wtoi(attr->value()));

	return defaultValue;
}

int32 XmlNode::GetInt32Attr(const WCHAR* key, int32 defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return ::_wtoi(attr->value());

	return defaultValue;
}

int64 XmlNode::GetInt64Attr(const WCHAR* key, int64 defaultValue)
{
	xml_attribute<WCHAR>* attr = _node->first_attribute(key);
	if (attr)
		return ::_wtoi64(attr->value());

	return defaultValue;
}

float XmlNode::GetFloatAttr(const WCHAR* key, float defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return static_cast<float>(::_wtof(attr->value()));

	return defaultValue;
}

double XmlNode::GetDoubleAttr(const WCHAR* key, double defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return ::_wtof_l(attr->value(), kr);

	return defaultValue;
}

const WCHAR* XmlNode::GetStringAttr(const WCHAR* key, const WCHAR* defaultValue)
{
	XmlAttributeType* attr = _node->first_attribute(key);
	if (attr)
		return attr->value();

	return defaultValue;
}

bool XmlNode::GetBoolValue(bool defaultValue)
{
	WCHAR* val = _node->value();
	if (val)
		return ::_wcsicmp(val, L"true") == 0;

	return defaultValue;
}

int8 XmlNode::GetInt8Value(int8 defaultValue)
{
	WCHAR* val = _node->value();
	if (val)
		return static_cast<int8>(::_wtoi(val));

	return defaultValue;
}

int16 XmlNode::GetInt16Value(int16 defaultValue)
{
	WCHAR* val = _node->value();
	if (val)
		return static_cast<int16>(::_wtoi(val));
	return defaultValue;
}

int32 XmlNode::GetInt32Value(int32 defaultValue)
{
	WCHAR* val = _node->value();
	if (val)
		return static_cast<int32>(::_wtoi(val));

	return defaultValue;
}

int64 XmlNode::GetInt64Value(int64 defaultValue)
{
	WCHAR* val = _node->value();
	if (val)
		return static_cast<int64>(::_wtoi64(val));

	return defaultValue;
}

float XmlNode::GetFloatValue(float defaultValue)
{
	WCHAR* val = _node->value();
	if (val)
		return static_cast<float>(::_wtof(val));

	return defaultValue;
}

double XmlNode::GetDoubleValue(double defaultValue)
{
	WCHAR* val = _node->value();
	if (val)
		return ::_wtof_l(val, kr);

	return defaultValue;
}

const WCHAR* XmlNode::GetStringValue(const WCHAR* defaultValue)
{
	WCHAR* val = _node->first_node()->value();
	if (val)
		return val;

	return defaultValue;
}

XmlNode XmlNode::FindChild(const WCHAR* key)
{
	return XmlNode(_node->first_node(key));
}

Vector<XmlNode> XmlNode::FindChildren(const WCHAR* key)
{
	Vector<XmlNode> nodes;

	xml_node<WCHAR>* node = _node->first_node(key);
	while (node)
	{
		nodes.push_back(XmlNode(node));
		node = node->next_sibling(key);
	}

	return nodes;
}

 

그럼 위의 XmlNode 를 사용한 XmlParser 는 다음과 같이 간단하게 만들 수 있게 된다 :

class XmlParser
{
public:
	bool ParseFromFile(const WCHAR* path, OUT XmlNode& root);

private:
	shared_ptr<XmlDocumentType>		_document = nullptr;
	String							_data;
};

//////////////////////////////////////////////////////////////////////////////////////////

bool XmlParser::ParseFromFile(const WCHAR* path, OUT XmlNode& root)
{
	Vector<BYTE> bytes = FileUtils::ReadFile(path);
	_data = FileUtils::Convert(string(bytes.begin(), bytes.end()));

	if (_data.empty())
		return false;

	_document = MakeShared<XmlDocumentType>();
	_document->parse<0>(reinterpret_cast<WCHAR*>(&_data[0]));
	root = XmlNode(_document->first_node());
	return true;
}

ParseFromFile 은, 그냥 첫 노드로부터 순차적으로 파싱을 해 줄 뿐이다. 🤣

 

그럼 이제 GameServer 에서는, 아래와 같이 XML 을 적절히 파싱하여 버전에 맞는 쿼리를 자동으로 생성해 줄 것이다 😊

XmlNode root;
XmlParser parser;
if (parser.ParseFromFile(L"GameDB.xml", OUT root) == false)
    return 0;

Vector<XmlNode> tables = root.FindChildren(L"Table");
for (XmlNode& table : tables)
{
    String name = table.GetStringAttr(L"name");
    String desc = table.GetStringAttr(L"desc");

    Vector<XmlNode> columns = table.FindChildren(L"Column");
    for (XmlNode& column : columns)
    {
        String colName = column.GetStringAttr(L"name");
        String colType = column.GetStringAttr(L"type");
        bool nullable = !column.GetBoolAttr(L"notnull", false);
        String identity = column.GetStringAttr(L"identity");
        String colDefault = column.GetStringAttr(L"default");
        // Etc...
    }

    Vector<XmlNode> indices = table.FindChildren(L"Index");
    for (XmlNode& index : indices)
    {
        String indexType = index.GetStringAttr(L"type");
        bool primaryKey = index.FindChild(L"PrimaryKey").IsValid();
        bool uniqueConstraint = index.FindChild(L"UniqueKey").IsValid();

        Vector<XmlNode> columns = index.FindChildren(L"Column");
        for (XmlNode& column : columns)
        {
            String colName = column.GetStringAttr(L"name");
        }
    }
}

Vector<XmlNode> procedures = root.FindChildren(L"Procedure");
for (XmlNode& procedure : procedures)
{
    String name = procedure.GetStringAttr(L"name");
    String body = procedure.FindChild(L"Body").GetStringValue();

    Vector<XmlNode> params = procedure.FindChildren(L"Param");
    for (XmlNode& param : params)
    {
        String paramName = param.GetStringAttr(L"name");
        String paramType = param.GetStringAttr(L"type");
        // TODO..
    }
}
 
Comments