U E D R S I H C RSS
ID
Password
Join
자신이 존재한다는 바로 그 사실에 한번도 놀라보지 못한 사람은 가장 위대한 사실을 놓치고 있는 사람. ―J.F.

개요 #

이 튜토리얼에서는 우리는 클라이언트가 아닌 이전에 우리가 만들어보았던 '간단한 서버'로 되돌아가게 될 것이다. 우리는 하나의 쓰레드에서 모든 것이 동작하는 아주 간단한 서버를 만들어 볼 것이다. 확실하게 이해했다고 생각되었다면 동시처리 개념을 설명하기 시작하는 다음 튜토리얼로 넘어갈 것이다.

서버 소스 #

server.cpp에서부터 시작하자.

다음은 Kirthika가 제시해준 개요다:
이 튜토리얼은 client-server의 보다 깔끔한 코드로 다시 구성한 것이다.
(예 : 객체들을 파괴할 때는 destroy() 메쏘드를 사용했고, 클라이언트로부터 데이타를 읽는 작업을 실행하기 전에 process() 메쏘드를 호출 한다.) 이벤트를 다루기 위하여 ACE_Reactor를 다시 사용할 것이다. 모든 것은 싱글 쓰레드상에서 이루어진다. 이 튜토리얼은 멀티쓰레드 모델의 서버 제작에 징검다리 역할을 할 것 이다.

// $Id: ACE_c6_a9_c5_e4_b8_ae_be_f3005_2f_b8_d6_c6_bc_be_b2_b7_b9_b5_e5_b8_a6_c1_a2_c7_cf_b1_e2_c0_fc_bf_a1,v 1.2 2004/05/20 12:30:07 redpixel Exp redpixel $

/* 우리는 main()함수를 매우 간단하게 유지하고자 한다. 우리가 사용하는 방법중
  하나는 복잡한 것들의 대부분은 일꾼쓰레드로 떠넘기는 것이다. 이 경우에서는,
  메인소스에서 acceptor 헤더를 include하는 것만 필요하다. "실제로 동작"하는 
  것들만 고려하도록 하자. */
#include "client_acceptor.h"

/* 이전에 우리는 finished변수를 설정하는 간단한 시그널 핸들러를 선언했었다. 물론,
  셧다운 요청을 다루는 방법에는 좀더 세련된 방법도 있다. 하지만, 우리는 이것에
  촛점을 맞춘 것이 아니므로, 가장 쉬운 방법을 취한 것이라 보면 된다. */
static sig_atomic_t finished = 0;
extern "C" void handler (int)
{
  finished = 1;
}

/* 서버는 설정된 TCP/IP 포트로부터 클라이언트의 접속요청을 listen 해야만 한다.
  기본적인 ACE포트는 10002이며, 이것은 여기서 우리가 원하는 일을 하는 데는
  충분하다. 대부분의 확실하게 작성된 어플리케이션은 커맨드라인을 가지거나,
  설정화일을 읽거나 다른 괜찮은 것을 실행할 수 있다. 앞서 언급한 시그널 핸들러와
  같이 이것은 중요한 것이 아니므로 쉬운 방법을 택하도록 하겠다. */
static const u_short PORT = ACE_DEFAULT_SERVER_PORT;

/* 마침내, 메인 함수까지 왔다. 함수 표현이 프로토타입과 맞지 않다면, 아무
  파라메터를 사용하지 않을 것이라도 몇몇 C++ 컴파일러들은 에러를 뱉을 것
  이므로 파라메터를 적어주자. */
