Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

함수 포인터로 구현하는 전술 패턴

소개

함수 포인터를 이용한 전술 패턴의 구현입니다. 2004년에 만든 문서인데, 생각난김에 내용을 수정/보완했습니다. 함수포인터에 대한 일반적인 내용은 함수 포인터의 사용문서를 참고하시기 바랍니다.

이 문서는 함수 포인터의 응용입니다.

전술 패턴 응용

전술 패턴(:12)은 실행시간에 알고리즘을 선택하기 위한 목적으로 사용합니다. 정책을 코드화 한 다음, 선택할 수 있게 한다는 의미에서 policy pattern이라고 부르기도 합니다. 이 패턴은 알고리즘 군을 정의한다음 이들을 캡슐화하고 코드상에서 필요에 따라서 선택하도록 합니다. 이 패턴을 이용해서 만들어진 코드는 소스코드의 변경 없이 알고리즘을 추가할 수 있게 됩니다.

함수 포인터를 전술 패턴에 응용하기 위해서, 프로그램을 직접 만들어 보기로 했습니다. 이 프로그램은 사용자의 요청을 읽어서 처리하는 프로그램입니다. HTTP 서버 프로그램과 비슷하다고 보시면 되겠습니다.

서버는 사용자의 다양한 요청을 분류하고 그에 맞는 알고리즘을 선택해서 작업을 해야 합니다. 가장 일반적인 방법은 if else, case문을 사용하는 겁니다. 조건 문을 이용해서 알고리즘을 선택하는 거죠. 이 방법은 단순하고 이해하기 쉽긴 하지만, 새로운 코드가 추가될 때마다 메인 소스코드에 직접 분기문을 늘려야 한다는 단점이 있습니다.

다음 같은 경우죠
클라이언트의 프로토콜 메시지를 분석한다.
if (로그인 요청 이라면)
{
  DB에 연결해서 이런 저런 작업..  
}
else if (사용자 정보 요청이라면)
{
  로그인 되었는지 여부 확인후 DB에 연결해서..
}
else if (사용자 정보 변경요청)
{
  ...
}
else if (...)
{
  ...
}
....
보기에도 힘들고, 복잡하고 뭔가 일관성이 없어 보입니다. 유지/ 보수성도 좋지 않고요. 이 코드를 바꿔보려고 합니다.

전술 패턴을 이용한 간결한 코드 생성

아이디어는 간단합니다. 유저의 요청 문자열을 키로하는 map을 만들고, 값으로는 해당 문자열을 처리하기 위한 함수의 포인터를 등록하는 겁니다. 예를 들어 유저가 "Helo"라는 요청을 했다면 FuncHelo 함수를 호출하는 식입니다.

그러기 위해서는 최초에 프로토콜 문자열 이름과 관련된 함수포인터를 등록하는 루틴 즉 command map을 만드는 루틴, 나중에 문자열로 검색해서 함수포인터를 돌려주는 루틴이 필요할 겁니다. 자료구조를 만들기가 귀찮은 저는 STL(:12)의 map을 이용하기로 했습니다.

저는 파일 다운로드 서비스를 하는 서버 프로그램을 만들려고 합니다. 이경우에 최소한 "login", "list", "down", "quit" 의 프로토콜이 필요 할 겁니다. 다음과 같은 자료구조를 만들어서 등록하기로 했습니다.

typedef map<string, void *(*)(void *, class FuncArg *)> ProcessingMap; 
int ProtocolFuncRegist()
{
  // 함수포인터를 등록한다.
  ProcessingMap["login"] = ProcessLogin;
  ProcessingMap["list"] = ProcessList;
  ProcessingMap["down"]	= ProcessDown;
}

void *ProcessLogin(void *data, class FuncArg *aFuncArg)
{
  // 아이디를 이용해서 유효한 유저인지를 검사한다.	
}

void *ProcessList(void *data, class FuncArg *aFuncArg)
{
  // 파일의 리스트를 보여준다.
}

