KoreanFoodie's Study

[C++ 게임 서버] 7-1. DB Connection 본문

Game Dev/Game Server

[C++ 게임 서버] 7-1. DB Connection

GoldGiver 2023. 12. 21. 16:49

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

[C++ 게임 서버] 7-1. DB Connection

핵심 :

1. 실제로 DB 를 연동해 보자. 연결을 위한 DBConnection/DBConnectionPool 을 만들어 보자.

2. 실습에서는 ODBC 서버를 사용할 것이다. 또한 로그는 파일 시스템에 남겨야 추후 버그를 트래킹할 수 있을 것이다. 

이제 게임 서버가 마지막 페이즈에 들어섰다. 기존까지는 로직을 구현했다면, 이제 DB 서버를 연결해 보면서 강의를 마무리하겠다.

그런데... 이제 정말 힘들다. 😹

후.. 그리고 사실 DB 연동을 하는 부분은 모든 API 를 다 외울 필요도 없고, 초기에 세팅을 한 번만 해주면 되는 부분이라, 큰 틀에서 원리 정보만 보고 넘어가도록 하겠다.

먼저, DBConnection 에서 여러 동작을 수행하는 DBConnection 클래스를 만들어 주겠다.

 

DBConnection.h

#pragma once
#include <sql.h>
#include <sqlext.h>

/*----------------
	DBConnection
-----------------*/

class DBConnection
{
public:
	bool			Connect(SQLHENV henv, const WCHAR* connectionString);
	void			Clear();

	bool			Execute(const WCHAR* query);
	bool			Fetch();
	int32			GetRowCount();
	void			Unbind();

public:
	bool			BindParam(SQLUSMALLINT paramIndex, SQLSMALLINT cType, SQLSMALLINT sqlType, SQLULEN len, SQLPOINTER ptr, SQLLEN* index);
	bool			BindCol(SQLUSMALLINT columnIndex, SQLSMALLINT cType, SQLULEN len, SQLPOINTER value, SQLLEN* index);
	void			HandleError(SQLRETURN ret);

private:
	SQLHDBC			_connection = SQL_NULL_HANDLE;
	SQLHSTMT		_statement = SQL_NULL_HANDLE;
};

음... 이름만 봐도, 대충 무슨 역할을 하는지 감이 온다. 사실 SQL 쪽 API 도, WSA 쪽 API 와 근본 원리는 비슷하다고 볼 수 있다.

 

DBConnection.cpp

/*----------------
	DBConnection
-----------------*/

bool DBConnection::Connect(SQLHENV henv, const WCHAR* connectionString)
{
	if (::SQLAllocHandle(SQL_HANDLE_DBC, henv, &_connection) != SQL_SUCCESS)
		return false;

	WCHAR stringBuffer[MAX_PATH] = { 0 };
	::wcscpy_s(stringBuffer, connectionString);

	WCHAR resultString[MAX_PATH] = { 0 };
	SQLSMALLINT resultStringLen = 0;

	SQLRETURN ret = ::SQLDriverConnectW(
		_connection,
		NULL,
		reinterpret_cast<SQLWCHAR*>(stringBuffer),
		_countof(stringBuffer),
		OUT reinterpret_cast<SQLWCHAR*>(resultString),
		_countof(resultString),
		OUT & resultStringLen,
		SQL_DRIVER_NOPROMPT
	);

	if (::SQLAllocHandle(SQL_HANDLE_STMT, _connection, &_statement) != SQL_SUCCESS)
		return false;

	return (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO);
}

void DBConnection::Clear()
{
	if (_connection != SQL_NULL_HANDLE)
	{
		::SQLFreeHandle(SQL_HANDLE_DBC, _connection);
		_connection = SQL_NULL_HANDLE;
	}

	if (_statement != SQL_NULL_HANDLE)
	{
		::SQLFreeHandle(SQL_HANDLE_STMT, _statement);
		_statement = SQL_NULL_HANDLE;
	}
}

bool DBConnection::Execute(const WCHAR* query)
{
	SQLRETURN ret = ::SQLExecDirectW(_statement, (SQLWCHAR*)query, SQL_NTSL);
	if (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO)
		return true;

	HandleError(ret);
	return false;
}

bool DBConnection::Fetch()
{
	SQLRETURN ret = ::SQLFetch(_statement);

	switch (ret)
	{
	case SQL_SUCCESS:
	case SQL_SUCCESS_WITH_INFO:
		return true;
	case SQL_NO_DATA:
		return false;
	case SQL_ERROR:
		HandleError(ret);
		return false;
	default:
		return true;
	}
}

