epoll 서버 샘플 프로그래밍

프로그래밍/서버프로그래밍 2007/04/18 10:19

epoll 서버 샘플 프로그래밍  by xevious7   http://www.xevious7.com/52
(문서의 작성일. 2006.5월9일)
(문서의 수정및 추가 2007년 4월)
(문서의 추가 : 참초할만한 다른 좋은 소스들 2008년 3월)

거의 1년여전에 이 문서를 작성해놓고 실수로 공개를 안하고 있었습니다.
계속 비공개문서로 있었던 것이죠. 아마도 무슨 바쁜일 때문에 미처 공개하지
못하고 다른 문서들로 인하여 계속 비공개로 남아있었던 것 같습니다.
최근에 epoll에 대한 것을 테스트하다가 뒤늦게 문서가 비공개로 되어있는
것을 알았습니다. 그래서  내용도 조금 수정하고 테스트서버도 작성해보았습니다.
소스도 공개합니다.

epoll을 이용하여 작성된 완성된 훌륭한 서버소스가 많이 공개되어있지만 ,
일반적으로 공부하려는 프로그래머가 그 큰 소스의 내부를 분석하여
이해하기는 좀 무리가 있다고 생각이 듭니다.

아무래도 잘 짜여지고 다듬어지다보니
보다 추상화가 많이 되어있고 보다 감싸져있기 때문에 쓰기는 편해도 이해하기는
힘들다고 할까요
.

직관적으로 이해를 돕기 위해서 , 여기 공개하는 epoll응용
테스트서버는 C언어로 작성하였습니다.  단순한 에코형서버입니다.
접속한 모든 클라이언트에게 내용을 전달합니다. 두 클라이언트가 접속하면
서로 채팅하는것이 될것이고 여러클라이언트가 붙으면 대화방처럼 작동합니다.

여기에 간단한 프로토콜을 넣으면 (채널관리 ,룸관리) 간단한 채팅서버가
될것입니다.

다시 말하면 이해를 위한 뼈대구조의 하나의 샘플입니다.


소스코드가 매끄럽지 못합니다. 다만 직관적 이해를 위해 코드를 압축하거나
추상화하는 것은 자제했습니다.

관련링크

http://www.joinc.co.kr/modules/moniwiki/wiki.php/epoll
윗글은 epoll의 제작자가 만든 원래의 URL에(man페이지) 대한 번역과
이 링크에도 간단한 에코서버소스가 공개되어있습니다.

epoll 제작자의 글은 다음 URL에 있습니다.
http://www.xmailserver.org/linux-patches/epoll.txt


대용량 서버에 대한 방법론에 대한것은 다음문서
http://www.kegel.com/c10k.html 에서 자세히 볼 수 있습니다.

epoll을 사용하기위한 조건

epoll은 2001년 7월 11일에 다비드 라이프니치(David Libenzi)씨에 의해서
리얼타임 시그날 (RealTime Sinal)의 대안으로 제안된 이후로 2.5.x 커널에 추가되기
시작하였습니다.

그리고 2.6.x 커널대에서는 기본으로 탑재되어 있습니다.

(2.6.x 커널은 2003년 12월에 릴리즈 되었습니다. 현재까지(2007년3월) 계속되고 있습니다.)
9.x 대 리눅스라면 epoll을 사용하는데 전혀 무리가 없다는 이야기입니다.
만약  2.6.x 커널 및의 리눅스라면 위의 joinc 문서에서 사용여부 가능방법과 라이브러리
등의 설치등등의 확인 방법이 설명되어있으니 참조하기 바랍니다.

자신의 리눅스 시스템에서 epoll이 사용가능한지 체크하는 C 코드

      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/epoll.h>
     
      int main(int argc, char** argv)
      {
               int epoll_fd;
                 
               if ( (epoll_fd = epoll_create(500)) == -1 )
               {
                       printf("epoll 파일디스크립터 생성 오류 \n");
               }
               else
               {
                       printf("epoll 파일디스크립터 생성성공 \n");
                      close(epoll_fd);
                      /* 생성된 fd는 다른 fd와 마찬가지로 프로그램 종료전에 반드시 닫아주어야한다. */

               }
        }
        /* EOF */

컴파일이 안되거나 , 실행시 오류가 난다면 무엇인가 설정이 잘못되어있는 것입니다. 