void *ProcessDown(void *data, class FuncArg *aFuncArg)
{
  // 파일을 전송한다. 
}

void *ProcessQuit(void *data, class FuncArg *aFuncArg)
{
  // 종료한다.
}
포인터로 전달될 함수는 두개의 매개 변수를 가지고 있습니다. 첫번째 매개 변수는 클라이언트의 요청 문자열이고, 두번째 매개 변수는 세션 정보를 담고 있는 클래스입니다. 데이터베이스나 파일을 열었을경우 데이터 베이스 포인터나 파일 포인터를 전달하기 위한 목적으로 사용할 겁니다. 혹은 해당 클라이언트의 상태, 로그인을 끝마친 유저인지, 권한이 있는 유저인지등에 대한 부가적인 정보를 넘기기 위해서 사용할 수도 있을 겁니다.

파일 전송 프로그램 만들기

만들고자 하는 프로그램은 파일을 전송하는 간단한 프로그램으로 로그인과정을 거치면, 파일리스트를 확인하고 원하는 파일을 다운로드 합니다.

그러나!! 이러한 모든 코드를 실제로 구현하지 않을 겁니다. 함수포인터 응용 방법을 다루는 것이므로 여기에만 집중할 겁니다. 실제 파일을 다운로드 받고, 리스트를 던져주는 실제 코드는 만들지 않도록 할것이다. eotls 메시지만 뿌려주는 껍데기 코드로 대체하려합니다.

통신 프로토콜

다음과 같이 헤더 + 바디로 구성한 초 간단 프로토콜을 사용할 겁니다.
struct ProtocolHeader
{
	int size;      // 데이터 사이즈 sizeof(int) + sizeof(int) + size(data) 
	char data[];   // 실제 데이터
	void H2N()
	{
		size = htonl(size);
		status = htonl(status);
	}
	void N2H()
	{
		size = ntohl(size);
		status = ntohl(status);
	}
}

  |<-------- Size ---------->|
  +----+----------------
  |SIZE| DATA ....
  +----+----------------

서버 프로그램 작성

서버 프로그램은 RealTimeSignal을 이용해서 다중의 클라이언트를 처리하도록 할 겁니다.

함수포인터를 맵에 등록시키는 것 외에는 일반적인 네트워크 프로그램과 동일합니다. 이외에 추가시켜야할게 있다면 클라이언트의 연결정보와 쓰레드 풀 관리를 위한 자료구조정도가 될 겁니다 해당 클라이언트가 연결을 했는지, 연결을 했다면 로그인을 마친 상태인지 등의 정보를 유지할 필요가 있기 때문이다. 쓰레드 풀 자료구조는, 쓰레드 풀에 등록되어 있는 쓰레드가 현재 몇개의 클라이언트를 처리하는지의 자료를 유지해서 가장 적은 클라이언트를 처리하는 쓰레드에게 새로 등록된(accept)클라이언트를 할당하기 위한 목적으로 사용합니다.

RTS와 쓰레드풀, 함수포인터에 대한 몇개의 소개된 글을 봤다면 소스에 대한 이해는 주석만으로 충분할 겁니다. 제시되는 코드는 기본적인 기능만을 수행하는데 중점을 두고 만들었습니다. 효율성, 아름다움등은 기대하지 마세요.

#ifndef __USE_GNU
#define __USE_GNU
#endif

#include <signal.h>
#include <fcntl.h>

#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <iostream>

#include <map>
#include <vector>

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define MAXDATALENTH	512


using namespace std;

int gpid;

// 클라이언트 데이터 정보 구조체
struct clidata
{
  int sockfd;			// 처리할 소켓 번호
  int signum;			// 처리할 RTS번호이자 처리할 쓰레드의 ID
  char cbuf[512];		// 처리해야할 데이터	