int main (int argc, char *argv[])
{
  ACE_UNUSED_ARG(argc);
  ACE_UNUSED_ARG(argv);

  /* 일찌기 우리가 작성했던 서버들에서는, reactor를 얻기위해서 전역 포인터
    변수를 사용했다. 이 방법은 별로 좋은 방법이 아니기 때문에 바로 main()
    으로 넘어온 것이다. Client_Handler를 설명할 때, 어떻게 이 reactor
    로부터 포인터를 관리하는 지를 알 수 있을 것이다.*/
  ACE_Reactor reactor;

  /* acceptor는 클라이언트들이 서버로 접속하는 것을 돌볼 것이다. 또한
    이것은 신규 클라이언트들 각각을 위해서 생성된 Client_Handler를
    준비할 것이다. 우리는 단지 하나의 TCP/IP 포트에서 listen할 것이기
    때문에, 단지 1개의 acceptor만을 필요로 하게 된다.
    심지어, 원한다면 추가적인 listen 포트들을 생성하고 개별적인 포트로부터
    listen할 수 있다. (예를 들어, 우리가 inetd 데몬을 다시 작성할 경우가
    이런 경우라고 할 수 있다) */
  Client_Acceptor peer_acceptor;

  /* 접속된 연결을 대표하는 ACE_INET_Addr을 선언한다. 우리의
    acceptor 객체를 Addr을 사용해서 open()할 것이다. 서버들은
    일반적으로 "잘 알려진" 포트로부터 listen하는 것이 보통이다. 만약
    아니라면, 클라이언트상에서는 서버의 주소와 포트 정보를 알수 있게
    작성해주는 것이 좋다. 만일 acceptor를 open()하는 것을 실패했을 경우
    어떻게 ACE_ERROR_RETURN이 사용되는지에 대해 주목하라. 이
    테크닉은 앞으로의 튜토리얼에서도 반복해서 사용된다.*/
  if (peer_acceptor.open (ACE_INET_Addr (PORT), &reactor) == -1)
    ACE_ERROR_RETURN ((LM_ERROR, "%p\n", "open"), -1);

  /* 이제, 우리는 open()작업이 성공적으로 수행되었음을 알 수 있다. 만일
     실패했다면, 위에서 이미 실행이 끝났을 것이다. open()의 부수적인 좋은
     효과는 성공적으로 실행돼면, reactor에 등록까지도 끝내준다는 점이다. */

  /* 시그널 핸들러가 "진짜" 일을 하게 하고 싶다면 시그널 핸들러를 reactor
     와 함께 등록할 수 있다. 어쨋든, 여기서 사용할 콜백 함수는 ACE_Event_Handler
     로 부터 상속받은 flag-setter 다. */
  ACE_Sig_Action sa ((ACE_SignalHandler) handler, SIGINT);

  /* ACE_ERROR_RETURN 과 같이, ACE_DEBUG 매크로가 사용되기 시작 했다.
    프로그램에서의 일정 형태의 디버깅 정보 출력에 도움이 될 것이다. */
  ACE_DEBUG ((LM_DEBUG, "(%P|%t) starting up server daemon\n"));

  /* reactor 의 handle_events() 메쏘드를 호출하며 무한 루프를 돌린다.
    handle_events() 는 등록된 핸들러들에 활동을 감시하며 필요할 때마다
    알맞은 콜백을 불러줄 것이다.  콜백-주도적 프로그래밍은 ACE 에서 큰
    의미를 지니므로 익숙해 지는게 좋다.  만약 시그널 핸들러가 무언가를
    잡아내면 플래그가 설정되고 루프가 종료 될 것이다. 편리하게도,
    handle_events() 이 실행 도중일지라도 시그널을 받기 때문에, while()
    루프로 돌아오게 될 것이다. (만약 이벤트 루프가 시그널에 의해 중단
    되길 원치 않는다면, ACE_Reactor 의 open() 메소드에 'restart' 플래그를
    확인하라.) */
  while (!finished)
    reactor.handle_events ();

  ACE_DEBUG ((LM_DEBUG, "(%P|%t) shutting down server daemon\n"));

  return 0;
}

#if defined (ACE_HAS_EXPLICIT_TEMPLATE_INSTANTIATION)
template class ACE_Acceptor <Client_Handler, ACE_SOCK_ACCEPTOR>;
template class ACE_Svc_Handler<ACE_SOCK_STREAM, ACE_NULL_SYNCH>;
#elif defined (ACE_HAS_TEMPLATE_INSTANTIATION_PRAGMA)
#pragma instantiate ACE_Acceptor <Client_Handler, ACE_SOCK_ACCEPTOR>
#pragma instantiate ACE_Svc_Handler<ACE_SOCK_STREAM, ACE_NULL_SYNCH>
#endif /* ACE_HAS_EXPLICIT_TEMPLATE_INSTANTIATION */

