본문 바로가기

[ Windows Program ]/Windows API

멀티쓰레드 윈도우즈 소켓 프로그래밍 #4 – 멀티쓰레드 프로그래밍

3.    멀티쓰레드 프로그래밍

 

이제 드디어 실제 에코서버에대해 설명해보기로 한다. 초간단 서버프로그램은 단순히 입력을 받은 문자열 그대로를 출력해 주는 것으로서 가장 기초적인 서버 프로그램이라고 수있다.  프로그램을 분석하기 위해 먼저 소스를 참조하기 바란다.

 

소스를 들여다 보면 이전의 서비스 프로그램 예제에 3개의 클래스가 추가된 것을 있을 것이다. 클래스는 다음과 같다.

 

LXSCK – 윈속 통신을 위한 모듈. 서버/클라이언트에서 사용가능한 공통모듈로서 컨넥션 기능은 없고 통신 기능만 있다. 통신을 위해 2개의 thread(read/write) 생성하여 사용한다.

LXSERVER – 서버용으로 윈속을 초기화하고 클라이언트로부터 접속을 기다린다. 접속이 있을 경우 접속을 받아들인 소켓 번호를 리턴한다.

ECHO_SERVER – 실제적인 에코서버 클래스이다. LXSCK 클래스를 받아들여서 이를 사용하여 클라이언트와 통신한다. 멀티 쓰레드 서버 통신을 위해 클래스가 생성되면 1개의 thread 생성한다.

 

LXSCK LSXSERVER 클래스는 통신을 위한 기본 클래스이므로 일단 관심있는 사람만 소스를 들여다 보고 설명은 다음에 하기로한다. 오늘은 어떻게 ECHO_SERVER 클래스가 동작하는 지에 대해서만 살펴본다.

 

먼저 ServiceMain() 함수를 보면 while 루프가 다소 달라졌다는 것을 있다.

 

  LXSERVER *lxsrv = new LXSERVER; //서버클래스를 생상한다.

  lxsrv->StartServer(LISTEN_PORT);  //서버용으로 윈속을 초기화한다. LISTEN_PORT 777 지정되었다.

  ECHO_SERVER *lxaServer[MAX_CLIENT]; //멀티쓰레드를 위한 ECHO_SERVER 클래스 어레이를 지정한다.

  memset(lxaServer, 0, MAX_CLIENT*sizeof(ECHO_SERVER *));//어레이를 NULL 초기화

  while (g_isRunning)

  {

    DbgOut("EchoServer : Service running... Let's wait for a next connection.\n");\

    // Waits a connection

    SOCKET idSocketAccept;

    if ( lxsrv->AcceptConnection(&idSocketAccept) != SOCKET_ERROR )

    { //클라이언트로부터의 접속을 기다리고 접속이 있을 경우 소켓을 돌려준다.

      // There is a connection. Create an independant server handler.

      // create socket communication class

      LXSCK *lxsck = new LXSCK(idSocketAccept); //접속이 이루어진 소켓으로 통신을 위한 클래스를 초기화한다.

      ECHO_SERVER *echo = new ECHO_SERVER(lxsck); //통신클래스가 준비됬으면 이제 에코서버를 준비한다.

     //에코서버 자체가 쓰레드를 생성해서 이미 실행중이다. 이에 에코서버 클래스를 배열의 슬롯에 넣는다.

      for(int i = 0; i < MAX_CLIENT; i++) {

        if ( lxaServer[i] == NULL ) {

          DbgOut("New server thread is added on slot [%d].\n", i);

          lxaServer[i] = echo;

          i = MAX_CLIENT;

        }

      }

    } else {

      DbgOut("Fatal error\n");

    }

// check each server is still on. if not, let's remove it.

// 실행이 종료된 에코서버가 있다면 클래스를 제거한다.

    for(int i = 0; i < MAX_CLIENT; i++) {

      if ( lxaServer[i] && !lxaServer[i]->IsThreadOn() ) {

        DbgOut("The Server in slot [%d] is not active, let's close it\n", i);

        delete lxaServer[i];

        lxaServer[i] = NULL;

      }

    }

    Sleep(1000);      

  }

 

자세한 설명은 위의 소스에 붉은 글씨로 남겼으니 참조하기 바란다.

에코 서버는 클래스가 생성될 아래와 같이 쓰레드를 생성하여 쓰레드 안에서 서버의 역활을 담당한다. 이렇게 쓰레드를 생성함으로서 위의 메인 while루프는 다시 다음 접속을 기다릴 있는 것이다.

 

void ECHO_SERVER::Start( void )

{

  DWORD dwThreadID;

  m_hThread = CreateThread( NULL, 0, (unsigned long (__stdcall *)(void *))ServerProc, (LPVOID)this, (DWORD)0, &dwThreadID );

}

 

