bdfgdfg

[온라인 서버] 네트워크 라이브러리 - Log Class 본문

게임프로그래밍/서버 책

[온라인 서버] 네트워크 라이브러리 - Log Class

marmelo12 2022. 1. 10. 18:15
반응형

Log Class

 - 온라인 게임 서버에서 로그가 차지하는 중요성은 매우 높다.

 - 온라인 게임 서버에서 로그는 현재 상태를 사용자에게 정확하고 자세하게 알려줄 수 있는 유일한 도구.

  -> 로그를 정확하게, 필요할 때 남기지 못한다면 서버가 제대로 동작하는지 문제가 없는지에 대한 파악이 어렵다.

 - Log클래스는 로그의 종류를 알림과 에러로 나눔으로써 자신이 원하는 정보를 보다 빠르게 찾게 한다.

  -> 중요도에 따라서 LOW, NORMAL, HIGH, CRITICAL 등 모두 4가지 등급을 두어 쉽게 정보의 중요성을 파악.

 - 로그를 남기는 것은 중요하지만 꼭 필요한 것들을 로그로 남기는 게 좋다.

  -> 1. 로그가 어떤 함수에서 남겨졌는지 알아야 한다.

  -> 2. 누구에 의해서 로그가 남겨졌는지를 알아야 한다.(여기서 누구는 시스템,패킷을 보낸 클라이언트등)

  -> 3. 로그가 발생되는 시간을 남겨야 한다.

  -> 4. 로그의 종류가 에러라면 에러가 어떤 작업을 하다가 발생한 것인지 남겨야하고 종류가 알림이라면 함수의 인자나 받은 패킷의 프로토콜 내용을 남겨야 한다.

 

아래는 Log클래스에서 사용할 로그의 종류이다. (알림과 에러)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//각 등급마다 16진수설정.
// OR(|)연산을 위해 2^n승씩 증가.
// 0000/0000 에서 비트는 2^n의 자리를 차지.
enum class enumLogInfoType
{
      LOG_NONE               = 0x00000000
    , LOG_INFO_LOW           = 0x00000001
    , LOG_INFO_NORMAL      = 0x00000002
    , LOG_INFO_HIGH           = 0x00000004
    , LOG_INFO_CRITICAL    = 0x00000008
    , LOG_INFO_ALL           = 0x0000000F
    , LOG_ERROR_LOW           = 0x00000010
    , LOG_ERROR_NORMAL     = 0x00000020
    , LOG_ERROR_HIGH       = 0x00000040
    , LOG_ERROR_CRITICA    = 0x00000080
    , LOG_ERROR_ALL           = 0x00000100
    , LOG_ALL               = 0x000001FF
};
cs

(컬러 스크립트 최고)

 

위에 나열된 값들은 각 등급마다 정해진 상수를 16진수로 되어있고 값은 OR('|') 연산을 통해 합칠 수 있도록(비트 안 겹치게) 2의 n승.

 

다음으로는 Log클래스에서 정보의 분류별로 여러가지 방법으로 로그를 저장할 수 있게 설계.

로그를 저장할 매체는 파일, 출력 창, 윈도우, DB, TCP, UDP로 분류.

Log클래스에서는 DB를 제외하고는 모두 구현되어있다. -> DB의 추가에 따라 OutputDB함수에 코드 추가해 사용.

1
2
3
4
5
6
7
8
9
10
11
12
// 정보의 분류별로 여러가지 방법으로 로그를 저장한다.
// 즉 로그를 저장할 매체에 대한 선언
enum class enumLogStorageType
{
      STORAGE_FILE        = 0x000000000
    , STORAGE_DB        = 0x000000001
    , STORAGE_WINDOW    = 0x000000002
    , STORAGE_OUTPUTWND = 0x000000003
    , STORAGE_UDP        = 0x000000004
    , STORAGE_TCP        = 0x000000005
};
 
cs

이렇게 enumLogStorageType은 로그를 저장할 매체에 대한 선언.

 

 - Log클래스는 어디서든지 로그를 사용하기 위해 Singleton클래스를 상속받는다.

  -> 또한 현재 처리되는 로직에 방해되지 않도록 최소한의 처리(로그를 내부큐(Queue)에 넣고 바로 반환))

  -> 큐에 넣어진 데이터는 내부적으로 틱 쓰레드를 사용하여 일정 시간마다 큐에 들어있던 로그를 가져와 처리한다.