이제 client_acceptor.h 를 살펴보자, 어떻게 클라이언트들을 접속시키는지 알고 싶은데, 꽤 복잡할 것이라 예상하고 있을 것이다. 맞을까? 틀렸다.

ACE 를 사용할 수록, 중요한 항목들이 벌써 해결 되어 있다는 것을 알게 될 것이다. 클라이언트 컨넥션 수락을 하는데에는 어짜피 여러가지 방법이 존재하지 않는다. ACE 팀은 C++ template 이 모든 일을 해줄 수 있도록 하는 접근법을 선택했다. 당신이 해야할 일은 새로운 컨넥션이 접속될 때 만들어질 object type 을 제공하는 것 뿐이다.

// $Id: ACE_c6_a9_c5_e4_b8_ae_be_f3005_2f_b8_d6_c6_bc_be_b2_b7_b9_b5_e5_b8_a6_c1_a2_c7_cf_b1_e2_c0_fc_bf_a1,v 1.2 2004/05/20 12:30:07 redpixel Exp redpixel $

#ifndef CLIENT_ACCEPTOR_H
#define CLIENT_ACCEPTOR_H

/* ACE_Acceptor<> template 은 ace/Acceptor.h 헤더 파일 내에 정의되어
   있다.  ACE 객체 들과 헤더간의 이름에 고정된 관습있는데 예를 들어
   ACE 객체 ACE_Foobar 는 ace/Foobar.h 에서 찾을 수 있을 것이다. */

#include "ace/Acceptor.h"

#if !defined (ACE_LACKS_PRAGMA_ONCE)
# pragma once
#endif /* ACE_LACKS_PRAGMA_ONCE */

/* 소켓들을 사용할 것이므로, SOCK_Acceptor 를 사용하여 클라이언트들이
   접속할 수 있도록 한다. */
#include "ace/SOCK_Acceptor.h"

/* 클라이언트가 접속되면 우리가 제작한 Client_Handler 객체가 사용될
   것이다.  ACE_Acceptor<> template 에 필요한 첫번째 파라메터는
   이떤 객체다.  어떤 경우에는, 그냥 클래스에 forward 선언을 하는 것으로
   되지만, 그렇지 않으면 모두 있어야 한다. */
#include "client_handler.h"

/* ACE_Acceptor<> 가 하는 일은 소켓 접속을 시도를 기다리고, 접속이
   일어나면 Client_Handler 객체를 만드는 것이다.  ACE_Acceptor<> 가
   있는지 모르고, 튜토리얼 001에서 직접 basic acceptor logic 을 만들
   었었다.  ACE templates 가 지루한 내용들을 거의 다 해결해 버렸기
   때문에, 오히려 식상해 졌을지도 모르겠다. */
typedef ACE_Acceptor <Client_Handler, ACE_SOCK_ACCEPTOR> Client_Acceptor;

#endif /* CLIENT_ACCEPTOR_H */

Ok. 이제 acceptor 를 설정하는 main() 루프가 완성 됐고, 얼마나 acceptor 객체를 만드는 것이 얼마나 간단한지 봤다. 여기 까지는 그다지 어려운 코드를 짜지 않았지만.. 이제 그럴 차레가 온 듯 하다.


첫번째로, 우리는 client_handler.h 에서 Client_Handler 의 선언을 보고 프로그램이 어디서 진짜 일을 하는 지 보기 위해 내용을 살펴봤다.


// $Id: ACE_c6_a9_c5_e4_b8_ae_be_f3005_2f_b8_d6_c6_bc_be_b2_b7_b9_b5_e5_b8_a6_c1_a2_c7_cf_b1_e2_c0_fc_bf_a1,v 1.2 2004/05/20 12:30:07 redpixel Exp redpixel $