  // 아래로는 쓰이지 않는다.
  // 프로그램이 해야하는 일에 따라서 다양한 변수를 선언해서
  // 사용할 수 있을 것이다.
  int status;
  int uid;
  char regdate[36];
  char serviceinfo[256];
  char ip[36];
  char port[8];
};

map<int, struct clidata> ClientList;    // 처리할 클라이언트 데이타 정보 
map<int, struct clidata>::iterator CLi;

// 함수포인터 호출시 넘겨줄 데이터 클래스
// 역시 프로그램이 하는일에 따라서 다양하게 선언할 수 있을 것이다. 
class FuncArg
{
  private:
    int status;
  public:
};


typedef struct _fd_sig
{
	int signum;
	int pid;
	int uid;
} FdPerSignum;

pthread_mutex_t MutexLock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  SyncCond  = PTHREAD_COND_INITIALIZER;

// 쓰레드 풀관리를 위한 map 자료구조
multimap<int, FdPerSignum> PollMap;
multimap<int, FdPerSignum>::iterator PollMapi;

// 프로토콜에 대해서 호출될 함수포인터를 포함할 map 
typedef map<string, void *(*)(void *, class FuncArg *)> __ProcessingMap;
__ProcessingMap ProcessingMap;
__ProcessingMap::iterator ProcessingFunci;

// login 문자열을 만났을 때 호출할 함수
void *ProcessLogin(void *data, class FuncArg *aFuncArg)
{
  struct clidata lclidata;
  lclidata = *(struct clidata *)data;
  printf("Read socket(%d) : LoginProcess : %s\n", lclidata.sockfd, 
    lclidata.cbuf);
}

// list 문자열을 만났을 때 호출할 함수
void *ProcessList(void *data, class FuncArg *aFuncArg)
{
  struct clidata lclidata;
  lclidata = *(struct clidata *)data;
  printf("Read socket(%d) : ListProcess : %s\n", lclidata.sockfd,
     lclidata.cbuf);
}

// down 문자열을 만났을 때 호출할 함수
void *ProcessDown(void *data, class FuncArg *aFuncArg)
{
  struct clidata lclidata;
  lclidata = *(struct clidata *)data;
  printf("Read socket(%d) : DownProcess : %s\n", lclidata.sockfd,
    lclidata.cbuf);
}

// quit 문자열을 만났을 때 호출할 함수
// 소켓을 종료한다. 
void *ProcessQuit(void *data, class FuncArg *aFuncArg)
{
  struct clidata lclidata;
  lclidata = *(struct clidata *)data;
  printf("Read socket(%d) : QuitProcess : %s\n", lclidata.sockfd,
    lclidata.cbuf);
  close(lclidata.sockfd);
}

// 파일지정자가 리얼타임 시그널에 대응하도록 설정한다.
int setup_sigio(int fd, int sig_num, int pid)
{
  if(fcntl(fd, F_SETFL, O_RDWR|O_NONBLOCK|O_ASYNC) < 0)
  {
    printf("setup nonblocking error:%d\n", fd);
  }

  if(fcntl(fd, F_SETSIG, SIGRTMIN+sig_num) < 0)
  {
    printf("Couldn't set signal %d on %d\n", SIGRTMIN, fd);
    return -1;
  }

  if(fcntl(fd, F_SETOWN, pid) < 0)
  {
    printf("Couldn't set owner %d on %d\n", pid, fd);
    return -1;
  }
  return 0;
}

