메뉴 건너뛰기

라이온하트 2nd edition

홈페이지를 새롭게 리뉴얼합니다.

조회 수 226497 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄

원문 Boost network performance with libevent and libev



요약


현대식 서버 애플리케이션을 빌드하려면 수백, 수천, 심지어는 수만 개의 이벤트를 동시에 수용할 방법이 필요하며, 이때 이벤트들이 내부 요청이든 운영 문제를 효과적으로 처리하는 네트워크 연결이든 상관없습니다. 사용 가능한 솔루션이 많이 있지만, libevent 라이브러리와 libev 라이브러리는 둘 다 성능과 이벤트 처리 기능 면에서 대변혁을 일으켰습니다. 본 기사에서는 UNIX® 애플리케이션 내에서 이런 솔루션을 사용하고 배치하기 위해 적용할 수 있는 기본적인 구조체와 메소드를 살펴볼 것입니다. libev와 libevent는 둘 다 많은 수의 동시 클라이언트 또는 연산을 지원할 필요가 있는 IBM Cloud 또는 Amazon EC2 환경 내에 배치된 애플리케이션을 포함한 고성능 애플리케이션에서 사용될 수 있습니다.



소개


수많은 서버 배치 작업(특히, 웹 서버 배치 작업)에서 직면하는 최대의 문제점 중 하나는 많은 수의 연결을 처리할 수 있는 능력이 있느냐 하는 문제이다. 네트워크 트래픽을 처리하기 위한 클라우드 기반 서비스를 빌드하든, IBM Amazon EC 인스턴스에 애플리케이션을 분배하든, 웹 사이트용 고성능 컴포넌트를 제공하든 상관없이, 많은 수의 동시 연결을 처리할 수 있어야 한다.

최근에 이루어지고 있는 더욱 동적인 웹 애플리케이션, 특히 AJAX 기술을 사용하는 웹 애플리케이션으로의 움직임이 좋은 사례이다. 어떤 이벤트 또는 문제에 대한 라이브 모니터링 기능을 제공하는 시스템과 같이, 수천 개의 클라이언트가 한 웹 페이지 내에서 직접 정보를 업데이트하도록 허용하는 시스템을 배치하고 있는 경우에는 정보를 효과적으로 제공할 수 있는 속도가 매우 중요하다. 그리드 또는 클라우드 환경에서는 수천 개의 클라이언트로부터 동시에 계속 개방된 연결이 있을 수 있고, 각 클라이언트에 요청 및 응답 서비스를 제공할 수 있어야 한다.

libevent와 libev가 어떻게 다중 네트워크 연결을 처리할 수 있는지 살펴보기 전에, 이런 유형의 연결을 처리하기 위한 전통적인 솔루션 몇 가지를 간략히 살펴보자.



다중 클라이언트 처리


다중 연결을 처리하는 기존의 방법은 수도 없이 많을 정도이지만, 보통 그런 방법을 사용하면 메모리 또는 CPU를 너무 많이 사용하거나 어떤 형태의 운영 체제 한계에 이르기 때문에 많은 수의 연결을 처리하는 문제가 발생한다.

사용되는 기본 솔루션은 다음과 같다.

라운드 로빈: 초창기 시스템에서는 개방된 네트워크 연결 목록을 단순히 반복하면서 읽어야 할 데이터가 있는지 결정하는 라운드 로빈 선택이라는 간단한 솔루션을 사용한다. 이 솔루션은 느린데다가(특히, 연결 수가 증가할 때) 비효율적이다(현재 연결을 사용하는 동안 다른 연결에서 요청을 보내고 응답을 기다리고 있을 수 있으므로). 각 연결을 반복 수행하는 동안 다른 연결은 기다려야 한다. 100개의 연결이 있는데 한 연결에만 데이터가 있더라도, 서비스가 필요한 그 한 연결에 이르기 위해 다른 99개의 연결을 반복하여 작업해야 한다.

poll, epoll 및 variation: 이 솔루션에서는 네트워크 소켓에서 데이터가 식별될 때 처리 함수가 호출되도록 콜백 메커니즘으로 모니터할 각 연결의 배열을 유지하는 구조체를 이용하는 라운드 로빈 접근 방식을 수정한 방식을 사용한다. Poll의 문제점은 구조체의 크기가 꽤 클 수 있다는 점이고, 목록에 새 네트워크 연결을 추가할 때 구조체를 수정하면 로드가 증가하여 성능에 영향을 줄 수 있다.