int32 DBConnection::GetRowCount()
{
	SQLLEN count = 0;
	SQLRETURN ret = ::SQLRowCount(_statement, OUT &count);

	if (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO)
		return static_cast<int32>(count);

	return -1;
}

void DBConnection::Unbind()
{
	::SQLFreeStmt(_statement, SQL_UNBIND);
	::SQLFreeStmt(_statement, SQL_RESET_PARAMS);
	::SQLFreeStmt(_statement, SQL_CLOSE);
}

bool DBConnection::BindParam(SQLUSMALLINT paramIndex, SQLSMALLINT cType, SQLSMALLINT sqlType, SQLULEN len, SQLPOINTER ptr, SQLLEN* index)
{
	SQLRETURN ret = ::SQLBindParameter(_statement, paramIndex, SQL_PARAM_INPUT, cType, sqlType, len, 0, ptr, 0, index);
	if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO)
	{
		HandleError(ret);
		return false;
	}

	return true;
}

bool DBConnection::BindCol(SQLUSMALLINT columnIndex, SQLSMALLINT cType, SQLULEN len, SQLPOINTER value, SQLLEN* index)
{
	SQLRETURN ret = ::SQLBindCol(_statement, columnIndex, cType, value, len, index);
	if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO)
	{
		HandleError(ret);
		return false;
	}

	return true;
}

void DBConnection::HandleError(SQLRETURN ret)
{
	if (ret == SQL_SUCCESS)
		return;

	SQLSMALLINT index = 1;
	SQLWCHAR sqlState[MAX_PATH] = { 0 };
	SQLINTEGER nativeErr = 0;
	SQLWCHAR errMsg[MAX_PATH] = { 0 };
	SQLSMALLINT msgLen = 0;
	SQLRETURN errorRet = 0;

	while (true)
	{
		errorRet = ::SQLGetDiagRecW(
			SQL_HANDLE_STMT,
			_statement,
			index,
			sqlState,
			OUT &nativeErr,
			errMsg,
			_countof(errMsg),
			OUT &msgLen
		);

		if (errorRet == SQL_NO_DATA)
			break;

		if (errorRet != SQL_SUCCESS && errorRet != SQL_SUCCESS_WITH_INFO)
			break;

		// TODO : Log
		wcout.imbue(locale("kor"));
		wcout << errMsg << endl;

		index++;
	}
}

실제 구현부를 보면 정신이 조금 아득해지긴 한다. 하지만 기본 적인 원리는 Session 이 하는 일과 뭔가 크게 달라 보이지 않는다. Bind 하고, Handle 하고... 등등.

 

추가로, 위의 DBConnection 을 우리는 Pool 을 통해 관리할 것이다.

DBConnectionPool.h

/*-------------------
	DBConnectionPool
--------------------*/

class DBConnectionPool
{
public:
	DBConnectionPool();
	~DBConnectionPool();

	bool					Connect(int32 connectionCount, const WCHAR* connectionString);
	void					Clear();

	DBConnection*			Pop();
	void					Push(DBConnection* connection);

private:
	USE_LOCK;
	SQLHENV					_environment = SQL_NULL_HANDLE;
	Vector<DBConnection*>	_connections;
};

 

DBConnectionPool.cpp

DBConnectionPool::DBConnectionPool()
{

}

DBConnectionPool::~DBConnectionPool()
{
	Clear();
}

bool DBConnectionPool::Connect(int32 connectionCount, const WCHAR* connectionString)
{
	WRITE_LOCK;

	if (::SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &_environment) != SQL_SUCCESS)
		return false;

	if (::SQLSetEnvAttr(_environment, SQL_ATTR_ODBC_VERSION, reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3), 0) != SQL_SUCCESS)
		return false;

	for (int32 i = 0; i < connectionCount; i++)
	{
		DBConnection* connection = xnew<DBConnection>();
		if (connection->Connect(_environment, connectionString) == false)
			return false;

		_connections.push_back(connection);
	}

	return true;
}

void DBConnectionPool::Clear()
{
	WRITE_LOCK;

	if (_environment != SQL_NULL_HANDLE)
	{
		::SQLFreeHandle(SQL_HANDLE_ENV, _environment);
		_environment = SQL_NULL_HANDLE;
	}

	for (DBConnection* connection : _connections)
		xdelete(connection);

	_connections.clear();
}