// 클라이언트로 부터 읽어들인 데이터를 처리할 쓰레드 함수
void * CliDataProcessing(void *rts_num)
{
  int signum = *((int *)rts_num);
  socklen_t socklen;
  char buf[MAXDATALENTH];
  class FuncArg FArg;

  struct sockaddr_in sockaddr;
  struct siginfo sinfo;

  FdPerSignum fdpersignum;

  int clientnum;
  int ret;
  int readlen;
  int datalen;

  sigset_t sigset;

  gpid = getpid();
  sigemptyset(&sigset);
  sigaddset(&sigset, SIGRTMIN+signum);
  sigprocmask(SIG_BLOCK, &sigset, NULL);

  pthread_mutex_lock(&MutexLock);
  usleep(500);
  pthread_cond_signal(&SyncCond);
  pthread_mutex_unlock(&MutexLock);

  sigemptyset(&sigset);
  sigaddset(&sigset, SIGRTMIN+signum);
  sigprocmask(SIG_BLOCK, &sigset, NULL);
  while(1)
  {
    struct clidata lclidata;
    socklen = sizeof(sockaddr);

    // 쓰레드가 처리하기로 지정된 소켓에서 리얼타임 시그널이 발생하는걸 기다린다.
    ret = sigwaitinfo(&sigset, &sinfo);
    if (ret == (SIGRTMIN + signum))
    {
      // 받을 데이터 크기를 읽어온다
      if ((readlen = read(sinfo.si_fd, (void *)&datalen, sizeof(int))) <= 0)
      {
        // 만약 데이터를 읽는 도중 에러가 발생했다면, 
        // 소켓을 닫고
        // 쓰레드풀의 값을 재조정한다.
        PollMapi = PollMap.begin();
        while(PollMapi != PollMap.end())
        {
          if(PollMapi->second.signum == signum)
          {
            fdpersignum = PollMapi->second;
            if (PollMapi->first > 0)
              clientnum = PollMapi->first - 1;
            PollMap.erase(PollMapi);
            PollMap.insert(pair<int, FdPerSignum>(clientnum, fdpersignum));
          }
          PollMapi++;
        }
        close(sinfo.si_fd);
      }
      else
      {
        // 클라이언트로 부터 데이터를 읽어온다.
        void *(*Func)(void *, class FuncArg *);
        int readn;
        memset(buf, 0x00, MAXDATALENTH);
        readn = read(sinfo.si_fd, buf, datalen+4);
        memset(lclidata.cbuf, 0x00, sizeof(lclidata.cbuf));
        memcpy(lclidata.cbuf, buf, datalen);

        if ((CLi = ClientList.find(sinfo.si_fd)) == ClientList.end())
        {
          lclidata.sockfd = sinfo.si_fd;
          lclidata.signum = signum;
          lclidata.status = 0;
            ClientList[sinfo.si_fd] = lclidata;
        }

        // 처리해야할 메시지에 대한 함수가 존재할 때
        // 해당 함수를 호출한다.
        ProcessingFunci = ProcessingMap.find(buf);
        if (ProcessingFunci != ProcessingMap.end())
        {
          cout << "Search Function data " << endl; 
          Func = ProcessingFunci->second;
          Func((void *)&lclidata, &FArg);
        }
        else
        {
          fprintf(stderr,"Unknown msg protocol : %s\n", buf);
        }
      }
    }
  }
}