1
2
3
4
class Log : public Thread, public Singleton
{
    DECLARE_SINGLETON(Log);
    ....
cs

 

가변 인자 함수.

가변 인자 함수란 말 그대로 고정되지 않은 인자를 말한다.(C언어의 Printf 함수와 같이)

가변 인자는 인자를... (점 3개)로 표시.

참고로 가변인자를 가지는 함수를 만들기 위해서는 고정 인자가 하나 이상은 반드시 필요하다. 또한 고정 인자들은

가변 인자보다 앞에 와야 한다. 고정 인자가 필요한 이유는 뒤에 나올 va_start의 인자로 사용하기 때문.

va_list는 가변인자의 주소를 나타낼 포인터.

va_start는 인자로 위의 va_list포인터를 넘기고 두번째 인자로 고정인자를 넘기는데. 이와 같은 행동은

초기화 작업으로서 고정 인자 바로 뒤에 가변 인자들을 위치시키고 va_list에 첫 가변인자의 주소를 할당한다.

 

출처 - https://hydroponicglass.tistory.com/285

va_end는 va_list를 해제 (사용후 해제해야 함)

중요한건 vsprintf함수인데 가변인자에 담긴 포맷 정보가 문자열에 자동으로 담기면서 문자열에 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void EnQueueLog(enumLogInfoType eLogInfoType, const char* outputString, ...)
{
    Monitor::Owner lock(g_log);
    UINT queueCnt = GetLog()->GetQueueSize();
    //현재 큐 사이즈를 초과했다면
    if (queueCnt >= MAX_QUEUE_CNT)
        return;
 
    va_list argptr;
    // argptr이 맨 처음 가변인수를 가르키도록 초기화.
    va_start(argptr, outputString);
    vsprintf(g_logMsg[queueCnt].m_outputString, outputString, argptr);
    va_end(argptr);
 
    g_logMsg[queueCnt].m_eLogInfoType = eLogInfoType;
    GetLog()->InsertMsgToQueue(&g_logMsg[queueCnt]);
}
cs

문자열에 저장 후 해당 로그 정보를 LogMsg 전역배열에 담고 큐에 넣어준다.

실제로 로그를 사용해보면

EnQueueLog(enumLogInfoType::LOG_ERROR_CRITICAL, "%d 정보 에러",50);

 

위와같이 서식문자에 자동으로 가변인자 값들이 매핑된다.

 

이제 저렇게 사용자가 로그를 넣을 때 큐에 저장이 되고, 틱 쓰레드가 일정 시간마다 큐의 상태를 확인하면서 처리한다고 했다.

틱 스레드가 처리하는 함수. -> 쓰레드 클래스의 OnProcess를 재정의(override) 해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
//쓰레드가 처리하는 함수.
void Log::OnProcess()
{
    UINT logCount = m_queueLogMsg.GetQueueSize(); // 큐에 들어있는 로그의 수를 가져온다.
    for (UINT i = 0; i < logCount; ++i)
    {
        LogMsg* pLogMsg = m_queueLogMsg.GetFrontQueue(); // 로그를 가져오고.
        //로그 찍기
        LogOutput(pLogMsg->m_eLogInfoType, pLogMsg->m_outputString); // 로그에 대한 처리.
        m_queueLogMsg.PopQueue();
    }
}
 
cs

처리되어지는 함수는 많지만 사용자가 사용하는 함수는 적다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LogConfig log;
 
log.m_eLogFileType = enumLogFileType::FILETYPE_TEXT; // 
log.m_hWnd = g_hWnd;
::strcpy(log.m_logFileName, "예제");
 
// 모든 로그 타입을 비쥬얼 스튜디오 출력창에 출력(저장)
log.m_logInfoTypes[(int)enumLogStorageType::STORAGE_OUTPUTWND] = (int)enumLogInfoType::LOG_ALL;
// INFO NORMAL타입과  ERROR_CRITICAL타입은 파일로 저장.
log.m_logInfoTypes[(int)enumLogStorageType::STORAGE_FILE] = (int)enumLogInfoType::LOG_INFO_NORMAL | (int)enumLogInfoType::LOG_ERROR_CRITICAL;
 
//로그 초기화
INIT_LOG(log);
 
//로그 큐에 삽입(일감 넣기)
EnQueueLog(enumLogInfoType::LOG_ERROR_CRITICAL, "%d 정보 에러",50);
EnQueueLog(enumLogInfoType::LOG_INFO_NORMAL, "낮은 수준 정보 알림");
cs

사용자가 사용하는 수준은 위와 같다. 파일 제목을 정하고 로그를 어떻게 저장(출력)할건지 선택하고 (종류)

로그를 초기화 한다음 해당 로그를 큐에 넣어준다.

LOG_ALL은 말그대로 모든 로그를 비쥬얼 스튜디오 출력창에 띄운다. %d 정보 에러는 INFO_NORMAL이기에 FILE로도 저장이 된다.

 

크게는 이정도만 알아도 된다고 생각하지만 좀 더 적어보면..

LogConfig구조체가 선언되었는데 이 구조체의 구조를 보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 초기화 구조체 정의..
struct LogConfig
{
    ////////////////////////////////////////////////////////////////////////////
    //배열순서(파일[0],디비[1],윈도우[2],디버그창[3],udp[4])
    //각배열에 출력하고싶은 LogInfo레벨을 or연산하여 넣는다.
    //예)파일에 LOG_INFO_NORMAL, 윈도우에 LOG_ALL
    //s_eLogInfoType[ STORAGE_FILE ] = LOG_INFO_NORMAL
    //s_eLogInfoType[ STORAGE_WINDOW ] = LOG_ALL
    int                m_logInfoTypes[MAX_STORAGE_TYPE];
    char            m_logFileName[MAX_FILENAME_LENGTH];
    // 로그 파일의 형식을 지정한다. xml OR TEXT 둘다 가능.
    enumLogFileType m_eLogFileType;
    //TCP/UDP로 로그를 남길 IP,PORT.
    char            m_IP[MAX_IP_LENGTH];
    int                m_udpPort;
    int                m_tcpPort;
    //서버 타입, 로그서버에 등록될 서버타입을 결정
    int                m_serverType;
 
 
    //DB로 로그를 남길 DSN정보
    char            m_dsnName[MAX_DSN_NAME];
    char            m_dsnID[MAX_DSN_ID];
    char            m_dsnPW[MAX_DSN_PW];
    //윈도우로 로그를 남길 윈도우 핸들.
    HWND            m_hWnd;
    //Log 처리 시간 기본으로 1초마다 처리
    DWORD            m_processTick;
    //Log파일 사이즈가 m_fileMaxSize보다 크면 새로운 파일을 만든다.
    DWORD            m_fileMaxSize;
 