select: select() 함수 호출에서는 정적 구조체를 사용하며, 이 구조체는 이전에 비교적 적은 수(1,024개의 연결)로 하드코드되었기 때문에 매우 큰 배치에는 비실용적이다.

선택한 OS에서 더 나은 성능을 발휘할 수 있는 개별 플랫폼에서 다른 구현 방법이 있지만(예: Solaris에 /dev/poll 구현 또는 FreeBSD/NetBSD에 kqueue 구현), 이런 구현은 이식 불가능하고 반드시 요청 처리의 상위 레벨 문제를 해결하는 것도 아니다.

위에서 언급한 솔루션들은 모두 요청을 기다렸다가 처리한 후, 실제 네트워크 상호 작용을 처리하기 위한 별개의 함수로 그 요청을 발송하는 단순한 루프를 사용한다. 여기서 핵심은 서로 다른 연결과 인터페이스를 수신, 업데이트 및 제어하려면 루프와 네트워크 소켓에 많은 관리 코드가 필요하다는 점이다.

서로 다른 많은 연결을 처리하는 다른 방법은 대부분의 현대식 커널에서 멀티스레딩 지원을 이용해 연결을 수신하여 처리함으로써 각 연결마다 새 스레드를 여는 것이다. 이 방법에서는 연결 처리의 책임이 다시 운영 체제로 직접 전환되지만, 각각의 스레드가 고유한 실행 공간을 필요로 할 것이므로 RAM과 CPU의 측면에서 비교적 큰 오버헤드가 발생할 것임을 알 수 있다. 각 스레드(따라서 네트워크 연결)가 사용 중인 경우에는 각 스레드로의 컨텍스트 전환이 중요해질 수 있다. 마지막으로, 많은 커널들이 그처럼 많은 수의 활성 스레드를 처리하도록 디자인되지 않았다.



libevent 접근 방식


libevent 라이브러리가 실제로는 select(), poll() 또는 다른 메커니즘의 기초를 대체하지는 못한다. 그 대신, libevent 라이브러리는 각 플랫폼에서 가장 효율적이고 고성능의 솔루션을 이용한 구현과 관련된 랩퍼를 제공한다.

각각의 요청을 실제로 처리하기 위해, libevent 라이브러리는 기본 네트워크 백엔드 주위에서 랩퍼 역할을 하는 이벤트 메커니즘을 제공한다. 이런 이벤트 시스템을 사용하면 기본 I/O 복잡성을 간소화하는 한편, 연결을 위한 핸들러를 매우 쉽고 간단하게 추가할 수 있다. 이것이 libevent 시스템의 핵심이다.

libevent 라이브러리의 추가 컴포넌트는 버퍼링된 이벤트 시스템(클라이언트와 주고받는 버퍼 데이터용)과 HTTP, DNS 및 RPC 시스템을 위한 코어 구현을 포함한 다양한 기능을 추가한다.

libevent 서버를 작성하는 기본적인 방법은 클라이언트에서의 연결을 승인하는 것과 같은 특정한 작업이 발생할 때 실행할 함수를 등록한 다음, 기본 이벤트인 loop event_dispatch()를 호출하는 것이다. 이제는 libevent 시스템에서 실행 프로세스의 제어를 처리한다. 이벤트와 이벤트를 호출할 함수를 등록한 후에는 이벤트 시스템이 자율적으로 운영되고, 애플리케이션 실행 중에 이벤트 큐에 이벤트를 추가(등록)하거나 이벤트 큐에서 이벤트를 제거(등록 취소)할 수 있다. 새로 개방된 연결을 처리하기 위해 새 이벤트를 추가할 수 있으므로, 유연한 네트워크 처리 시스템을 빌드할 수 있는 것은 바로 이런 이벤트 등록의 자유로움 덕분이다.

예를 들어, 새 연결을 열기 위해 accept() 함수를 호출해야 할 때마다 수신 소켓을 연 다음 콜백 함수를 등록하여 네트워크 서버를 작성할 수 있다. 이에 대한 기본 사항을 정리한 내용이 목록 1에 표시되어 있다.



목록 1. 새 연결을 열기 위해 accept() 함수를 호출해야 할 때마다 수신 소켓을 열고 콜백 함수를 등록하여 네트워크 서버 작성


