7.5.4 EH 메신저 – StsSVC 구현 [TCP/IP 소켓 프로그래밍 with 윈도우즈]

StsSVC를 구현합시다. EH 메신저 솔루션에 StsSVC 이름의 콘솔 응용 프로젝트를 만드세요. 그리고 이 책에서는 StsSVC를 구현한 이후에 테스트 프로젝트를 만들고 테스트하는 부분은 다루지 않을게요.

StsSVC 프로젝트에 Program.cpp 파일을 추가하여 소스를 구현합시다.

#include "..\\Common\\EHMessenger.h"
#pragma comment(lib,"ws2_32")
#pragma comment(lib,"..\\Debug\\EHPacketLib")
#pragma comment(lib,"..\\Debug\\StsLib")
#pragma comment(lib,"..\\Debug\\DbmLib")
#pragma comment(lib,"..\\Debug\\EHWrapSocketLib")

StsSVC에서는 주기적으로 최근에 수신한 KeepAlive 메시지와 현재 시간을 비교하는 작업이 필요합니다. 이 부분은 Win32 API에서 제공하는 타이머를 사용합시다. 이를 위해 Windows.h 파일을 포함합니다.

#include <Windows.h>

시간을 얻어오는 부분은 사용하기 편한 time 함수를 사용합시다. 이를 위해 time.h 파일을 포함합니다.

#include <time.h>
#include <iostream>

사용자에게 수신한 계정 정보를 보관하기 위해 map과 list를 사용합시다.

#include <map>
#include <list>
using namespace std;

수신한 계정 정보와 마지막 KeepAlive 시간을 하나의 구조체로 정의합시다.

struct KARecord
{
    string id;
    IN_ADDR ip;
    int stsport;
    int smsgport;
    int fileport;
    time_t last;
    KARecord(KeepAlive *ka)
    {
        id = ka->GetId();
        ip = ka->GetIP();
        stsport = ka->GetStsPort();
        smsgport = ka->GetSmsgPort();
        fileport = ka->GetFilePort();
        last = time(0);
    }
};

사용자 id를 키로 KARecord 개체를 값으로 하는 map을 KAMap, 반복자를 KAIter 이름으로 재지정합시다.

typedef map<string,KARecord *> KAMap; 
typedef map<string,KARecord *>::iterator KAIter;

KARecord 개체를 보관하는 list를 KAList, 반복자를 KALIter 이름으로 재지정합시다.

typedef list<KARecord *> KAList;
typedef list<KARecord *>::iterator KALIter;

전역 변수로 KAMap 형식 변수를 선언하여 계정 정보를 관리합시다.

KAMap kamap;

진입점 main 함수에서는 윈속 초기화 후에 Sts 서버를 가동하고 윈속을 해제화합니다.

void StartStsServer();
int main()
{
    WSADATA wsadata;
    WSAStartup(MAKEWORD(2,2),&wsadata);
    StartStsServer();
    WSACleanup();
    return 0;
}

Sts 서버 가동 함수를 작성합시다.

LRESULT CALLBACK MainProc(HWND hWnd,UINT iMessage,WPARAM wParam, LPARAM lParam);
void StartStsServer()
{

Sts 서버에서는 주기적인 작업이 필요합니다. 이를 위해 윈도우 클래스를 등록합시다.

    HINSTANCE hIns = GetModuleHandle(0);
    WNDCLASS wndclass={0};
    wndclass.lpszClassName = TEXT("StsSVC");
    wndclass.hInstance = hIns;
    wndclass.lpfnWndProc = MainProc;
    RegisterClass(&wndclass);

그리고 윈도우를 생성합니다. 대신 사용자와 상호 작용할 필요는 없어서 화면에 시각화는 하지 않습니다.

    CreateWindow(TEXT("StsSVC"),TEXT(""),0,0,0,0,0,0,0,hIns,0);

메시지 루프도 필요하겠죠.

    MSG Message;
    while(GetMessage(&Message,0,0,0))
    {
        DispatchMessage(&Message);
    }
}

윈도우 콜백 프로시져에서는 생성할 때 Sts 서버 소켓을 생성하고 타이머를 설정하는 부분이 필요합니다. 이를 수행하는 OnCreate 함수를 만들어 사용합시다.

void OnCreate(HWND hWnd);
LRESULT CALLBACK MainProc(HWND hWnd,UINT iMessage,WPARAM wParam, LPARAM lParam)
{
    switch(iMessage)
    {
    case WM_CREATE: OnCreate(hWnd); return 0;
    }
    return DefWindowProc(hWnd,iMessage,wParam,lParam);
}

OnCreate 함수를 작성합시다.