#ifndef CLIENT_HANDLER_H
#define CLIENT_HANDLER_H

/* ACE_Event_Handler 객체 계통의 어딘가에 우리만의 client handler 가 존재해야 한다.
   ACE_Reactor 의 요구사항인데, 각각의 event handler 가 ACE_Event_Handler
   포인터를 가지고 있어야 하기 때문이다.  ACE_Event_Handler로 부터 Client_Handler를
   직접 상속 받더라도, 실제 구현을 위해서는 ACE_SOCK_Stream 을 반드시 만들어야 한다.
   (역자 - ACE_SOCK_Stream 은 TCP 프로토콜 객체임) 따라서, ACE_Event_Handler를 직접
   상속해서 사용하려면 직접 ACE_SOCK_Stream 인스턴스의 관리를 해줘야 한다.

   (__이부분 부터__)
   다행히도 대부분의 ACE_SOCK_Stream 관련 일들을 자동으로 처리해 놓은 ACE 객체가
   있다.  ACE_Event_Handler 를 상속받은 ACE_Svc_Handler 템플리트가 그 것이다. 우리는
   여기서 이 것을 사용할 것이다.
   (__이부분 까지__는 완전히 제 맘대로 의역함.. 원래 문장이 의미전달에 별로 좋지
    않았다고 생각함 괜찮으시면 이 주석도 지워주십시요. -비엽(김한주 aka. Cronan) */

#include "ace/Svc_Handler.h"

#if !defined (ACE_LACKS_PRAGMA_ONCE)
# pragma once
#endif /* ACE_LACKS_PRAGMA_ONCE */

#include "ace/SOCK_Stream.h"

/* ACE_Svc_Handler 의 기능 중 하나는 ACE_Task<> 의 인터페이스들을 보여줄(사용할?)
   수 있다는 것이다.  아래의 ACE_NULL_SYNCH 파라메터가 이 것을 나타낸다.. 이
   튜토리얼의 내용과는 전혀 상관 없어보이지만, 다음 튜토리얼의 동시처리(concurrency)
   옵션에서 이 것을 다시 다룰 것 이다.  */
class Client_Handler : public ACE_Svc_Handler <ACE_SOCK_STREAM, ACE_NULL_SYNCH>
{
public:
  // 생성자...
  Client_Handler (void);

  /* destroy() 메쏘드를 소멸자로 사용할 것을 권장한다. 소멸자 오퍼레이터를
    오버로드하여 사용할 수도 있겠지만, 직관적이지도 않고 쉽지도 않다. (적어도
    필자에겐)  대신, 새로운 소멸자를 위해 새로운 메쏘드를 만들고 소멸자를
    보호하여 우리 자신과, 상속되거나, friend 만 호출할 수 있도록 한다. 
    이 정도면 괜찮은 타협이라고 생각한다. */
  void destroy (void);

  /* ACE 객체의 대부분은 open() 메쏘드를 가지고 있다.  이 메쏘드는 객체가
     일을 할 수 있도록 준비 시킨다.  ACE_Event_Handler 는 오버라이딩 할
     수 있는 virtual open() 메쏘드가 있다.  ACE_Acceptor<> 는 클라이언트가
     접속하여 새로운 Client_Handler 를 만들 때, 이 메쏘드를 호출할 것 이다.
     open() 의 파라메터는 void* 형인데, 우리를 만들었던 acceptor 를 가리키는
     포인터 이다.  그러므로 ACE_Acceptor<>*를 가리키게 해야 하지만,
     ACE_Event_Handler가 일반적인 이상, 어떤 객체를 선택해야 할 지 애매모호
     하다.  이 부분에 대해서는  open() 함수 구현에서 어떻게 하는지 알게 될
     것이다. */
  int open (void *acceptor);