int main(int argc, char **argv)
{
...
    ev_init();

    /* Setup listening socket */

    event_set(&ev_accept, listen_fd, EV_READ|EV_PERSIST, on_accept, NULL);
    event_add(&ev_accept, NULL);

    /* Start the event loop. */
    event_dispatch();
}



event_set() 함수는 새 이벤트 구조체를 작성하는 반면, event_add()는 이벤트 큐 메커니즘에 이벤트를 추가한다. 이번에는 event_dispatch()가 이벤트 큐 시스템을 시작하고 요청을 수신하여 승인하기 시작한다.

목록 2에 더 완전한 예제가 주어져 있으며, 여기서는 매우 간단한 에코 서버를 빌드한다.


목록 2. 간단한 에코 서버 빌드


#include <event.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define SERVER_PORT 8080
int debug = 0;

struct client {
  int fd;
  struct bufferevent *buf_ev;
};

int setnonblock(int fd)
{
  int flags;

  flags = fcntl(fd, F_GETFL);
  flags |= O_NONBLOCK;
  fcntl(fd, F_SETFL, flags);
}

void buf_read_callback(struct bufferevent *incoming,
                       void *arg)
{
  struct evbuffer *evreturn;
  char *req;

  req = evbuffer_readline(incoming->input);
  if (req == NULL)
    return;

  evreturn = evbuffer_new();
  evbuffer_add_printf(evreturn,"You said %sn",req);
  bufferevent_write_buffer(incoming,evreturn);
  evbuffer_free(evreturn);
  free(req);
}

void buf_write_callback(struct bufferevent *bev,
                        void *arg)
{
}

void buf_error_callback(struct bufferevent *bev,
                        short what,
                        void *arg)
{
  struct client *client = (struct client *)arg;
  bufferevent_free(client->buf_ev);
  close(client->fd);
  free(client);
}

void accept_callback(int fd,
                     short ev,
                     void *arg)
{
  int client_fd;
  struct sockaddr_in client_addr;
  socklen_t client_len = sizeof(client_addr);
  struct client *client;

  client_fd = accept(fd,
                     (struct sockaddr *)&client_addr,
                     &client_len);
  if (client_fd < 0)
    {
      warn("Client: accept() failed");
      return;
    }

  setnonblock(client_fd);

  client = calloc(1, sizeof(*client));
  if (client == NULL)
    err(1, "malloc failed");
  client->fd = client_fd;

  client->buf_ev = bufferevent_new(client_fd,
                                   buf_read_callback,
                                   buf_write_callback,
                                   buf_error_callback,
                                   client);

  bufferevent_enable(client->buf_ev, EV_READ);
}

int main(int argc,
         char **argv)
{
  int socketlisten;
  struct sockaddr_in addresslisten;
  struct event accept_event;
  int reuse = 1;

  event_init();

  socketlisten = socket(AF_INET, SOCK_STREAM, 0);

  if (socketlisten < 0)
    {
      fprintf(stderr,"Failed to create listen socket");
      return 1;
    }

  memset(&addresslisten, 0, sizeof(addresslisten));

  addresslisten.sin_family = AF_INET;
  addresslisten.sin_addr.s_addr = INADDR_ANY;
  addresslisten.sin_port = htons(SERVER_PORT);

  if (bind(socketlisten,
           (struct sockaddr *)&addresslisten,
           sizeof(addresslisten)) < 0)
    {
      fprintf(stderr,"Failed to bind");
      return 1;
    }

  if (listen(socketlisten, 5) < 0)
    {
      fprintf(stderr,"Failed to listen to socket");
      return 1;
    }

  setsockopt(socketlisten,
             SOL_SOCKET,
             SO_REUSEADDR,
             &reuse,
             sizeof(reuse));

  setnonblock(socketlisten);

  event_set(&accept_event,
            socketlisten,
            EV_READ|EV_PERSIST,
            accept_callback,
            NULL);

  event_add(&accept_event,
            NULL);

  event_dispatch();

  close(socketlisten);

  return 0;
}


아래에서는 다른 함수와 연산에 대해 논한다.

main(): main 함수는 연결 수신에 사용할 소켓을 작성한 다음, 이벤트 핸들러를 통해 각각의 연결을 처리하기 위해 accept()에 대한 콜백을 작성한다.