epoll 을 사용하기 위한 API

epoll을 사용하기 위해서 알아야 될 API는 다음 세가지입니다.
int epoll_create(int size);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

함수의 각각의 설명은 man페이지나 제작자의 다음 링크에서 볼 수 있습니다.
http://www.xmailserver.org/linux-patches/epoll_create.txt
http://www.xmailserver.org/linux-patches/epoll_ctl.txt
http://www.xmailserver.org/linux-patches/epoll_wait.txt

한글번역문서는 jacking님의 일본man 페이지 번역이 다음링크에 있습니다.
http://jacking75.cafe24.com/Network/epoll.htm

위의 각각의 3개의 API의 이해가 소스를 이해하는데 필수적입니다. 당연한
이야기이지만요.

소스는 epoll LT모드를 사용하고있습니다. ( ET를 선언하지 않은경우
자동으로 LT입니다. 소스에 LT를 따로 선언하지 않았습니다.)
ET모드는 socket이 반드시 non blocking 소켓이어야 하지만 LT는
상관없습니다. 그래서 따로 non blocking 부분을 설정하는 부분이 없고
소켓 그대로 사용하였습니다. 또한 닫혀진 소켓에 대한 epoll set에서
삭제하는 부분도 없습니다.  이부분은 epoll 제작자의 FAQ에서 보면
닫힌 소켓에대해서 epoll set에서 자동으로 없어지는가 하는 질문에
닫힌 소켓은 자동으로 epoll set에서 없어진다라고 되어있기 때문에
따로 처리하지 않았습니다. 소켓이 닫히는순간 epoll set에서 사라집니다.

주석은 영어로 되어있습니다.  ^^;;

추가적인 참조사이트들

더 참조할만한 epoll echo server
다음링크 (청아님 홈페이지입니다.)
여기서 ET모드를 포함한 non-blocking socket 등의 좀더 정밀한
샘플소스및 설명이 있습니다.
http://chonga.pe.kr/blog/index.php?pl=919&ct1=31

추가적인 링크입니다. 장성재님 블로그에 non-blocking 모드에 대한
echo-server와  send시에 발생할수 있는 오류처리를 위한 부분까지
친철하게 추가한 소스가 있습니다.
http://blog.ilovelinux.org/2009/10/epoll-echo-server.html


테스트서버 소스
/*-----------------------------------------------------------------

  epoll server test program by EuiBeom Hwang. : 2005-2007.
  Platform : Linux 2.6.x (kernel)
  compiler Gcc: 3.4.3.
  License: GNU General Public License  
  descption : simple test server. multi client accept.
              sample skelecton epoll structure(conceptional)
              this code is just sample implemention code.
------------------------------------------------------------------*/

/* header files */
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

/* definition */
#define MAX_CLIENT   10000
#define DEFAULT_PORT 9006
#define MAX_EVENTS   10000


/* global definition */
int g_svr_sockfd;              /* global server socket fd */
int g_svr_port;                /* global server port number */

struct {
         int  cli_sockfd;  /* client socket fds */
         char cli_ip[20];              /* client connection ip */
} g_client[MAX_CLIENT];

int g_epoll_fd;                /* epoll fd */

struct epoll_event g_events[MAX_EVENTS];

/* function prototype */
void init_data0(void);            /* initialize data. */
void init_server0(int svr_port);  /* server socket bind/listen */
void epoll_init(void);            /* epoll fd create */
void epoll_cli_add(int cli_fd);   /* client fd add to epoll set */

void userpool_add(int cli_fd,char *cli_ip);

/*--------------------------------------------------------------*/
/* FUNCTION PART
---------------------------------------------------------------*/

/*---------------------------------------------------------------
  function : init_data0
  io: none
  desc: initialize global client structure values
----------------------------------------------------------------*/
void init_data0(void)
{
  register int i;

  for(i = 0 ; i < MAX_CLIENT ; i++)
  {
     g_client[i].cli_sockfd = -1;
  }
}

/*-------------------------------------------------------------
  function: init_server0
  io: input : integer - server port (must be positive)
  output: none
  desc : tcp/ip listening socket setting with input variable
----------------------------------------------------------------*/