쓰레드가 생성되면 파라메터로서 1개의 LPARAM 변수를 쓰레드에 전달 있다. 무엇을 전달하는 가는 전적으로 사용자에게 달려있지만 클래스의 주소를 전달한다면 쓰레드 안에서 클래스의 퍼블릭 멤버함수들을 호출 수도 있으므로 클래스의 주소를 전달하는 것이 좋다.

쓰레드를 생성할 클래스의 주소(this) 쓰레드로 넘겨주어 쓰레드가 소속된 멤버함수를 아래와 같이 호출한다.

 

DWORD ECHO_SERVER::ServerProc( LPVOID lpParam )

{

  ECHO_SERVER *lxs = (ECHO_SERVER *)lpParam;

 

  DbgOut( "ServerProc Thread is ready...\n" );

  while( lxs->ServerHandler() ) {

    Sleep(10);

  }

  DbgOut( "ServerProc Thread is ended...\n" );

  ExitThread( 0 );

  return (DWORD)0;

}

 

그럼 이제 실제 서버 동작을 하는 ServerHandler() 멤버 함수를 보자.

 

bool ECHO_SERVER::ServerHandler( void )

{

  bool bRet = true;

  TCHAR tszBuffer[MAX_PATH];

  int nRead = m_lxSck->ReadLine(tszBuffer, MAX_PATH);

  if ( nRead > 0 ) {

    TCHAR tszTemp[MAX_PATH];

    wsprintf(tszTemp, "ECHO [%s]\r\n", tszBuffer);

    m_lxSck->SendLine(tszTemp);

    if ( !stricmp(tszBuffer, "quit") ) {

      Sleep(1000);

      m_lxSck->Stop();

      Sleep(1000);

      delete m_lxSck;

      bRet = false;

    }

  }

  return bRet;

}

 

함수에선 소켓 통신 클래스인 LXSCK 멤버함수중 하나인 ReadLine() 함수를 사용하여 만약 클라이언트로 부터 입력이 있다면 1 라인의 문자열을 읽어들인다. ReadLine() 함수는 입력 버퍼에 문자열이 있을 경우 문자열과 함께 문자열의 길이를 리턴한다.

만약 입력된 문자열이 있을 경우 SendLine() 함수를 통해 문자열을 포함하는 ECHO 문자열을 클라이언트에 전송한다.

만약 입력된 문자열이 quit이면 소켓 통신을 종료하고 LXSCK 클래스를 제거한다. ECHO_SERVER 클래스는 경우 false 값을 쓰레드에 리턴하고 쓰레드의 while() 루프는 false 되어 쓰레드는 종료하게 된다. 쓰레드가 종료될 경우 쓰레드의 종료코드를 ExitThread() 함수를 써서 지정해준다.

 

멀티쓰레드는 클래스 자체내에서 각자의 쓰레드를 생성하여 사용하므로 해결이 가능하다. 하지만 독립적으로 동작하는 클래스들을 생성, 제거하기 위해서는 생성된 클래스 주소를 어딘가에 저장해 놓아야 한다. 이를 위해 링크드 리스트를 사용할 것을 권장하지만 강좌에선 이해를 돕기위해 ECHO_SERVER클래스 주소의 배열을 사용하였다. 배열은 최대 50개까지이며 이는 클라이언트를 동시에 50개까지 받을 있다는 뜻이다. 실제로 50 넘는 경우의 에러체크는 예제에서 보이지 않았다. 각자 해보기 바란다.

 

클라이언트가 접속될 때마다 생성된 ECHO_SERVER 클래스의 주소를 저장해둘 배열.

ECHO_SERVER *lxaServer[MAX_CLIENT];

 

배열중 슬롯을 찾아 그곳에 새로 생성된 ECHO_SERVER클래스의 주소를 저장한다.

for(int i = 0; i < MAX_CLIENT; i++) {

  if ( lxaServer[i] == NULL ) {

    DbgOut("New server thread is added on slot [%d].\n", i);

    lxaServer[i] = echo;

    i = MAX_CLIENT;

  }

}

 

실행이 종료(쓰레드가 종료) ECHO_SERVER 클래스가 존재한다면 클래스를 제거하고 슬롯에 NULL 값을 넣어 다음 쓰레드가 사용할 수있게 한다.

for(int i = 0; i < MAX_CLIENT; i++) {

  if ( lxaServer[i] && !lxaServer[i]->IsThreadOn() ) {

    DbgOut("The Server in slot [%d] is not active, let's close it\n", i);

    delete lxaServer[i];

    lxaServer[i] = NULL;

  }

}

 

이로서 멀티쓰레드를 구현한 서버프로그래밍에 대해 살펴보았다.

LXSCK LXSERVER 내용은 네버엔딩님의 윈속강좌를 충실히 보았다면 있는 내용들 이기 때문에 굳이 설명을 해야할 필요는 못느끼나. 몇가지 팁이 있기에 다음편에 보충설명을 하고자 한다.

 


- Reference

  http://cafe.naver.com/nevernding