accept_callback(): 연결이 승인될 때 이벤트 시스템에서 호출되는 함수이다. 이 함수는 클라이언트에 대한 연결을 승인하고, 클라이언트 소켓 정보 및 버퍼 이벤트 구조체를 추가하고, 클라이언트 소켓의 읽기/쓰기/오류 이벤트에 대한 콜백을 이벤트 구조체에 추가하고, (임베디드 이벤트 버퍼 및 클라이언트 소켓이 있는) 클라이언트 구조체를 인수로 전달한다. 해당 클라이언트 소켓에 읽기, 쓰기 또는 오류 연산이 있을 때마다, 해당 콜백 함수가 호출된다.

buf_read_callback(): 클라이언트 소켓에 읽을 데이터가 있을 때 호출되는 함수이다. 이 함수는 에코 서비스로서 클라이언트에 다시 "you said..."를 쓴다. 이 소켓은 새 요청을 승인하기 위해 열린 상태로 유지된다.

buf_write_callback(): 쓸 데이터가 있을 때 호출되는 함수이다. 이런 간단한 서비스는 필요하지 않으므로, 정의가 비어 있다.

buf_error_callback(): 오류 조건이 존재할 때 호출되는 함수이다. 이 함수에는 클라이언트가 연결을 끊는 시점이 포함된다. 클라이언트 소켓이 닫히는 모든 상황에서, 클라이언트 소켓용 이벤트 항목이 이벤트 목록에서 제거된다. 클라이언트 구조체용 메모리를 사용할 수 있게 된다.

setnonblock(): 네트워크 소켓을 비블로킹 I/O로 설정한다.

클라이언트 연결 수가 늘어나면 클라이언트 연결을 처리하기 위한 새 이벤트가 이벤트 큐에 추가되고, 클라이언트 연결이 끊길 때 제거된다. 막후에서는 libevent가 네트워크 소켓을 처리하고, 어떤 클라이언트에서 서비스를 받아야 할지 식별하고, 각각의 경우에 해당 함수를 호출하고 있다.

애플리케이션을 빌드하려면 libevent 라이브러리를 추가하는 C 소스 코드 $ gcc -o basic basic.c -levent를 컴파일한다.

클라이언트 관점에서, 서버는 서버로 전송되는 모든 텍스트를 그냥 다시 에코한다(아래의 목록 3 참조).


목록 3. 서버는 서버로 전송되는 텍스트를 다시 에코

 

$ telnet localhost 8080

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

Hello!

You said Hello!


이와 같은 네트워크 애플리케이션은 다중 연결을 사용할 필요가 있는 IBM Cloud 시스템과 같이 대규모로 분배된 배치 환경에서 유용할 수 있다.

이처럼 간단한 솔루션으로는 성능상의 이점과 많은 수의 동시 연결을 확인하기 어렵다. 임베디드 HTTP 구현을 사용하면 대량 확장성의 이해에 도움이 될 수 있다.


내장 HTTP 서버 사용

일반 네트워크 기반 libevent 인터페이스는 원시 애플리케이션을 빌드하려는 경우에 유용하지만, 정보를 로드하거나 더 공통적으로는 정보를 동적으로 다시 로드하는 웹 페이지 및 HTTP 프로토콜을 기반으로 애플리케이션을 개발하는 것이 점점 더 일반화되고 있다. AJAX 라이브러리를 사용 중인 경우, 리턴하는 정보가 XML 또는 JSON이더라도 다른 쪽 끝에서 HTTP를 예상한다.

libevent 내부의 HTTP 구현이 Apache의 HTTP 서버를 대체하지는 않겠지만, 클라우드 및 웹 환경 모두와 연관된 대규모 동적 컨텐츠에 실용적인 솔루션이 될 수 있다. 예를 들어, IBM Cloud 관리 또는 기타 솔루션에 libevent 기반 인터페이스를 배치할 수 있다. HTTP를 사용하여 통신할 수 있으므로, 서버가 다른 컴포넌트와 통합할 수 있다.

libevent 서비스를 사용하려면 기본 네트워크 이벤트 모델에 대해 이미 설명한 것과 같은 기본 구조체를 사용하지만, 네트워크 인터페이스를 처리하지 않아도 HTTP 랩퍼가 이를 자동으로 처리해준다. 따라서 전체 프로세스가 네 가지 함수 호출(초기화, HTTP 서버 시작, HTTP 콜백 함수 설정 및 이벤트 루프 진입)에다가 데이터를 되돌려 보내는 콜백 함수의 내용으로 전환된다. 목록 4에서 매우 간단한 예제를 제시한다.