  /* 등록된 handler 에서 읽기 이벤트를 감지하면, 해당 handler의 handle_input() 이
     호출된다..  이 메쏘드가 오류 코드 (-1) 를 리턴하면 reactor 는 handle_close()
     를 불러 스스로 지우려고 시도 한다.  event handler 가 여러가지 유형의 콜백
     등록을 허용하므로,  어떤 메쏘드에서 오류를 내었는지를 handle_close() 에서
     확실히 알 수 있도록 하기 위해 ACE_Reactor_Mask 타잎의  콜백 마스크가 제공
     된다.  이렇게 함으로써 handle_* 메쏘드 에서 따로 상태 정보를 관리할 필요가
     없어진다. <handle> 파라메터에 대한 것은 메쏘드 구현에서 설명할 것이다.
     부수적인 효과로, reactor 는 -1 를 리턴한 객체에 대해 remove_handler() 도 함께
     호출한다.  결국 이 메쏘드도 직접 호출할 필요가 없다. */
  int handle_close (ACE_HANDLE handle,
                    ACE_Reactor_Mask mask);

protected:

  /* reactor 에 등록 할 때, 읽기 이벤트의 통보를 원한다고 알려줘야 한다.
     reactor 가 핸들로 부터 무언가를 읽을 것이 있다고 감지하면, handle_input()
     이 호출 될 것이다.  여기서 제공되는 _handle 은 이 행동을 일으킨 핸들
     (UNIX 에서의 파일 디스크립터)이다.  우리가 ACE_Svc_Handler<> 상속했고
     ACE_SOCK_Stream이 자동으로 생겼으므로, 이 핸들은 필요없는 여분의 데이터라
     할 수 있다.  허나, 만약 우리가 ACE_Event_Handler 로 부터 직접 상속을 받을
     일이 있을 때는 peer 정보가 어디에도 존재하지 않으므로 handle 정보는
     클라이언트 데이터를 읽을 때 중요한 정보가 될 것이다. */
  int handle_input (ACE_HANDLE handle);

  /* ACE 와는 아무 상관도 없는 메쏘드다.  필자는 이 메쏘드를 handle_input()
     에서 호출할 worker 함수로 사용한다.  이 메쏘드는 앞으로 다룰
     동시발생(concurrency)에 대한 튜토리얼에서 중요한 역할을 할 것이다.
     이 메쏘드는 프로그램 레벨(application-level)의 코드로 생각해도 무방하며,
     다른 코드들은 모두 프레임워크 코드라고 보시기 바란다. */
  int process (char *rdbuf, int rdbuf_len);

  /* 소멸자에선 아무 것도 하지 않길 원하지만, 객체 보호 차원에서 선언을 해 둔다.
     위에서 언급했 듯이, 소멸자에 넣을 내용들은 모두 destory() 메쏘드 내로 넣을
     것을 추천한다. */
  ~Client_Handler (void);
};

#endif /* CLIENT_HANDLER_H */

이제 드디어 뭔가 코드를 작성해야 하는 client_handler.cpp 이다. 이 파일에는 다른 파일을 모두 합친 것보다 많은 코드가 들어갈 것 이다.

// $Id: ACE_c6_a9_c5_e4_b8_ae_be_f3005_2f_b8_d6_c6_bc_be_b2_b7_b9_b5_e5_b8_a6_c1_a2_c7_cf_b1_e2_c0_fc_bf_a1,v 1.2 2004/05/20 12:30:07 redpixel Exp redpixel $

/* client_handler.h 에서 Client_Acceptor 포인터를 사용 하였으므로,
   Client_Acceptor 객체가 선언되어야 한다.

   사실 client_acceptor.h 가 client_handler.h 를 include 하므로
   이 파일을 다시 포함하는 것은 중복된 코드를 발생 시킨다. 그렇지만,
   이 문제가 발생할 것에 대비해 미리 처리를 해놓았으므로 명확히 하기
   위해 그냥 적어두도록 한다.

   여기서는 이 것 이외에 어떤 ACE 헤더 파일들을 포함하지 않는다.
 */
#include "client_acceptor.h"
#include "client_handler.h"

