KoreanFoodie's Study
[C++ 게임 서버] 7-1. DB Connection 본문
[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 |