void * socket_acceptor(void *argdata)
{
  int server_sockfd, cli_sockfd;
  int clientnum;
  FdPerSignum fdpersignum;
  socklen_t socklen;

  sigset_t sigset;
  struct sockaddr_in serveraddr, clientaddr;
  struct siginfo sinfo;

  int signum = *((int *)argdata);

  if((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  {
    perror("socket error "); 
  }
  bzero(&serveraddr, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
  serveraddr.sin_port = htons(5555);

  if (bind (server_sockfd, (struct sockaddr *)&serveraddr,
    sizeof(serveraddr)) == -1)
  {
    perror("bind error");
  }

  if(listen(server_sockfd, 5) == -1)
  {
    perror("listen error");
  }

  if(setup_sigio(server_sockfd, signum, getpid()) == -1)
  {
    fprintf(stderr, "sigio setting error\n");
  }

  gpid = getpid();
  sigemptyset(&sigset);
  sigaddset(&sigset, SIGRTMIN+signum);
  sigprocmask(SIG_BLOCK, &sigset, NULL);

  pthread_mutex_lock(&MutexLock);
  usleep(500);
  pthread_cond_signal(&SyncCond);
  pthread_mutex_unlock(&MutexLock);

  while(1)
  {
    int ret;
    printf("Wait I/O Event (%d)\n", signum);
    ret = sigwaitinfo(&sigset, &sinfo);
    printf("IO Event OK\n");
    if (ret == SIGRTMIN+signum)
    {
      printf("Accept \n");
      socklen = sizeof(clientaddr);
      cli_sockfd = accept(server_sockfd,
          (struct sockaddr *)&clientaddr,
          &socklen);

      if (cli_sockfd < 0)
      {
        fprintf(stderr, "Accept error\n");
        continue;
      }
      PollMapi = PollMap.begin();
      fdpersignum = PollMapi->second;
      clientnum = PollMapi->first+1;
      setup_sigio(cli_sockfd, PollMapi->second.signum, PollMapi->second.pid);
    cout << "할당 쓰레드 번호 " << PollMapi->second.signum << endl; 
      PollMap.erase(PollMapi);

      PollMap.insert(pair<int, FdPerSignum>(clientnum, fdpersignum));
    }
    else
    {
      // 에러처리
      cout << "Unknown Error : " << ret <<endl;
    }
  }
}

// 각 프로토콜을 처리할 함수를 등록한다.
int ProtocolFuncRegist()
{
  ProcessingMap["login"] = ProcessLogin;    
  ProcessingMap["list"] = ProcessList;
  ProcessingMap["down"] = ProcessDown;
  ProcessingMap["quit"] = ProcessQuit;
}

int main(int argc, char **argv)
{

  FdPerSignum fdpersignum;
  int status;
  int loopi, loopj;
  sigset_t sigset;
  sigaddset(&sigset, SIGRTMIN);

  int thread_num = 2;              // 쓰레드 풀의 크기
  vector<void *(*)(void *)> thread_list;    // 쓰레드 리스트
    vector<pthread_t> threadident(thread_num);  // 쓰레드 ID 저장

  // 생성될 쓰레드의 갯수만큼 리얼타임 시그널을 등록한다. 
  for(loopi = 0; loopi < thread_num +1; loopi ++)
  {
    sigaddset(&sigset, SIGRTMIN+(loopi+1));
  }

  // 등록된 리얼타임 시그널이 발생할경우 블럭되도록 설정한다.
  sigprocmask(SIG_BLOCK, &sigset, NULL);
  // 각 프로토콜을 처리할 함수를 등록한다. 
  ProtocolFuncRegist();

  // 클라이언트의 연결을 기다리는 쓰레드를 등록한다. 
  thread_list.push_back(socket_acceptor);

  // 클라이언트의 데이터를 처리하는 쓰레드를 등록한다.
  for (loopi = 0; loopi < thread_num; loopi++)
  {
    thread_list.push_back(CliDataProcessing);
  }

  for (loopi = 0, loopj =1; loopi < thread_list.size(); loopi++, loopj++)
  {
    pthread_mutex_lock(&MutexLock);
    pthread_create(&threadident[loopi], NULL, 
                        thread_list[loopi],
                        (void *)&loopj);

    pthread_cond_wait(&SyncCond, &MutexLock);

    fdpersignum.signum = loopj;
    fdpersignum.pid = gpid;
    cout << "PID is " << gpid << endl;  
    if(loopi != 0)
      PollMap.insert(pair<int, FdPerSignum>(0, fdpersignum));  

    pthread_mutex_unlock(&MutexLock);
  }

  for(loopi = 0; loopi < thread_list.size(); loopi ++)
  {
    pthread_join(threadident[loopi], (void **)&status);
  }

  return 1;
}

관련 문서

히스토리

  1. 작성일 : 2004년 12월 14일
  2. 수정일
    • 2012년 12월 27일 : docbook 스타일 문서를 wiki 형식으로 변환