    LogConfig()
    {
        ::ZeroMemory(thissizeof(LogConfig));
        m_processTick = DEFAULT_TICK; // 1000 / 1000;
        m_udpPort      = DEFAULT_UDPPORT;
        m_tcpPort      = DEFAULT_TCPPORT;
        m_fileMaxSize = 1024 * 50000// 50MB 기본으로 설정. 최대 100MB
    }
};
cs

m_logInfoTypes에 우리가 앞에서 저장할 타입(enum값)을 넣어주고. 출력하고 싶은 LogInfo레벨을 OR연산하여 넣는다.

로그 인포는 맨위에서 선언한 이것.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//각 등급마다 16진수설정.
// OR(|)연산을 위해 2^n승씩 증가.
// 0000/0000 에서 비트는 2^n의 자리를 차지.
enum class enumLogInfoType
{
      LOG_NONE               = 0x00000000
    , LOG_INFO_LOW           = 0x00000001
    , LOG_INFO_NORMAL      = 0x00000002
    , LOG_INFO_HIGH           = 0x00000004
    , LOG_INFO_CRITICAL    = 0x00000008
    , LOG_INFO_ALL           = 0x0000000F
    , LOG_ERROR_LOW           = 0x00000010
    , LOG_ERROR_NORMAL     = 0x00000020
    , LOG_ERROR_HIGH       = 0x00000040
    , LOG_ERROR_CRITICA    = 0x00000080
    , LOG_ERROR_ALL           = 0x00000100
    , LOG_ALL               = 0x000001FF
};
cs

fileName은 말그대로 파일의 이름. m_eLogFileType은 로그 파일의 형식을 정하는데 우리는 앞에서 TEXT로 했었다.

추가로 XML도 사용할 수 있다지만 TEXT만으로도 충분해 보인다.

1
2
3
4
5
6
7
enum class enumLogFileType
{
      FILETYPE_NONE        = 0x00
    , FILETYPE_XML        = 0x01
    , FILETYPE_TEXT        = 0x02
    , FILETYPE_ALL        = 0x03
};
cs

그 외의 다양한 설정 변수들은 주석을 참고하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Log : public Thread, public Singleton
{
    DECLARE_SINGLETON(Log);
public:
    Log();
    ~Log();
 
    //인터페이스 함수
    bool        Init(LogConfig& config);
    void        LogOutput(enumLogInfoType eLogInfo, char* outputString);
    void        LogOutputLastErrorToMsgBox(char* outputString);
    // 모든 로그를 끝낸다.
    void        CloseAllLog();
public:
    // 쓰레드 처리 함수
    void        OnProcess() override;
    void        SetHWND(HWND hWnd = NULL) { m_hWnd = hWnd; }
public:
    // 큐에 관련된 함수 현재 큐 크기.
    UINT        GetQueueSize() { return m_queueLogMsg.GetQueueSize(); }
    void        InsertMsgToQueue(LogMsg* pLogMsg) { m_queueLogMsg.PushQueue(pLogMsg); }
private:
    int                m_logInfoTypes[MAX_STORAGE_TYPE];
    char            m_logFileName[MAX_FILENAME_LENGTH];
    // 로그 파일의 형식을 지정한다. xml OR TEXT 둘다 가능.
    enumLogFileType m_eLogFileType;
    //TCP/UDP로 로그를 남길 IP,PORT.
    char            m_IP[MAX_IP_LENGTH];
    int                m_udpPort;
    int                m_tcpPort;
    //서버 타입, 로그서버에 등록될 서버타입을 결정
    int                m_serverType;
 
 
    //DB로 로그를 남길 DSN정보
    char            m_dsnName[MAX_DSN_NAME];
    char            m_dsnID[MAX_DSN_ID];
    char            m_dsnPW[MAX_DSN_PW];
    // 로그 저장 변수
    char            m_outStr[MAX_OUTPUT_LENGTH];
 
    // 윈도우로 로그를 남기기 위한 윈도우 핸들.
    HWND            m_hWnd; 
    //File Handle변수
    HANDLE            m_logFile;
    //TCP/UDP소켓
    SOCKET            m_sockTcp;
    SOCKET            m_sockUdp;
    //메시지 큐
    Queue<LogMsg*>    m_queueLogMsg;
    //현재 메세지 버퍼 위치
    int                m_msgBufferIdx;
    DWORD            m_fileMaxSize;
 
    /////////////////////////////////////////////////////////////////////////////
    //내부 호출 함수
    //출력관련..함수    
    void OutputFile(char* outputString);
    void OutputDB(char* outputString);
    void OutputWindow(enumLogInfoType eLogInfo, char* outputString);
    void OutputDebugWnd(char* outputString);
    void OutputUDP(enumLogInfoType eLogInfo, char* outputString);
    void OutputTCP(enumLogInfoType eLogInfo, char* outputString);
 
 
    //초기화 함수들
    bool InitDB();
    bool InitFile();
    bool InitUDP();
    bool InitTCP();
};
cs

로그 클래스.

InsertMsgToQueue함수는 메시지를 큐에 넣는 역할을 한다.

인자로 LogMsg구조체 변수의 포인터를 받는데 LogMsg구조체 변수는 로그를 남길때마다 생성하여 큐에 넣지 않고

미리 MAX_QUEUE_CNT(10000)만큼의 LogMsg구조체 변수를 배열로 선언해놓고 필요할 때 배열에서 꺼내 쓰는 방식.

//메시지 구조체
struct LogMsg
{
	enumLogInfoType m_eLogInfoType;
	char		m_outputString[MAX_OUTPUT_LENGTH];
};

위에서 사용자가 사용할 함수 INIT_LOG를 보면

bool INIT_LOG(LogConfig& config)
{
	return GetLog()->Init(config);
}

단일체 LOG객체에 접근하여  로그클래스의 Init함수를 호출한다. 

