libevent 라이브러리

libevent 는 규모있는 네트워크 서버를 개발하기 위한 이벤트 통지(Event Notification) 라이브러리 입니다. libevent API는 파일 명세자(file descriptor)에 대해 특정 이벤트가 발생하거나, 타임 아웃에 도달했을 때, 콜백 함수를 실행시켜 주는 메커니즘을 제공합니다.

libevent가 지원하는 이벤트 및 멀티플렉서는 다음과 같습니다.

  • /dev/poll
  • kqueue
  • event ports
  • select
  • poll
  • epoll
  • real-time signals (experimental)

libevent는 멀티플랫폼을 지원합니다. 지원하는 플랫폼은 다음과 같습니다.

  • Linux
  • BSD
  • Mac OSX
  • Solaris
  • Windows

libevent는 멀티 쓰레드 어플리케이션에서도 사용할 수 있습니다.

libevent의 내부 이벤트 메커니즘은 공개된 API와 완벽하게 독립적입니다. 즉, 간단한 업데이트를 통해 libevent에 새로운 기능을 추가해도 응용 프로그램의 재구성이 필요 없습니다. 기본적으로 libevent를 사용하는 모든 프로그램은 <event.h> 헤더 파일을 포함해야 하고, 링커 옵션에 ‘-levent’ 를 추가해야 합니다. 다음은 libevent를 사용하는 일반적인 순서입니다.

  1. event_init () : libevent 라이브러리를 초기화 합니다.
  2. event_set () : event 구조체를 초기화 합니다.
  3. event_add () : 감시할 이벤트 목록에 추가합니다.
  4. event_dispatch () : 이벤트 루프

다음은 event2/event.h에 정의되어 있는 자료 구조 입니다.

  • struct event : 감시할 이벤트에 대한 자료 구조, glib의 GSource에 상응하는 자료 구조로 보입니다.
  • struct event_base : 이벤트의 베이스로, glib의 GMainContext에 상응하는 자료 구조로 보입니다.
  • struct bufferevent : 이벤트 콜백에 대한 추상화
  • struct evbuffer : 버퍼에 대한 추상화
  • struct event_watermark : 읽기/쓰기 콜백 호출의 기준 값
  • struct evkeyval : 키-값 쌍

libevent는 정규 이벤트 콜백에 대한 최상위 추상화를 제공하며, 이를 buffered event 라고 부릅니다. buffered event는 자동으로 입/출력 버퍼를 채우고, 소모하는 기능을 제공합니다. buffered event 관련 API는 다음과 같습니다. 자세한 내용은 event2/buffer.h 를 참고합니다.

  • bufferevent_new ()
  • bufferevent_enable()
  • bufferevent_disable()
  • bufferevent_read()
  • bufferevent_write()

libevent는 비동기 DNS 리졸버 (Asynchronous DNS resolution)를 지원하며, 자세한 사항은 event2/evdns.h 를 참고합니다.

libevent는 임베디드 환경에서도 HTTP 요청을 처리할 수 있는 매우 간단한 이벤트 드리븐 HTTP 서버 (Event-driven HTTP servers) 기능을 제공합니다. 자세한 사항은 event2/evhttp.h 를 참고합니다.

libevent 는 RPC 서버/클라이언트 생성을 위한 프레임워크를 제공합니다. 자세한 사항은 evrpc.h 를 참고합니다.

Posted in Development | Tagged , , | 1 Comment

libmicrohttpd 내장 웹서버 라이브러리

최근 프로젝트에서 내장 웹서버를 운영하기 위해 libmicrohttpd 라이브러리를 적용해 보았습니다. 물론, 임베디드 시스템에서 많이 사용하는 GoAhead 등과 같은 상용 라이브러리를 사용하거나, BusyBox 내장 웹서버, 또는 다른 많은 오픈소스 라이브러리를 사용할 수도 있지만, 라이센스도(LGPL 또는 eCos) 괜찮고, 성능과 API 구성이 단순하고 명쾌한 것 같아서 시도해 보았습니다. (물론 아직도 주변에는 라이센스 무시하고 상용 라이브러리 몰래 사용하는 곳이 많긴 하지만… :-) 참고로, 이 라이브러리는 XBMC 프로젝트를 들여다보다가 내부에서 사용하는 걸 우연하게 발견했습니다.