DWORD WINAPI  DoItStsServer(LPVOID pin);
void CALLBACK KeepAliveTimerProc(HWND, UINT, UINT_PTR, DWORD);
void OnCreate(HWND hWnd)
{

Sts 서버에서는 KeepAlive를 수신하는 부분과 주기적인 타이머로 확인하는 부분이 필요합니다. 수신하는 부분은 별도의 쓰레드에서 수행하게 합시다.

    DWORD ThreadID;
    CloseHandle(CreateThread(0,0,DoItStsServer,0,0,&ThreadID));

주기적인 타이머를 설정합니다. 이 책에서는 1초 주기로 할게요.

    SetTimer(0,0,1000,KeepAliveTimerProc);
}

KeepAlive를 수신하는 쓰레드의 진입점 함수를 작성합시다.

DWORD WINAPI DoIt(LPVOID pin);
DWORD WINAPI  DoItStsServer(LPVOID pin)
{

STS_PORT를 인자로 TCP 서버를 가동합니다.

    SOCKET sock = EHWrapSocket::CreateTCPServer(STS_PORT,5);
    SOCKADDR_IN clientaddr;
    int len = sizeof(clientaddr);
    SOCKET dosock;
    DWORD ThreadID;

메시지를 수신하였을 때 이를 처리하는 부분은 별도의 쓰레드에서 수행하기로 합시다.

    while(1)
    {
        dosock = accept(sock,(SOCKADDR *)&clientaddr, &len);
        CloseHandle(CreateThread(0,0,DoIt,(LPVOID)dosock,0,&ThreadID));
    }
    return 0;
}

메시지를 수신하였을 때 처리하는 쓰레드 진입점 함수를 작성합시다.

void StoreLastKeepAliveTime(KeepAlive *ka);
void AddLoggedUserInfo(KeepAlive *ka);
DWORD WINAPI DoIt(LPVOID pin)
{

쓰레드 인자로 전달받은 소켓을 형식 변환합니다.

    SOCKET sock = (SOCKET)pin;

그리고 메시지를 수신합니다.

    EHPacket ep(sock);
    closesocket(sock);

수신한 메시지가 KeepAlive가 아니면 오류입니다.

    if(ep.GetMsgId() != MID_KEEPALIVE)
    {
        cout<<"오류!!! 수신한 메시지가 KeepAlive 메시지가 아님."<<endl;
        return 0;
    }

수신한 메시지를 KeepAlive 메시지로 변환합니다.

    KeepAlive ka(&ep);
    if(ka.GetStsPort()==0)
    {

만약 수신한 메시지의 stsport값이 0이면 처음 수신한 KeepAlive 메시지는 아닙니다. 이 때는 수신한 시간을 기록합니다. 이 부분은 별도의 함수로 작성합시다.

        StoreLastKeepAliveTime(&ka);
    }

그렇지 않다면 처음 수신한 KeepAlive 메시지입니다. 이에 관한 처리도 별도의 함수로 작성합시다.

    else
    {
        AddLoggedUserInfo(&ka);
    }
    return 0;
}

수신한 KeepAlive 시간을 기록하는 함수를 작성합시다.

void StoreLastKeepAliveTime(KeepAlive *ka)
{
    cout<<ka->GetId()<<"로부터 KeepAlive 메시지 수신"<<endl;

계정 정보를 관리하는 배열에서 KARecord 개체를 참조합니다.

    KARecord *kar = kamap[ka->GetId()];
    if(kar==0)
    {

만약 KARecord 개체가 없다면 계정 정보가 없다는 것인데 이러한 시나리오는 나올 수 없습니다. 그렇지만 개발자의 논리적 버그로 이와 같은 문제가 발생했다면 빠르게 확인할 수 있게 오류 메시지를 출력합시다. 별도의 시스템 구동 로그 파일을 만들어서 파일에 기록하는 것도 좋은 방법이겠죠. 여기에서는 단순하게 콘솔에 출력하기로 할게요.

        cout<<"오류!!!"<<ka->GetId()<<"는 로긴 상태가 아님"<<endl;
    }

현재 시간으로 가장 최근에 수신한 KeepAlive 시간을 설정합니다.

    else
    {
        kar->last = time(0);
    }
}

처음으로 KeepAlive 메시지를 수신하였을 때 처리하는 함수를 작성합시다.