/* 생성자는 아무 것도 하지 않는다. 이 것은 예외를 발생시키고 싶지 않고,
   생성자 실패를 알려줄 좋은 방법이 없기 때문에 기본적으로 좋은 생각에
   속한다.  필자 방식대로 할 수만 있다면 실패 했을 때 return 0 을 리턴할
   텐데... 쩝... */
Client_Handler::Client_Handler (void)
{
}

/* 소멸자 역시 아무 것도 하지 않는다. 이 것도 디자인에 의한 것이다.
   필자는 진심으로 destory() 메쏘드를 소멸자 대신 사용하길 원한다. 
   따라서, 소멸자 호출시에 아무 것도 하지 않는다. */
Client_Handler::~Client_Handler (void)
{
  // 파괴될 때 peer 가 확실히 닫히도록 한다.  소멸자가 불러진 다는 것은
  // 곧 peer 가 이미 닫혔다는 사실을 반증하지만, 확실히 해두는 것도 나쁘지 않다.
  this->peer ().close ();
}

/* 그렇게 얘기하던 destroy() 메쏘드다!  필자가 이렇게 강조하는 이유는 소멸자
   내에서 무언가를 하는 것은 Bed Idea (TM) 이기 때문이다.  그렇다고는 하지만,
   현재 이 메쏘드의 유형은 void 다, 사실 int 를 리턴해서 호출자에게 문제가
   있으면 알려줘야 한다.  void 를 가지더라도 예외를 발생(throw exception)
   시키는 방법이 있지만,  역시 소멸자 내에서의 그런 것은 번거롭다. */
void Client_Handler::destroy (void)
{
  /* reactor 에게 이 객체에 대한 모든 것을 잊으라고 알려준다.  open()
     메쏘드에 사용하였던 인자를 여기에 사용하자. 덧붙여, DONT_CALL 
     플래그를 넣어 handle_close()가 불려지지 않도록 해야 한다. 여기까지
     왔다는 말은 handle_close() 가 불려졌기 때문이므로, 다시 hanle_close()
     가 발생한다면 재귀호출로 인한 더러운 무한루프에 빠질 것이다! */
  this->reactor ()->remove_handler (this, ACE_Event_Handler:: READ_MASK | ACE_Event_Handler::DONT_CALL);

  /* 이 부분이 바로 소멸자를 사용하지 말라고 할 수 있는 근거가 된다.
     우리 자신을 파괴하므로, 오브젝트가 확실하게 메모리 누수에 대해 방지될
     수 있다. */
  delete this;
}

/* 전에 언급 하였듯이, open() 메쏘드는 새로운 접속이 발생했을 때
   Client_Acceptor 에 의해 호출된다. Client_Acceptor 인스턴스 포인터는
   void* 로 캐스팅 되어 이 곳에 오게 된다.  이제 전역 데이터 대신 이
   것을 사용하도록 하자. */