내장 웹서버 라이브러리를 사용하면 얻을 수 있는 가장 좋은 장점은 MJPEG, RTSP-over-HTTP 등과 같은 웹기반 스트리밍 서버를 만들때 매우 편리하다는 점입니다. 한 프로세스 안에서 웹서버 + 스트리밍 생성 루틴이 함께 동작하므로 메모리 복사 오버헤드가 줄어들고 프로그래밍 복잡도 역시 감소합니다. 또한 CGI 프로세스가 따로 동작할 필요가 없으므로 웹을 통해 설정을 변경해도 별도의 프로세스간 통신이 불필요합니다.

물론, 내장 웹서버 방식이 장점만 있는 건 아닙니다. 기존 스탠드얼론 웹서버가 알아서 자동으로 해주던 부분, 예를 들어 디렉토리 인덱스 파일 생성, 동시 접속 클라이언트 수 관리, 클라이언트 캐시를 위한 수정 시각 고려 등을 프로그래머가 직접 작성해야 합니다. 특히 libmicrohttpd 라이브리는 GoAhead 등과 같은 상용 라이브러리에 비하면 자동화 부분이 조금 부족합니다. 예를 들어 파일 시스템의 일반 파일에 대한 웹서비스 처리도 직접 만들어야 합니다. 물론, 대부분 필요한 기능은 예제 소스를 참고하면 어렵지 않게 구현할 수 있습니다. 하지만 제 생각에는, 웹서버 라이브러리가 너무 많은 기능을 자동화하지 않는게 오히려 더 맞다고 생각하는데, 왜냐하면 대부분 응용 프로그램에서 웹서버를 내장하는 경우는 특수한 용도(RESTful / SOAP)이거나 필요한 기능만 구현하기 위해서이기 때문에 나머지는 모두 개발자가 제어하는게 라이브러리의 제 역할이 아닌가 생각합니다.

Posted in Development | Tagged , , , | Comments Off

도메인 메일 호스트(MX) 주소 얻기

예를 들어 nobody@hades.net이라는 메일 주소의 서버는 hades.net인 것 같지만 실제로 메일을 호스팅하는 서버는 해당 도메인 서버에 질의해서 MX 레코드에 기록된 호스트를 찾아야 합니다. 그리고 이 작업을 위해 DNS 관련 프로토콜을 직접 구현하거나. djbdns 등과 같은 라이브러리를 이용합니다.

그런데, 요즘 기존 코드를 리팩토링하면서 가능한 오래된(?) 라이브러리에 대한 의존성을 없애고 있는데 위에서 설명한 작업을 하는 함수가 리눅스 기본 glibc 라이브러리가 당연히 제공하는 걸 알게 되어 잠시 허탈했습니다.

다음은 도메인 이름을 인수로 주면 해당 도메인의 MX 레코드, 즉 메일서버 호스트를 glibc API를 이용해 작성한 코드입니다.

#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
 
static char *
lookup_mx (const char *name)
{
  unsigned char response[NS_PACKETSZ];  /* big enough, right? */
  ns_msg        handle;
  int           ns_index;
  int           len;
 
  len = res_search (name, C_IN, T_MX, response, sizeof (response));
  if (len < 0)
    {
      /* failed to search MX records */
      return strdup (name);
    }
  if (ns_initparse (response, len, &handle) < 0)
    {
      /* failed to parse MX records for '%s'", name); */
      return strdup (name);
    }
  len = ns_msg_count (handle, ns_s_an);
  if (len <= 0)
    {
      /* no mx records */
      return strdup (name);
    }
  for (ns_index = 0; ns_index < len; ns_index++)
    {
      ns_rr rr;
      char  dispbuf[4096];
 
      if (ns_parserr (&handle, ns_s_an, ns_index, &rr))
        {
          /* WARN: ns_parserr failed */
          continue;
        }
      ns_sprintrr (&handle, &rr, NULL, NULL, dispbuf, sizeof (dispbuf));
      if (ns_rr_class (rr) == ns_c_in && ns_rr_type (rr) == ns_t_mx)
        {
          char mxname[MAXDNAME];
 
          dn_expand (ns_msg_base (handle),
                     ns_msg_base (handle) + ns_msg_size (handle),
                     ns_rr_rdata(rr) + NS_INT16SZ,
                     mxname,
                     sizeof (mxname));
          return strdup (mxname);
        }
    }
  return strdup (name);
}