void init_server0(int svr_port)
{
  struct sockaddr_in serv_addr;
 
  /* Open TCP Socket */
  if( (g_svr_sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0 )
  {
      printf("[ETEST] Server Start Fails. : Can't open stream socket \n");
      exit(0);
  }

  /* Address Setting */
  memset( &serv_addr , 0 , sizeof(serv_addr)) ;

  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_addr.sin_port = htons(svr_port);

  /* Set Socket Option  */
  int nSocketOpt = 1;
  if( setsockopt(g_svr_sockfd, SOL_SOCKET, SO_REUSEADDR, &nSocketOpt, sizeof(nSocketOpt)) < 0 )
  {
      printf("[ETEST] Server Start Fails. : Can't set reuse address\n");
      close(g_svr_sockfd);
      exit(0);
  }
 
  /* Bind Socket */
  if(bind(g_svr_sockfd,(struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
  {
     printf("[ETEST] Server Start Fails. : Can't bind local address\n");
     close(g_svr_sockfd);
     exit(0);
  }

  /* Listening */
  listen(g_svr_sockfd,15); /* connection queue is 15. */

  printf("[ETEST][START] Now Server listening on port %d\n",svr_port);
}
/*------------------------------- end of function init_server0 */

void epoll_init(void)
{
  struct epoll_event events;

  g_epoll_fd = epoll_create(MAX_EVENTS);
  if(g_epoll_fd < 0)
  {
     printf("[ETEST] Epoll create Fails.\n");
     close(g_svr_sockfd);
     exit(0);
  }
  printf("[ETEST][START] epoll creation success\n");

  /* event control set */
  events.events = EPOLLIN;
  events.data.fd = g_svr_sockfd;

  /* server events set(read for accept) */
  if( epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, g_svr_sockfd, &events) < 0 )
  {
     printf("[ETEST] Epoll control fails.\n");
     close(g_svr_sockfd);
     close(g_epoll_fd);
     exit(0);
  }

  printf("[ETEST][START] epoll events set success for server\n");
}
/*------------------------------- end of function epoll_init */

void epoll_cli_add(int cli_fd)
{
 
  struct epoll_event events;

  /* event control set for read event */
  events.events = EPOLLIN;
  events.data.fd = cli_fd;

  if( epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, cli_fd, &events) < 0 )
  {
     printf("[ETEST] Epoll control fails.in epoll_cli_add\n");
  }

}

void userpool_add(int cli_fd,char *cli_ip)
{
  /* get empty element */
  register int i;

  for( i = 0 ; i < MAX_CLIENT ; i++ )
  {
     if(g_client[i].cli_sockfd == -1) break;
  }
  if( i >= MAX_CLIENT ) close(cli_fd);

  g_client[i].cli_sockfd = cli_fd;
  memset(&g_client[i].cli_ip[0],0,20);
  strcpy(&g_client[i].cli_ip[0],cli_ip);

}

void userpool_delete(int cli_fd)
{
  register int i;

  for( i = 0 ; i < MAX_CLIENT ; i++)
  {
       if(g_client[i].cli_sockfd == cli_fd)
       {
          g_client[i].cli_sockfd = -1;
          break;
       }
  }
}

void userpool_send(char *buffer)
{
  register int i;
  int len;

  len = strlen(buffer);

  for( i = 0 ; i < MAX_CLIENT ; i ++)
  {
      if(g_client[i].cli_sockfd != -1 )
      {
          len = send(g_client[i].cli_sockfd, buffer, len,0);
          /* more precise code needed here */
      }
  }
 
}


void client_recv(int event_fd)
{
  char r_buffer[1024]; /* for test.  packet size limit 1K */
  int len;
  /* there need to be more precise code here */
  /* for example , packet check(protocol needed) , real recv size check , etc. */

  /* read from socket */
  len = recv(event_fd,r_buffer,1024,0);
  if( len < 0 || len == 0 )
  {
      userpool_delete(event_fd);
      close(event_fd); /* epoll set fd also deleted automatically by this call as a spec */
      return;
  }

  userpool_send(r_buffer);

}

void server_process(void)
{
  struct sockaddr_in cli_addr;
  int i,nfds;
  int cli_sockfd;
  int cli_len = sizeof(cli_addr);

  nfds = epoll_wait(g_epoll_fd,g_events,MAX_EVENTS,100); /* timeout 100ms */

  if(nfds == 0) return; /* no event , no work */
  if(nfds < 0)
  {
      printf("[ETEST] epoll wait error\n");
      return; /* return but this is epoll wait error */
  }

  for( i = 0 ; i < nfds ; i++ )
  {
      if(g_events[i].data.fd == g_svr_sockfd)
      {
          cli_sockfd = accept(g_svr_sockfd, (struct sockaddr *)&cli_addr,(socklen_t *)&cli_len);
          if(cli_sockfd < 0) /* accept error */
          {
          }
          else
          {
             printf("[ETEST][Accpet] New client connected. fd:%d,ip:%s\n",cli_sockfd,inet_ntoa(cli_addr.sin_addr));
             userpool_add(cli_sockfd,inet_ntoa(cli_addr.sin_addr));
             epoll_cli_add(cli_sockfd);
          }
          continue; /* next fd */
      }
          /* if not server socket , this socket is for client socket, so we read it */
         client_recv(g_events[i].data.fd);   

  } /* end of for 0-nfds */
}
/*------------------------------- end of function server_process */

void end_server(int sig)
{
  close(g_svr_sockfd); /* close server socket */
  printf("[ETEST][SHUTDOWN] Server closed by signal %d\n",sig);
  exit(0);
}

int main( int argc , char *argv[])
{
 
  printf("[ETEST][START] epoll test server v1.0 (simple epoll test server)\n");
  /* entry , argument check and process */
  if(argc < 3) g_svr_port = DEFAULT_PORT;
  else
  {
     if(strcmp("-port",argv[1]) ==  0 )
     {
        g_svr_port = atoi(argv[2]);
        if(g_svr_port < 1024)
        {
           printf("[ETEST][STOP] port number invalid : %d\n",g_svr_port);
           exit(0);
        }
     }
  }


  init_data0();  

  /* init server */
  init_server0(g_svr_port);
  epoll_init();    /* epoll initialize  */

  /* main loop */
  while(1)
  {
     server_process();  /* accept process. */
  } /* infinite loop while end. */

}


top

Trackback Address :: http://xevious7.com/trackback/52

  1. 섕글섕글 2007/04/18 21:35 MODIFY/DELETE REPLY

    앗.. 제가 처음 이곳을 알았을 당시 원했던 내용의 페이지네요~
    지금은 제작자의 문서 등을 어렵사리 구해보고 나름 터득한 것들이 있지만요^^;

    이 페이지를 지금 공개 해주신데에 감사하다 할까요.. 처음부터 위 내용을 보았더라면, 스스로 찾아 알아가는 길을 선택할 여유를 부릴 수 없었을테니요;

    • xevious7 2007/04/19 09:33 MODIFY/DELETE

      :) 이끌어 주는 것도 중요하지만
      첫째로 중요한 것은 스스로 하는 것 이겠지요.
      그것이 진짜고 그것이 가장 오래가기 때문에 ^^~

  2. 장성재 2007/06/03 20:07 MODIFY/DELETE REPLY

    좋은 글 공개해 주심에 감사드립니다.
    그러나, epoll 관련 API 사용법에만 중점이 있는거 같습니다.
    epoll은 성능좋은 edge trigger 일 뿐입니다.
    API 사용보다는 epoll를 non-block 소켓에서 사용할 때, 전체적인 메커니즘이
    소스에 포함되지 않아서 좀 아쉽니다.
    epoll 사용의 주요 이슈는 많은 fd 들을 이벤트 방식으로 어떻게 처리하느냐입니다.
    read/write 이벤트를 체크하도록 epoll에 어느 시점에 등록하고, 이벤트에 따른 처리기법에 대한 내용이 포함되었으면 좋겠습니다. 감사합니다.

    (에코서버에서 데이터 수신 이후에 같은 데이터를 전송할 때, 해당소켓에 wirte 이벤트 감시를 등록하고, epoll_wait를 이용하여 write 이벤트를 감시(바로 wirte 가능이라고 리턴함. 송신버퍼는 비어있을 가능성이 매우크기때문.)하고, epoll이 write 이벤트가 있다고 리턴하면 그때에 send()를 호출하여 전송해야 합니다. 소켓에서 read 할 때, write 할때 모두 철저하게 이벤트 발생에 의존하여 처리해야하는 것이지요.)

    • xevious7 2007/06/04 09:37 MODIFY/DELETE

      안녕하세요
      지적하신대로 내용은 깊은 내용이 못되고 있습니다.
      여기서는 대략적인 뼈대구조와 사용법만을
      목적으로 하였습니다.~
      그러고 보니 제목이 잘못되었군요.
      epoll 서버 프로그래밍라는 거창한 제목으로
      되어있군요. epoll 샘플 프로그래밍 정도 되어야
      되는데 말입니다.~

      감사합니다. :)

    • xevious7 2007/06/04 09:54 MODIFY/DELETE

      장성재님이 지적하신 자세한 부분들은
      윗글에서 말했듯이 많은 epoll 서버 공개소스에서
      볼 수 있습니다.

      언어를 배운다고 바로 소설을 쓸 수 없는것처럼
      말씀하신대로 epoll 은 하나의 도구일 뿐입니다.

      그 도구를 어떻게 이용할것인가는 각양각색이겠지요.

      여기의 소스는 accpet에서의 read에 대한처리만
      하고 있습니다.
      ()에 말한대로 할 수도 있지만 서버의 특성에 따라
      또는 구현에 따라 달라질 수도 있는 부분이 ()부분입니다.
      또한 read , write할때 모두 철저하게 이벤트발생에
      의존해야 처리해야 한다는 것도 , 항상 그렇게 해야된다고
      말할 수 없습니다. 모두 철처하게 라는 것은
      조금 틀린 말씀이라 말하고 싶습니다.

      이 서버의 경우 () 설명하신대로 event 처리 루틴을
      넣으나 안넣으나 같은 동작을 하게 됩니다.
      () 설명한 부분을 굳이 넣을 필요가 없는 것이죠.

      또한 실제 서버의 구현에서도 필요한 경우만 해주면됩니다.
      모두 철저하게 할필요는 없다고 생각합니다.

      처음엔 단순한 감사메시지로 생각했는데 읽어보니
      아쉬움및 소스에 대한 의견피력이시군요. =0=

      소스를 보시면 알겠지만 just sample이며
      read와 write부분에는 주석으로 이부분은 정밀한
      제어하 필요하다라고 밝혀드렸습니다.

  3. 장성재 2008/03/22 14:41 MODIFY/DELETE REPLY

    오랜만에 다시 오네요.
    제가 올린 ()부분의 핵심은 epoll의 사용법은 아니고요..
    non-blocking 소켓에서 read/write의 기본적인 개념때문입니다.
    non-blocking 소켓에서 write()를 수행할 경우에 write 하고자 하는 length만큼
    write가 되지 않을 수 있음을 알아야 합니다.
    예를 들어, 100바이트를 보내기 위해 write() 함수를 호출했을 때, 100바이트를 다 보내지 못하고 리턴됩니다. 리턴될 때, 보낸 length를 반드시 체크해야 합니다. 아마도 대부분의 경우에 100이 리턴되겠지요. 하지만, 네트웍이 바쁘거나 상태가 좋지 않거나 접속이 많은 경우에는 100보다 작은 숫자가 리턴됩니다. 원하는 바이트길이만큼 보내지 못한 것이지요. 이럴때에는 어떻게 해야할까요? 다시 write()를 바로 호출해야할까요? 그렇게 처리할 수도 있지만 바로 직전에 왜 100바이트를 모두 전송하지 못했는지를 이해한다면 그렇게 처리하는 것이 바람직하지 않습니다. 송신버퍼에 100바이트를 채울 여유가 없기때문에 100보다 작은 숫자가 리턴되는 것이기때문에 바로 다시 write()를 호출한다고해서 남은 바이트가 바로 전송된다는 보장이 없습니다.
    write()처리를 이벤트를 통해서 해야하는 이유가 여기에 있습니다. 100바이트를 모두 보내지 못했을 때, 그 소켓의 writable를 계속 체크해서 그 이벤트가 오면(송신버퍼에 빈공간이 생기면) 그 시점에 write()를 호출하면 되는 것이죠.
    따라서, non-blocking의 read/wirte는 소켓 이벤트의 결과에 따라 일괄적으로 처리를 해주어야 합니다.

    • xevious7 2008/03/22 17:38 MODIFY/DELETE

      댓글감사합니다.

      앞서 댓글에 말씀드린데로 ,
      read / write 부분 즉 send , recv부분에는
      주석으로 정밀한 제어가 필요하다고 밝혀드렸습니다.

      그 정밀한 제어라는 것은 성재님이 말한 부분과
      같은 확인처리 같은 것이겠지요.

      또 한마디 더 말씀드리면 , 글을 제대로 읽으셨는지
      모르시겠지만 , 이 epoll 샘플 서버는
      non-blocking sokcet이 아닙니다.

      blocking socket이죠.

    • xevious7 2008/03/22 17:53 MODIFY/DELETE

      사용법 정도의 epoll 샘플서버입니다.
      처음 접하는 사람들에게 작은 가이드정도의 문서입니다.

      좀더 추상화가 되고 좀더 여러가지 기법 proactor나
      reactor 같은 방편 이나 이런것을 보시려면
      ACE같은 공개 서버라이브러리 참조하시면
      바로 훌륭한 소스를 얻을 수 있을 것입니다.

      성재님이 댓글로 달아두신 non-blocking 소켓에서의
      기본적인 개념은 여기 찾아오시는 ,네트웍 프로그래밍
      의 경험이 없으신 분들을 위해서 도움이 되리라
      생각합니다.



    • xevious7 2008/03/22 17:59 MODIFY/DELETE

      추가적으로 성재님이 말씀하신 non-blocking
      모드까지 포함하고 LT모드 뿐만 아니라
      ET모드까지 구현한 epoll sample 소스가
      청아님 홈피에도 공개되어있습니다.

      ACE같은 라이브러리는 너무 방대하기때문에
      초보자가 처음에 접하기는 너무 어려문면이
      있으니 쉽게 접하려면 그런 문서들을 보시기 바랍니다.

      링크도 추가해놓았습니다.



    • xevious7 2008/03/22 18:41 MODIFY/DELETE

      지금가서 보니 ~청아님 소스도 굳이
      send시 보낸바이트수 같은 처리부분을 하지
      않으셨네요.

      하지만 성재님이 말씀하신
      그러한 부분들은 네트웍프로그래밍을 하는
      사람들이라면 다 알고 있는 사실입니다.

      청아님이나 저나 그것을 모르기때문에
      send에서 처리하는 부분을 넣지 않은것이 아닙니다.

      지적하는 부분은 무엇을 말하고자 하는 것은
      알겠지만 ,
      그러한 세세한 부분까지 다 넣는다면
      이미 샘플이 아닌게 되곘죠.

      물론 제가 전번댓글에서 꼭 이벤트처리를 해야된다는
      것은 아니다. 라는 글때문에 뻔한 send함수특성까지
      말씀하시면서 댓글을 달아주신것 같습니다.








  4. 장성재 2008/03/22 22:32 MODIFY/DELETE REPLY

    좋은 글 감사드립니다. ^^

  5. 장성재 2008/03/25 09:55 MODIFY/DELETE REPLY

    저도 epoll를 이용한 echo server 예제를 제작해 보았습니다.
    자세한 설명은....
    http://ilovelinux.org/sjang/79

    위에서 제가 설명한 부분의 실제 예제라고 보시면 됩니다.

    즐거운 나날 되세요~

    • xevious7 2008/03/25 11:33 MODIFY/DELETE

      포스트한 글이 많은 분에게 도움이 되겠네요.
      제글의 링크에 추가하겠습니다.

  6. 방문자 2011/12/29 17:49 MODIFY/DELETE REPLY

    좋은 글 감사드립니다.
    많은 공부가 되었네요

  7. 장성재 2012/07/24 08:29 MODIFY/DELETE REPLY

    epoll 예제가 아래 링크로 변경되었습니다.
    http://blog.ilovelinux.org/2009/10/epoll-echo-server.html

  8. 후불제 2018/05/16 05:27 MODIFY/DELETE REPLY

    정품 비아그라 구입 주소

    https://viamiles.com

    물건먼저 받고 입금하는 후불제 사이트 소개를합니다.

  9. 오랄 2018/06/11 04:24 MODIFY/DELETE REPLY

    재가자주 이용하는 야동사이트 입니다

    https://yasilhouse.com

    볼개 너무많튼대요

    즐거운시간보내새

Write a comment