 -> 즉 매번 로그를 넘겨줄 때마다 초기화를 진행한다는 소리.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
bool Log::Init(LogConfig& config)
{
    char    strTime[100];
    time_t    curTime;
    tm* locTime; // Timer
    curTime = ::time(NULL);
    locTime = ::localtime(&curTime);
 
    //각 설정값 세팅
    ::CopyMemory(m_logInfoTypes, config.m_logInfoTypes, MAX_STORAGE_TYPE * sizeof(INT32));
 
    ::strftime(strTime, 100"%m월%d일%H시%M분", locTime);
 
    //LOG 디렉토리 생성
    ::CreateDirectoryA("..\\..\\LOG"NULL);
    ::sprintf(m_logFileName, "..\\..\\Log\\%s_%s.log", config.m_logFileName, strTime);
 
    ::strncpy(m_IP, config.m_IP, MAX_IP_LENGTH);
    ::strncpy(m_dsnName, config.m_dsnName, MAX_DSN_NAME);
    ::strncpy(m_dsnID, config.m_dsnID, MAX_DSN_ID);
    ::strncpy(m_dsnPW, config.m_dsnPW, MAX_DSN_PW);
 
    m_eLogFileType    = config.m_eLogFileType;
    m_tcpPort        = config.m_tcpPort;
    m_udpPort        = config.m_udpPort;
    m_serverType    = config.m_serverType;
    m_fileMaxSize    = config.m_fileMaxSize;
 
    m_hWnd            = config.m_hWnd;
    bool bRet        = false;
 
    //파일로그를 설정했다면
    if (m_logInfoTypes[(int)enumLogStorageType::STORAGE_FILE] != (int)enumLogInfoType::LOG_NONE)
    {
        bRet = InitFile();
    }
    if (!bRet)
    {
        CloseAllLog();
        return false;
    }
    //db로그를 설정했다면
    if (m_logInfoTypes[(int)enumLogStorageType::STORAGE_DB] != (int)enumLogInfoType::LOG_NONE)
    {
        bRet = InitDB();
    }
    if (!bRet)
    {
        CloseAllLog();
        return false;
    }
    if (m_logInfoTypes[(int)enumLogStorageType::STORAGE_UDP] != (int)enumLogInfoType::LOG_NONE)
    {
        bRet = InitUDP();
    }
    if (!bRet)
    {
        CloseAllLog();
        return false;
    }
 
    if (m_logInfoTypes[(int)enumLogStorageType::STORAGE_TCP] != (int)enumLogInfoType::LOG_NONE)
    {
        bRet = InitTCP();
    }
    if (!bRet)
    {
        CloseAllLog();
        return false;
    }
 
 
    Thread::CreateThread(config.m_processTick); // 쓰레드 생성
    Thread::Run(); // 실행!
 
 
 
    return true;
}
cs

이렇게 초기화를 통해 해당 로그의 시간대, 로그 파일 저장할 디렉터리, 변수 설정등.

사용자가 로그를 남기기 위해 설정한 Config정보를 토대로 해당 저장매체를 초기화 해준다.

다시 설명해보면