DBConnection* DBConnectionPool::Pop()
{
	WRITE_LOCK;

	if (_connections.empty())
		return nullptr;

	DBConnection* connection = _connections.back();
	_connections.pop_back();
	return connection;
}

void DBConnectionPool::Push(DBConnection* connection)
{
	WRITE_LOCK;
	_connections.push_back(connection);
}

그나마 좀 살펴볼 만한 부분은 Connect 이다. Connect 를 보면, connectionCount 만큼 연결을 수행해 주는 것을 알 수 있다.

그리고 당연하게도, DBConnectionPool 의 경우, CoreGlobal 에서 관리해 줄 것이다.

extern class DBConnectionPool*	GDBConnectionPool;
DBConnectionPool*	GDBConnectionPool = nullptr;

class CoreGlobal
{
public:
	CoreGlobal()
	{
		GDBConnectionPool = new DBConnectionPool();
	}

	~CoreGlobal()
	{
		delete GDBConnectionPool;
	}
} GCoreGlobal;

 

그럼 이제 GameServer 에서 아래와 같이 SQL 서버를 연동하고, 쿼리를 쏴볼 수 있다.

ASSERT_CRASH(GDBConnectionPool->Connect(1, L"Driver={SQL Server Native Client 11.0};Server=(localdb)\\MSSQLLocalDB;Database=ServerDb;Trusted_Connection=Yes;"));

// Create Table
{
    auto query = L"									\
        DROP TABLE IF EXISTS [dbo].[Gold];			\
        CREATE TABLE [dbo].[Gold]					\
        (											\
            [id] INT NOT NULL PRIMARY KEY IDENTITY, \
            [gold] INT NULL							\
        );";

    DBConnection* dbConn = GDBConnectionPool->Pop();
    ASSERT_CRASH(dbConn->Execute(query));
    GDBConnectionPool->Push(dbConn);
}

// Add Data
for (int32 i = 0; i < 3; i++)
{
    DBConnection* dbConn = GDBConnectionPool->Pop();
    // 기존에 바인딩 된 정보 날림
    dbConn->Unbind();

    // 넘길 인자 바인딩
    int32 gold = 100;
    SQLLEN len = 0;

    // 넘길 인자 바인딩
    ASSERT_CRASH(dbConn->BindParam(1, SQL_C_LONG, SQL_INTEGER, sizeof(gold), &gold, &len));

    // SQL 실행
    ASSERT_CRASH(dbConn->Execute(L"INSERT INTO [dbo].[Gold]([gold]) VALUES(?)"));

    GDBConnectionPool->Push(dbConn);
}

// Read
{
    DBConnection* dbConn = GDBConnectionPool->Pop();
    // 기존에 바인딩 된 정보 날림
    dbConn->Unbind();

    int32 gold = 100;
    SQLLEN len = 0;
    // 넘길 인자 바인딩
    ASSERT_CRASH(dbConn->BindParam(1, SQL_C_LONG, SQL_INTEGER, sizeof(gold), &gold, &len));

    int32 outId = 0;
    SQLLEN outIdLen = 0;
    ASSERT_CRASH(dbConn->BindCol(1, SQL_C_LONG, sizeof(outId), &outId, &outIdLen));

    int32 outGold = 0;
    SQLLEN outGoldLen = 0;
    ASSERT_CRASH(dbConn->BindCol(2, SQL_C_LONG, sizeof(outGold), &outGold, &outGoldLen));

    // SQL 실행
    ASSERT_CRASH(dbConn->Execute(L"SELECT id, gold FROM [dbo].[Gold] WHERE gold = (?)"));

    while (dbConn->Fetch())
    {
        cout << "Id: " << outId << " Gold : " << outGold << endl;
    }

    GDBConnectionPool->Push(dbConn);
}

잘 보면 여기서 DB 에 gold 와 len 을 3 번 넘겨주고 있는데, 실제 DB 데이터를 까보면 아래와 같이 정상적으로 들어감을 확인할 수 있다.

참고로 위 스샷은, 강의에서 파쿠리(?) 한 것이다 😅

'Game Dev > Game Server' 카테고리의 다른 글

[C++ 게임 서버] 7-3. XML Parser  (0) 2023.12.21
[C++ 게임 서버] 7-2. DB Bind  (0) 2023.12.21
[C++ 게임 서버] 6-7. JobTimer  (0) 2023.12.21
[C++ 게임 서버] 6-6. JobQueue #5  (0) 2023.12.20
[C++ 게임 서버] 6-5. JobQueue #4  (0) 2023.12.20
Comments