관련 자료는 Stack Overflow에서 본 것 같기도 하고… 아무튼, 명색이 전문 리눅스 C 프로그래머로서 15년 넘게 버티고 있으면서도 아직까지도 기본 C 라이브러리가 제공하는 함수도 제대로 알지 못하는 스스로를 돌아보게 됩니다. :(

Posted in Development | Tagged , , | Comments Off

Ice 인터넷 통신 엔진

GStreamer 2011 컨퍼런스 발표 자료를 보다가 Ice 미들웨어라는 걸 알게 되었습니다. 참고로 제가 본 발표 자료에서는 원격 임베디드 장치와 PC 사이의 비디오 스트리밍 및 장치 제어에 사용하고 있습니다.

Ice(The Internet Communication Engine) 미들웨어는 쉽게 말해 간편하게 사용할 수 있는 통신 라이브러리인데, 윈도우, 리눅스, 솔라리스, 맥OS, iOS 등의 운영체제를 지원하면서 C++, Java, C#, Python, Ruby, PHP, Objective C 등의 언어에서 사용할 수 있습니다. 라이센스는 GPL과 더불어 상용 제품에 사용할 수 있는 라이센스를 동시에 적용합니다. 즉, 다양한 이기종간의 네트웍 통신을 가능하게 합니다.

지원하는 기능을 보면, 우선 가장 눈에 띄는 점이 방화벽(firewall) 넘어 자유롭게 피어(peer)간 TCP / UDP 통신을 지원한다는 점입니다. 요즘은 방화벽 대신 NAT, IP 공유기 등이 더 일반적이겠지만, UDP 홀 펀칭(hole punching)이나 인증, 필터링 등과 같은 복잡한 작업을 모두 대신해 준다는 점입니다. 더불어 동기 / 비동기 방식 RPC(Remote Procedure Call) 지원,  SSL을 통한 암호화 지원, 다중 서버 인스턴스 지원을 통한 결함 허용(fault tolerance)과 로드 밸런싱(load balancing) 지원, 마지막으로 (제가 직접 사용해보진 않아서 잘 모르지만) 성능과 확장성도 매우 뛰어나다고 합니다.

라이브러리를 개발한 ZeroC라는 회사는 이 라이브러리를 기반으로 다양한 상용 제품을 제공하는 것은 물론, 용역(?) 개발을 하면서 라이브러리가 실무 분야에서 검증된 것임을 강조하고 있습니다.(당연히 저는 이 회사와 아무 관계가 없습니다)

언제나 느끼는 거지만, 세상에는 내가 아는 것보다 모르는게 훨씬 더 많고, 뭐가 어디에 있는지 알고 있는게 점점 더 중요해지고 있는 것 같습니다.

Posted in Development | Tagged , , | Comments Off

GUDev 사용하기

이제는 리눅스 데스크탑 뿐 아니라 임베디드 시스템에서도 당연하게 사용하는 udev 시스템은 단순히 장치 파일을 자동으로 생성해 주는 역할 뿐 아니라 여러 핫플러그(hot-plug) 방식 장치를 감지하는데도 유용하게 사용됩니다. 비단 키보드, 마우스 같은 입력 장치 뿐 아니라 USB 플래시, SATA / IDE / SCSI 하드디스크, CD-RW 등과 같은 저장장치가 삽입되었거나 제거되었을 경우 쉽게 감지할 수 있게 도와줍니다.

이러한 udev 서브 시스템의 혜택을 개발자가 얻기 위해 많은 방법이 존재하지만, 이 글에서는 GLib 메인루프 기반으로 동작하는 GUDev 라이브러리를 이용하는 법을 설명합니다. GLib 라이브러리를 사용하지 않을 경우 직접 libudev 라이브러리를 사용해도 되지만, 기본 개념만 파악하면 쉽게 어떤 라이브러리를 사용해도 상관없기 때문에 인터페이스가 더 편하고 직관적인 GUDev 라이브러리를 사용합니다. 참고로, 이 글은 “libudev and Sysfs Tutorial” 글과 “gudev Vala bindings” 글을 참고했습니다.

먼저 라이브러리를 설치하려면 우분투에서는 libgudev-1.0-dev 패키지, 아치리눅스에서는 udev 패키지를 설치하면 됩니다. (여담이지만, 아치리눅스는 개발에 필요한 헤더파일과 라이브러리가 별도 패키지로 분리되어 있는 경우가 별로 없는 것 같습니다)

다음 소스 코드는 현재 시스템에 장착된 모든 블럭 장치(block)를 보여주고, 이후 USB 플래시가 삽입되거나 제거되었을때 이를 감지하여 표시하도록 한 소스 코드입니다.(gudev.c)

#include <gudev/gudev.h>
 
static void
print_device (GUdevDevice *device)
{
  const gchar * const *symlinks;
 
  g_print ("  subsystem       : %s\n"
           "  devtype         : %s\n"
           "  name            : %s\n"
           "  number          : %s\n"
           "  sysfs_path      : %s\n"
           "  driver          : %s\n"
           "  action          : %s\n"
           "  seqnum          : %lld\n"
           "  device_type     : %d\n"
           "  device_number   : %d\n"
           "  device_file     : %s\n"
           "\n",
           g_udev_device_get_subsystem (device),
           g_udev_device_get_devtype (device),
           g_udev_device_get_name (device),
           g_udev_device_get_number (device),
           g_udev_device_get_sysfs_path (device),
           g_udev_device_get_driver (device),
           g_udev_device_get_action (device),
           g_udev_device_get_seqnum (device),
           g_udev_device_get_device_type (device),
           g_udev_device_get_device_number (device),
           g_udev_device_get_device_file (device));
}
 
static void
uevented (GUdevClient *client,
          gchar       *action,
          GUdevDevice *device,
          gpointer     user_data)
{
  g_print ("[action:%s]\n", action);
  print_device (device);
}
 
static void
print_block_device (gpointer data,
                    gpointer user_data)
{
  GUdevDevice *device = data;
 
  print_device (device);
  g_object_unref (device);
}
 
static void
print_block_devices (GUdevClient *client)
{
  GList *devices;
 
  devices = g_udev_client_query_by_subsystem (client, "block");
  if (devices)
  {
    g_print ("[block devices]\n");
    g_list_foreach (devices, print_block_device, NULL);
    g_list_free (devices);
  }
}
 
int
main (int    argc,
      char **argv)
{
  const gchar *subsystems[4] =
    { "usb/usb_interface", "scsi/scsi_device", "block", NULL };
  GUdevClient *client;
  GMainLoop   *main_loop;
 
  g_type_init ();
  main_loop = g_main_loop_new (NULL, FALSE);
 
  client = g_udev_client_new (subsystems);
  g_signal_connect (client, "uevent", G_CALLBACK (uevented), NULL);
 
  print_block_devices (client);
 
  g_main_loop_run (main_loop);
  g_object_unref (client);
 
  return 0;
}

컴파일 하려면 다음과 같이 실행하면 됩니다.

$ gcc -o gudev gudev.c `pkg-config --cflags --libs gudev-1.0`

소스 코드를 간단하게 설명하면, 제일 먼저 g_udev_client_new() 함수를 이용해 GUdevClient 객체를 생성합니다. 이때 넘겨주는 인수는 변화를 감지하고 싶은 서브 시스템 목록인데, 여기서는 모든 블럭 장치와 USB, SCSI 서브 시스템을 지정했습니다.(SCSI는 실제로 모든 종류의 하드디스크를 의미하기도 합니다) 만일 NULL을 지정하면 변화 감지 기능을 사용하지 않고 그냥 질의(query) 계열 API만 사용할 수 있으며, 비어있는 목록을 넘겨주면 시스템의 모든 서브시스템의 장치 변화를 감지해서 시그널로 알려줍니다. 참고로 매뉴얼에는 클라이언트를 생성한 쓰레드의 메인루프를 사용하여 감지 루틴이 실행된다고 하니, 만일 별도 쓰레드에서 이 감지 작업을 수행하려면 쓰레드를 먼저 만들고 그 쓰레드 안에서 생성해야 합니다. 이 예제에서는 테스트를 위해 기본 메인 루프를 사용하고 있습니다.

직접 장치 목록을 질의(query)하거나 시그널이 발생했을 경우 넘겨받는 GUdevDevice 객체와 g_udev_device_get_*() 계열 API를 이용하면 장치의 세부 정보를 얻을 수 있습니다. 위 예제에서는 udev / sysfs 관련 속성 등은 출력하지 않고 있지만, 필요하다면 더 자세한 정보를 얻을 수 있습니다.

실행하면 대략 다음과 같이 출력됩니다. (당연히 실행 환경에 따라 결과가 다릅니다)

$ ./gudev
[block devices]
  subsystem       : block
  devtype         : disk
  name            : sda
  number          : (null)
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda
  driver          : (null)
  action          : (null)
  seqnum          : 0
  device_type     : 98
  device_number   : 2048
  device_file     : /dev/sda
 
  subsystem       : block
  devtype         : partition
  name            : sda1
  number          : 1
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda/sda1
  driver          : (null)
  action          : (null)
  seqnum          : 0
  device_type     : 98
  device_number   : 2049
  device_file     : /dev/sda1
 
  subsystem       : block
  devtype         : disk
  name            : sr0
  number          : 0
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1f.2/host5/target5:0:0/5:0:0:0/block/sr0
  driver          : (null)
  action          : (null)
  seqnum          : 0
  device_type     : 98
  device_number   : 2816
  device_file     : /dev/sr0

위 출력에서는 일반 디스크 장치(/dev/sda)와 디스크 파티션(/dev/sda1), DVD-RW 장치(/dev/sr0)가 있음을 보여줍니다. 여기서 만일 일반 USB 플래시 메모리를 삽입하면 다음과 같은 결과가 출력됩니다.

[action:add]
  subsystem       : usb
  devtype         : usb_interface
  name            : 2-3:1.0
  number          : 0
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0
  driver          : usb-storage
  action          : add
  seqnum          : 1934
  device_type     : 0
  device_number   : 0
  device_file     : (null)
 
[action:add]
  subsystem       : scsi
  devtype         : scsi_device
  name            : 17:0:0:0
  number          : 0
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0/host17/target17:0:0/17:0:0:0
  driver          : sd
  action          : add
  seqnum          : 1938
  device_type     : 0
  device_number   : 0
  device_file     : (null)
 
[action:change]
  subsystem       : scsi
  devtype         : scsi_device
  name            : 17:0:0:0
  number          : 0
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0/host17/target17:0:0/17:0:0:0
  driver          : sd
  action          : change
  seqnum          : 1944
  device_type     : 0
  device_number   : 0
  device_file     : (null)
 
[action:add]
  subsystem       : block
  devtype         : disk
  name            : sdc
  number          : (null)
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0/host17/target17:0:0/17:0:0:0/block/sdc
  driver          : (null)
  action          : add
  seqnum          : 1945
  device_type     : 98
  device_number   : 2080
  device_file     : /dev/sdc
 
[action:add]
  subsystem       : block
  devtype         : partition
  name            : sdc1
  number          : 1
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0/host17/target17:0:0/17:0:0:0/block/sdc/sdc1
  driver          : (null)
  action          : add
  seqnum          : 1946
  device_type     : 98
  device_number   : 2081
  device_file     : /dev/sdc1

위 예제 소스 코드에서 감시하기 위해 지정한 서브 시스템 모두의 변화를 보여주다보니 복잡해 보이지만, 결국 USB 플래시 메모리가 USB / SCSI / BLOCK 서브시스템에 모두 정상적으로 감지되는 걸 확인할 수 있습니다. 다시 장치를 제거하면 다음과 같이 출력됩니다.

[action:remove]
  subsystem       : block
  devtype         : disk
  name            : sdc
  number          : (null)
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0/host16/target16:0:0/16:0:0:0/block/sdc
  driver          : (null)
  action          : remove
  seqnum          : 1926
  device_type     : 0
  device_number   : 2080
  device_file     : /dev/sdc
 
[action:remove]
  subsystem       : scsi
  devtype         : scsi_device
  name            : 16:0:0:0
  number          : 0
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0/host16/target16:0:0/16:0:0:0
  driver          : (null)
  action          : remove
  seqnum          : 1927
  device_type     : 0
  device_number   : 0
  device_file     : (null)
 
[action:remove]
  subsystem       : usb
  devtype         : usb_interface
  name            : 2-3:1.0
  number          : 0
  sysfs_path      : /sys/devices/pci0000:00/0000:00:1d.7/usb2/2-3/2-3:1.0
  driver          : (null)
  action          : remove
  seqnum          : 1931
  device_type     : 0
  device_number   : 0
  device_file     : (null)

이 정보를 실제로 어떻게 활용할지는 이제 어플리케이션에게 달린 몫입니다.

Posted in Development | Tagged , , , | Comments Off