목록 4. libevent 서비스를 이용한 간단한 예제

 

#include <sys/types.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <event.h>
#include <evhttp.h>

void generic_request_handler(struct evhttp_request *req, void *arg)
{
  struct evbuffer *returnbuffer = evbuffer_new();

  evbuffer_add_printf(returnbuffer, "Thanks for the request!");
  evhttp_send_reply(req, HTTP_OK, "Client", returnbuffer);
  evbuffer_free(returnbuffer);
  return;
}

int main(int argc, char **argv)
{
  short          http_port = 8081;
  char          *http_addr = "192.168.0.22";
  struct evhttp *http_server = NULL;

  event_init();
  http_server = evhttp_start(http_addr, http_port);
  evhttp_set_gencb(http_server, generic_request_handler, NULL);

  fprintf(stderr, "Server started on port %dn", http_port);
  event_dispatch();

  return(0);
}



이전 예제와 비교해 볼 때, 이 예제 코드의 기본적인 내용은 상대적으로 자명하다. 이 코드의 기본 요소는 HTTP 요청을 수신할 때 사용할 콜백 함수를 설정하는 evhttp_set_gencb() 함수와 응답 버퍼를 간단한 성공 메시지로 채우는 generic_request_handler() 콜백 함수 자체이다.

HTTP 랩퍼는 여러 가지 다양한 기능을 제공한다. 예를 들어, (CGI 요청에서 사용하는 것과 같은) 일반적인 요청에서 쿼리 인수를 추출하게 될 요청 구문 분석기가 있고, 요청되는 다른 경로 내에서 트리거되도록 다른 핸들러를 설정할 수도 있다. 적절히 다른 콜백 및 처리 함수와 함께, '/db/' 경로 또는 '/memc'로서 memcached를 통한 인터페이스를 사용하여 데이터베이스에 대한 인터페이스를 제공할 수 있다.

libevent 툴킷의 다른 요소 하나는 일반 타이머에 대한 지원이다. 이런 지원을 이용해 특정 주기 이후에 이벤트를 예약할 수 있다. HTTP 구현과 이 요소를 결합하면 파일 내용 수정 시 리턴되는 데이터를 업데이트하도록 파일 내용을 준비하기 위한 경량 서비스를 제공할 수 있다. 예를 들어, 뉴스 이벤트가 쉴 새 없이 쏟아지는 중에 프론트엔드 웹 애플리케이션이 주기적으로 계속 뉴스 항목을 다시 로드하는 라이브 업데이트 서비스를 제공하고 있다면, 뉴스 컨텐츠를 손쉽게 준비할 수 있을 것이다. 전체 애플리케이션과 웹 서비스가 메모리에 있으므로 응답 시간이 매우 빨라진다.

이것이 목록 5의 예제 이면에 숨어 있는 주 목적이다.


목록 5. 일반 타이머를 사용하여 뉴스 이벤트가 쉴 새 없이 쏟아지는 중에 라이브 업데이트 서비스 제공

 

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <event.h>
#include <evhttp.h>

#define RELOAD_TIMEOUT 5
#define DEFAULT_FILE "sample.html"

char *filedata;
time_t lasttime = 0;
char filename[80];
int counter = 0;

void read_file()
{
  int size = 0;
  char *data;
  struct stat buf;

  stat(filename,&buf);

  if (buf.st_mtime > lasttime)
    {
      if (counter++)
        fprintf(stderr,"Reloading file: %s",filename);
      else
        fprintf(stderr,"Loading file: %s",filename);

      FILE *f = fopen(filename, "rb");
      if (f == NULL)
        {
          fprintf(stderr,"Couldn't open filen");
          exit(1);
        }

      fseek(f, 0, SEEK_END);
      size = ftell(f);
      fseek(f, 0, SEEK_SET);
      data = (char *)malloc(size+1);
      fread(data, sizeof(char), size, f);
      filedata = (char *)malloc(size+1);
      strcpy(filedata,data);
      fclose(f);


      fprintf(stderr," (%d bytes)n",size);
      lasttime = buf.st_mtime;
    }
}

