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); } } }