 LogConfig log;
log.m_eLogFileType = enumLogFileType::FILETYPE_TEXT; // 
log.m_hWnd = g_hWnd;
::strcpy(log.m_logFileName, "예제");

// 모든 로그 타입을 비쥬얼 스튜디오 출력창에 출력(저장)
log.m_logInfoTypes[(int)enumLogStorageType::STORAGE_OUTPUTWND] = (int)enumLogInfoType::LOG_ALL;
// INFO NORMAL타입과  ERROR_CRITICAL타입은 파일로 저장.
log.m_logInfoTypes[(int)enumLogStorageType::STORAGE_FILE] = (int)enumLogInfoType::LOG_INFO_NORMAL | (int)enumLogInfoType::LOG_ERROR_CRITICAL;

INIT_LOG(log);

사용자가 로그를 남길 곳에서 LogConfig구조체 변수를 정의하고, INIT_LOG를 호출하여 Init을 호출해야 하는 루틴.

로그를 남기는 시간과 로그를 남길 디렉터리 주소, 그리고 위 코드에서 처럼 저장할 매체를 FILE,OUTPUTWND에 값을 넣어줬다면 INIT_LOG(전역 함수. 단일체 LOG클래스에 접근하여 Init을 호출)를 통하여 위의 정보를 토대로 로그를 작성하기 위한 밑작업이 완료되는 것.

반응형
Comments