void load_file()
{
  struct event *loadfile_event;
  struct timeval tv;

  read_file();

  tv.tv_sec = RELOAD_TIMEOUT;
  tv.tv_usec = 0;

  loadfile_event = malloc(sizeof(struct event));

  evtimer_set(loadfile_event,
              load_file,
              loadfile_event);

  evtimer_add(loadfile_event,
              &tv);
}

void generic_request_handler(struct evhttp_request *req, void *arg)
{
  struct evbuffer *evb = evbuffer_new();

  evbuffer_add_printf(evb, "%s",filedata);
  evhttp_send_reply(req, HTTP_OK, "Client", evb);
  evbuffer_free(evb);
}

int main(int argc, char *argv[])
{
  short          http_port = 8081;
  char          *http_addr = "192.168.0.22";
  struct evhttp *http_server = NULL;

  if (argc > 1)
    {
      strcpy(filename,argv[1]);
      printf("Using %sn",filename);
    }
  else
    {
      strcpy(filename,DEFAULT_FILE);
    }

  event_init();

  load_file();

  http_server = evhttp_start(http_addr, http_port);
  evhttp_set_gencb(http_server, generic_request_handler, NULL);

  fprintf(stderr, "Server started on port %dn", http_port);
  event_dispatch();
}



버의 기본 작동 원리는 이전 예제와 동일하다. 우선, 이 스크립트는 기본 URL 호스트/포트 조합(요청 URI를 처리 안 함)에 대한 요청에 바로 응답할 HTTP 서버를 설정한다. 파일을 로드하는 것이 첫 단계이다(read_file()). 이와 동일한 함수가 원본 로드에 사용되고 타이머 이벤트에 의한 콜백 중에도 사용될 것이다.

read_file() 함수에서는 stat() 함수 호출을 사용하여 파일 수정 시간을 검사하는데, 파일이 마지막으로 로드된 이후로 파일이 변경된 경우에만 파일 내용을 다시 읽는다. 이 함수는 데이터를 별개의 구조체로 복사하는 fread()에 대한 단일 호출을 사용하여 파일 데이터를 로드한 후, strcpy()를 사용하여 로드된 문자열에서 글로벌 문자열로 데이터를 이동한다.

load_file() 함수는 타이머가 트리거될 때 함수 역할을 하게 된다. 이 함수는 read_file()을 호출하여 파일 내용을 로드하고, 파일 로드 시도가 이루어지기 전에 RELOAD_TIMEOUT 값을 초의 값으로 사용하여 타이머를 설정한다. libevent 타이머는 초와 마이크로초 단위로 모두 타이머를 지정할 수 있게 해주는 timeval 구조체를 사용한다. 타이머는 연속적이지 않다. 타이머 이벤트가 트리거될 때 타이머를 설정한 다음, 이벤트 큐에서 이벤트가 제거된다.

컴파일하려면 이전 예제와 같은 형식인 $ gcc -o basichttpfile basichttpfile.c -levent를 사용한다.

이제 데이터로 사용할 정적 파일을 작성한다. 기본 파일은 sample.html이지만, 어떤 파일이든 명령행의 첫 인수로 지정할 수 있다(아래의 목록 6 참조).


목록 6. 데이터로 사용할 정적 파일 작성

 

$ ./basichttpfile

Loading file: sample.html (8046 bytes)

Server started on port 8081


현재, 이 프로그램은 요청을 수락할 준비가 되어 있지만, 다시 로드하기 위한 타이머도 작동 중이다. sample.html의 내용을 변경하면 로그에 기록된 메시지로 파일을 자동으로 다시 로드해야 한다. 예를 들어, 목록 7의 출력 결과에 최초 로드와 두 번 다시 로드한 데이터가 표시된다.

목록 7. 최초 로드 및 두 번 다시 로드한 내용을 보여주는 출력 결과
 
$ ./basichttpfile
Loading file: sample.html (8046 bytes)
Server started on port 8081
Reloading file: sample.html (8047 bytes)
Reloading file: sample.html (8048 bytes)

 
모든 이점을 누리려면 사용자 환경에서 열린 파일 디스크립터 수에 대한 ulimit가 없도록 해야 한다. ulimit 명령을 이용해 (적절한 사용 권한 또는 루트 액세스 권한으로) 이것을 변경할 수 있다. 정확한 설정은 사용하는 OS에 따라 다르겠지만, Linux®에서는 -n 옵션으로 열린 파일 디스크립터 수와 네트워크 소켓 수를 설정할 수 있다.