int Client_Handler::open (void *_acceptor)
{
  /* void* 를 Client_Acceptor*로 변환한다.  ACE_*_cast 매크로를 사용할
     수도 있지만, 필자는 어떻게 그리고 언제 사용해야 될지를 모르겠다.
     그렇지만 void* 로 부터는 아무거나 캐스팅해도 컴파일러가 경고를 주지
     않으므로 이런 것을 할 때는 매우 조심해야 한다.  새로운 캐스팅 스타일
     을 사용하면 이런 것에 대비할 수 있다. (역자: More Effective C++ 에
     나오지요: static_cast<type definition>(data) */
  Client_Acceptor *acceptor = (Client_Acceptor *) _acceptor;

  /* 우리 자신을 등록할 때 reactor 참조가 설정된다, 이 곳에서 하기로 결정
     한 별다른 이유는 없다... */
  this->reactor (acceptor->reactor ());

  /* 접속된 클라이언트의 주소를 저장하려면 이 것이 있어야 한다. 추후에
     디버그 메세지 출력에 이용할 것 이다. */
  ACE_INET_Addr addr;

  /* ACE_Svc_Handler 템플리트 클래스는 숨겨진 ACE_SOCK_Stream로의 접근을
     위해 peer() 메소드를 제공한다.  이 객체에서 get_remote_addr() 를
     통해 클라이언트의 주소 정보를 가진 ACE_INET_Addr 를 리턴 받을 수
     있다.  거의 대부분의 ACE 메쏘드가 그렇듯 에러가 나면 -1 을 리턴한다.
     ACE_INET_Addr을 통해 클라이언트의 호스트이름,TCP/IP 주소,TCP/IP
     포트 값등의 질의에 사용할 수 있다. 하나 주의: ACE_INET_Addr 의
     get_host_name() 메쏘드는 서버에서 호스트이름을 찾을 수 없으면 빈
     문자열을 리턴한다.  그에 반해 get_host_addr() 은 TCP/IP 주소를
     나타내는 숫자와 점으로 이루어진 문자열을 리턴한다. */
  if (this->peer ().get_remote_addr (addr) == -1)
    return -1;

  /* 클라이언트 주소를 얻을 수 있게 되면, 진짜 유효한 클라이언트와 접속이
     이루어진 것이다.  가끔, 유효하지 않은 클라이언트가 매우 빨리 접속을
     했다가 바로 끊는 경우가 있다.  접속된 컨넥션을 가지고 있을 필요가
     있는가 체크하기 위하여 위와 같이 반드시 체크해야 한다.

     이제, reactor 에 우리 자신을 등록하여 무언가 읽을 것이 있으면 통보해
     달라고 하였다.  처음에 만들었던 acceptor 의 reactor 값을 여기서 사용
     한다는 것이 주의하라.  싱글쓰레드 구현에 연구 목적이 있으므로 이렇게
     해도 옳다. */
  if (this->reactor ()->register_handler (this, ACE_Event_Handler::READ_MASK) == -1)
    ACE_ERROR_RETURN ((LM_ERROR, "(%P|%t) can't register with reactor\n"), -1);

  /* 여기서 접속된 클라이언트의 이름을 메세지로 출력하기 위해 ACE_INET_Addr
     객체를 사용한다.  다시 말하지만, 호스트 이름으로 빈 문자열을 받을 수
     있는데 서버의 DNS 설정이 제대로 되어 있지 않거나, 무언가 TCP/IP 주소를
     호스트 이름으로 변환할 수 없는 이유가 있기 때문이다. */
  ACE_DEBUG ((LM_DEBUG, "(%P|%t) connected with %s\n", addr.get_host_name ()));

  /* 성공시에는 언제나 0 을 리턴 한다. */
  return 0;
}

/* open() 메쏘드에서 Reactor 등록과 읽을 데이터가 있을 때 통보해 달라는
   요청을 하였다.  Reactor 가 클라이언트로 부터 읽을 데이터가 생기면
   아래 메쏘드인 handle_input() 을 호출해 준다.  필자가 전에 언급했듯이,
   여기서 _handle 파라메터는 사용하지 않을 것이지만, virtual 메쏘드 이므로
   오버라이딩을 피하기 위해 reactor 에게 메쏘드 목록을 한정시킨다. */
int Client_Handler::handle_input (ACE_HANDLE handle)
{
  /* 어떤 컴파일들은 이 파라메터를 사용하지 않으면 경고를 뱉는다. 이
     매크로를 사용하여 컴파일러가 조용히 있게 만든다. */
  ACE_UNUSED_ARG (handle);

  /* 이제 데이터를 받을 버퍼를 만들고 초기화 한다.  이 것은 간단한
     테스트 프로그램이므로 작은 크기의 버퍼를 사용한다. */
  char buf[BUFSIZ];

  /* data 영역의 포인터를 인자로 process() 메쏘드를 호출한다. 데이터를
     어떻게 다룰 것인가는 이 메쏘드에게 맡기도록 한다.  그냥 데이터를
     읽어서 결과만 process()에 보내는 방법도 괜찮지만, 프로그램의
     로직이 약간 받은 데이터에 더 받을 것이 있는가?와 같은 판단을
     내리는 행동을 해야 한다면, 프로그램-로직 단계에 이 것들을
     넣는 것이 바람직 하다. */
  return this->process (buf, sizeof (buf));
}