void SendOtherUserInfo(KARecord *rk, KARecord *ok);
void AddLoggedUserInfo(KeepAlive *ka)
{
    cout<<ka->GetId()<<"로부터 첫 번째 KeepAlive 메시지 수신"<<endl;

수신한 메시지로 KARecord 개체를 생성합니다.

    KARecord *kar = new KARecord(ka);

처음으로 KeepAlive 메시지를 수신하면 DbmSVC에게 상태 변화 요청 메시지를 전송합니다. 이 때 상태값은 STS_LOGGED입니다.

    ChangeSts cs(ka->GetId(),STS_LOGGED);
    SOCKET clisock = EHWrapSocket::Connect(DBM_IP,DBM_PORT);
    cs.Serialize(clisock);
    closesocket(clisock);

계정 관리 map의 시작 위치와 끝 위치를 구합니다.

    KAIter seek = kamap.begin();
    KAIter last = kamap.end();

보관한 KARecord 개체를 참조할 변수를 선언합시다.

    KARecord *stored_kar=0;

순차적으로 위치를 이동하면서 보관한 계정 정보를 참조합니다.

    for(seek=seek  ;seek != last; ++seek)
    {
        stored_kar = (*seek).second;

만약 참조한 정보가 참이면 기존 계정에게 새로운 계정 정보를 전송하고 새로운 계정에게 기존 계정 정보를 전송합니다. 이 부분은 별도의 함수로 작성합시다.

        if(stored_kar)
        {
            SendOtherUserInfo(stored_kar,kar);
            SendOtherUserInfo(kar,stored_kar);
        }
    }

계정 관리 map 에 새로운 계정 정보를 보관합니다.

    kamap[ka->GetId()] = kar;
}

계정 정보를 전송하는 함수를 작성합시다.

void SendOtherUserInfo(KARecord *rk, KARecord *ok)
{

먼저 전송할 OtherUserInfo 메시지를 생성합니다.

    OtherUserInfo oui(ok->id,ok->ip,ok->smsgport,ok->fileport);

수신할 계정에 연결하여 메시지를 전송합니다.

    SOCKET clisock = EHWrapSocket::Connect(inet_ntoa(rk->ip),rk->stsport);
    oui.Serialize(clisock);
    closesocket(clisock);
}

주기적으로 마지막 수신한 KeepAlive 시간과 현재 시간을 비교하여 약속한 시간 범위 내에 KeepAlive 메시지를 보내지 않은 계정을 강제 로그 아웃 처리하는 함수를 작성합시다.

void ForceLogOut(KARecord *kar);
void CALLBACK KeepAliveTimerProc(HWND, UINT, UINT_PTR, DWORD)
{

현재 시각을 구합니다.

    time_t now = time(0);

강제 로그 아웃 처리할 계정 정보를 보관할 리스트 변수를 선언합시다.

    KAList kalist;

계정 정보를 보관하는 map의 시작 위치와 끝 위치를 구합니다.

    KAIter seek = kamap.begin();
    KAIter last = kamap.end();

보관한 계정 정보를 참조할 변수를 선언합니다.

    KARecord *stored_kar=0;

순차적으로 보관한 계정 정보를 참조합니다.

    for(seek=seek  ;seek != last; ++seek)
    {
        stored_kar = (*seek).second;

만약 계정 정보가 참일 때 현재 시간과 가장 최근에 수신한 KeepAlive 시간의 차이를 구합니다.

        if(stored_kar)
        {
            if( now - stored_kar->last > 3)
            {

그리고 그 차이가 약속한 시간(이 책에서는 3초)보다 크면 강제 로그 아웃할 대상이므로 계정 정보를 보관한 map은 0으로 설정하고 list에 계정 정보를 추가합니다.

                (*seek).second = 0;
                kalist.push_back(stored_kar);
            }
        }
    }

강제 로그 아웃할 계정 정보를 보관한 리스트의 시작 위치와 끝 위치를 구합니다.

    KALIter seek2 = kalist.begin();
    KALIter last2 = kalist.end();

순차적으로 위치를 이동하여 강제 로 그아웃 처리합니다. 강제 로그 아웃 처리는 별도의 함수로 작성합시다.

    for(seek2 = seek2 ; seek2!=last2; ++seek2)
    {
        ForceLogOut(*seek2);
    }
}

강제로 로그 아웃 처리하는 함수를 작성합시다.

void ForceLogOut(KARecord *kar)
{

먼저 계정 정보의 모든 포트 번호를 0으로 설정합시다. Peer에서 수신한 계정 정보의 포트 번호가 0이면 상대가 로그 아웃한 것으로 판별할 것입니다.

    kar->fileport = kar->smsgport = kar->stsport = 0;

Dbm 서비스에 연결하여 상태를 변경 요청합니다. 이 때 변경할 상태는 STS_REG입니다.

    ChangeSts cs(kar->id,STS_REG);
    SOCKET clisock = EHWrapSocket::Connect(DBM_IP,DBM_PORT);
    cs.Serialize(clisock);
    closesocket(clisock);

로긴 상태의 계정 정보를 보관하는 map의 시작 위치와 끝 위치를 구합니다.

    KAIter seek = kamap.begin();
    KAIter last = kamap.end();

보관한 계정 정보를 참조할 변수를 선언합시다.

    KARecord *stored_kar=0;

순차적으로 보관한 계정 정보를 참조합니다.

    for(seek=seek  ;seek != last; ++seek)
    {
        stored_kar = (*seek).second;

참조한 계정 정보가 있다면 해당 계정에게 강제 로그 아웃할 계정 정보를 전송합니다.

        if(stored_kar)
        {
            SendOtherUserInfo(stored_kar,kar);
        }
    }
}