목록 8. -n 옵션을 사용하여 열린 파일 디스크립터 수 설정
 
$ ulimit -n
1024

 
한계를 늘리려면 숫자를 $ ulimit -n 20000과 같이 지정한다.
서버의 성능을 확인하려면 Apache Bench 2(ab2)와 같은 벤치마킹 애플리케이션을 사용하면 된다. 총 요청 수뿐 아니라, 동시 쿼리 수도 지정할 수 있다. 예를 들어, 100,000개의 요청을 사용하여 벤치마크를 실행한다면, $ ab2 -n 100000 -c 1000 http://192.168.0.22:8081/과 같이 그 중에서 1,000개를 동시 쿼리로 지정할 수 있다.
필자는 이 샘플 시스템을 실행하면서 서버 샘플에 표시된 8K 파일을 사용하여 초당 거의 11,000개의 요청 수를 기록했다. libevent 서버는 단일 스레드에서 실행 중이고, 요청 열기 방법으로도 제한될 것이므로 단일 클라이언트가 서버에 무리를 주지는 않을 것이라는 점을 유념하자. 비록 그렇다 하더라도, 그 정도 속도는 교환 중인 문서의 상대적 크기를 고려할 때 단일 스레드 애플리케이션으로는 인상적인 수준이다.

다른 언어 구현
수많은 시스템 애플리케이션에 C가 실용적인 언어이긴 하지만, 스크립팅 언어가 더욱 유연하고 실용적으로 사용될 수 있는 현대적 환경에서는 C가 종종 사용되지 않는다. 다행히도, Perl 및 PHP와 같은 대부분의 스크립팅 언어는 기본적으로 C로 작성되므로, 확장 모듈을 통해 libevent와 같은 C 라이브러리를 사용할 수 있다.
예를 들어, 목록 9에서는 Perl 네트워크 서버용 스크립트의 기본 구조를 보여준다. accept_callback() 함수는 목록 1의 코어 libevent 예제에서 accept 함수와 동일할 것이다.

목록 9. Perl 네트워크 서버용 스크립트의 기본 구조
 
my $server = IO::Socket::INET->new(
    LocalAddr       => 'localhost',
    LocalPort       => 8081,
    Proto           => 'tcp',
    ReuseAddr       => SO_REUSEADDR,
    Listen          => 1,
    Blocking        => 0,
    ) or die $@;

my $accept = event_new($server, EV_READ|EV_PERSIST, &accept_callback);

$main->add;

event_mainloop();

 
이런 언어로 작성되는 라이브러리 구현에서는 libevent 시스템의 코어를 지원하는 경향이 있고, HTTP 랩퍼를 항상 지원하지는 않는다. 따라서 스크립트로 작성된 애플리케이션을 이용한 이런 솔루션을 사용하면 더 복잡해진다. C 기반 libevent 애플리케이션에 언어를 임베드하거나 스크립트 언어 환경을 기반으로 한 수많은 HTTP 구현 중 하나를 사용하는 두 가지 경로가 있다. 예를 들어, Python에는 매우 뛰어난 기능을 가진 HTTP 서버 클래스(httplib/httplib2)가 있다.
이런 기능이 있음에도, C로 다시 구현할 수 없는 스크립팅 언어에는 아무 것도 없다는 점을 지적해야 할 것이다. 하지만, 시간을 고려해야 하므로 기존 코드베이스와의 통합이 더 중요해질 수 있다.

libev 라이브러리
libevent와 마찬가지로, libev 시스템은 이벤트 기반 루프를 제공하기 위해 poll(), select() 등의 기본 구현을 바탕으로 하는 이벤트 루프 기반 시스템이다. 필자가 본 기사를 작성하는 시점에서는 libev 구현의 오버헤드가 낮아 벤치마크 성능이 더 높았다. libev API가 더 원시적 형태로서, HTTP 랩퍼는 없지만 구현에 내장된 더 많은 유형의 이벤트를 지원한다. 예를 들어, 목록 4의 HTTP 파일 솔루션에서 사용할 수 있었던 여러 파일에 대한 속성 변화를 모니터하는 데 사용할 수 있는 evstat 구현이 있다.
하지만, 기본적인 사항은 동일하다. 즉, 필요한 네트워크 수신 소켓을 작성하고, 실행 중에 호출할 이벤트를 등록한 다음, 프로세스의 나머지 부분을 처리하는 libev를 포함한 기본 이벤트 루프를 시작하는 것은 똑같다.
예를 들어, Ruby 인터페이스를 사용하면 목록 10에 표시된 첫 번째 코드 목록에 있는 것과 유사한 에코 서버를 제공할 수 있다.