/* handle_input() 으로 부터 -1 을 리턴 받거나, reactor 가 다른 문제가
   생긴 것을 감지하면 handle_close() 가 호출된다.  reactor framework가
   소멸자에 대해 신경을 써줄 것이므로 여기서 destroy() 메쏘드를 따로 호출
   하는 대신 바로 자기 자신을 파괴한다. */
int Client_Handler::handle_close (ACE_HANDLE handle, ACE_Reactor_Mask mask)
{
  ACE_UNUSED_ARG (handle);
  ACE_UNUSED_ARG (mask);

  delete this;
  return 0;
}

/* 마지막으로 프로그램-로직(Application-logic) 단계에 왔다. 여태까지 했던 모든 것과는 달리
   이 부분이 바로 진짜 자신의 프로그램에서 신경써야 하는 곳이다. 이 메쏘드
   에서는 클라이언트 데이터를 받고 처리하는 작업을 한다.  진짜 프로그램
   에서는, main() 안에 이 것과 관련되지 않은 다른 무언가가 있겠지만 그
   것을 마친 후에는 실제 행동은 여기서 이루어 진다. */
int Client_Handler::process (char *rdbuf, int rdbuf_len)
{
    ssize_t bytes_read = -1;

  /* 클라이언트로 부터 데이터를 받기 위해 제공된 버퍼(char* rdbuf)를 사용
     한다.  만약 읽기 오류가 나면 (recv() 가 -1 을 리턴하면) 컨넥션이 끊어
     졌다고 보는게 좋다.  또한, zero 바이트를 받았을 경우에도 뭔가 잘못된
     것이다.  reactor 는 읽기 이벤트가 발생하지 않으면 이 메쏘드를 호출하지
     않으므로 호출 자체가 되지 않아야 정상이다.  그 외에는 보여줄 데이터를
     디버그 메세지로 출력한다. */
    switch ( (bytes_read = this->peer ().recv (rdbuf, rdbuf_len)) )
    {
    case -1: // 오류를 출력하고 리턴한다.
      ACE_ERROR_RETURN ((LM_ERROR,
                         "(%P|%t) %p bad read\n",
                         "client"),
                        -1);
    case 0: // 오류를 출력하고 리턴한다.
      ACE_ERROR_RETURN ((LM_ERROR,
                         "(%P|%t) closing daemon (fd = %d)\n",
                         this->get_handle ()),
                        -1);
    default: // 데이터를 출력
        // 출력하기 전에 문자열 뒤에 NULL 문자를 추가한다. (역자: 어디에 추가했지? -_-? rdbuf[rdbuf_len] = '\0'; 을 빼먹은 건가? 하하)
      ACE_DEBUG ((LM_DEBUG,
                  "(%P|%t) from client: %s",
                  rdbuf));
    }

   /* recv() 는 recv_n() 이라는 비슷한 함수가 있다는 것을 언급하고 싶다.
      recv_n() 을 사용하면 정확히 몇 바이트 받기를 원하는 가를 정할 수
      있기 때문에, 얼마나 받을지가 확실하다면 매우 유용하게 사용할 수
      있다.  안타깝게도, 이 프로그램에서는 클라이언트가 얼마나 보낼
      것인지가 불분명 하므로 불친절한 recv() 를 이용해 제한선(rdbuf_len)
      만 설정해 준다. 클라이언트가 얼마나 보낼 것인가 모를 때에는 이 것이
      알맞다. */

  return 0;
}

끝내며 #

튜토리얼 5는 이 것으로 끝이다. 이 튜토리얼에서는 싱글 쓰레드를 사용하는 reactor 기반의 서버를 제작하였다. 구현에 있어서 실제로 별로 필요없는 것들을 몇가지 다뤘지만, 이는 앞으로 다룰 두가지 전략: thread per connect 과 thread poll 들에 사용하기 위함이다.

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2010-10-28 12:42:52
Processing time 0.5042 sec