목록 10. Ruby 인터페이스를 사용하여 에코 서버 제공
 
require 'rubygems'
require 'rev'

PORT = 8081

class EchoServerConnection < Rev::TCPSocket
  def on_read(data)
    write 'You said: ' + data
  end
end

server = Rev::TCPServer.new('192.168.0.22', PORT, EchoServerConnection)
server.attach(Rev::Loop.default)

puts "Listening on localhost:#{PORT}"
Rev::Loop.default.run

 
Ruby 구현은 HTTP 클라이언트, OpenSSL 및 DNS를 포함한 수많은 공통 네트워크 솔루션을 위해 랩퍼가 제공되었으므로 특히 훌륭하다. 다른 스크립트 언어로는 포괄적인 Perl 및 Python 구현이 포함되며, 아마 사용해보고 싶을지 모르겠다.

요약
libevent와 libev는 모두 서버 쪽 또는 클라이언트 쪽 요청에 응답하기 위해 대용량 네트워크와 기타 I/O를 지원하는 유연하면서도 강력한 환경을 제공한다. (CPU/RAM 사용률이 낮은) 효율적인 형식으로 수천, 수만의 연결을 지원하는 것이 목표다. 본 기사에서는 IBM Cloud, EC2 또는 AJAX 기반 웹 애플리케이션 지원에 사용 가능한 libevent의 내장 HTTP 서비스를 포함하여, 이에 대한 여러 가지 예제를 살펴보았다.




List of Articles
번호 분류 제목 글쓴이 날짜 조회 수
225 Information jwplayer 6.6 embed source code LionHeart 2013.10.18 80731
224 금융개발 대신증권 Cybos Plus 사용에 관한 도움말 file LionHeart 2013.10.19 101811
223 C# C# Access 데이터베이스, 테이블, 칼럼 생성 방법 LionHeart 2013.10.21 86116
222 OS CENTOS에 memcached 설치방법 1 LionHeart 2013.10.23 111525
» C# libevent 및 libev로 네트워크 성능 향상 LionHeart 2013.10.23 226497
220 Information network simulator 3 (ns-3) overview LionHeart 2013.10.24 185540
219 Information Zend Guard 3.3.0 x64 file LionHeart 2013.10.24 96021
218 Information 우분투에 ns 2.35 설치 LionHeart 2013.10.24 91103
217 Android [안드로이드] 단말기 정보 (계정, device, 고유 정보값) 가져오기 badung007 2013.10.25 128290
216 Android [안드로이드] 인터넷 연결 상태 확인 badung007 2013.10.25 76571
215 Android [유니티3D엔진] 안드로이드 유니티 연동_JNI badung007 2013.10.26 191235
214 Information ns2 version 2.35 install LionHeart 2013.10.26 90874
213 OS CentOS 6에서 구글 크롬(Chrome) 사용하는 법 LionHeart 2013.10.26 119018
212 C# [Visual Studio] 비주얼 스튜디오 코딩 화면 셋팅 file badung007 2013.10.26 81168
211 Android [안드로이드] android:launchMode, singleTop과 singleTask의 차이 badung007 2013.10.26 98632
210 OS gFTP 2.0.18 FTP client for RHEL 5, 6 32bit, 64bit file LionHeart 2013.10.27 108528
209 Android 안드로이드 개발환경의 이해 LionHeart 2013.10.27 110500
208 Computer Centos 6 32bit에 Skype 설치 1 LionHeart 2013.10.27 143251
207 Android [안드로이드] ProgressDialog 시간 처리 badung007 2013.10.27 101952
206 Android [유니티3D엔진] C# 싱글톤 구현 및 인스턴스 활용 badung007 2013.10.28 94442
Board Pagination ‹ Prev 1 2 3 4 5 6 7 8 9 10 ... 12 Next ›
/ 12