Libevent R6: Bufferevents: concepts and basics

From 탱이의 잡동사니
Revision as of 11:00, 3 February 2015 by Pchero (talk | contribs)
Jump to navigation Jump to search

Overview

원문은 이곳<ref>http://www.wangafu.net/~nickm/libevent-book/Ref6_bufferevent.html</ref>에서 확인할 수 있다.

때로는 이벤트의 응답을 기다리기보다, 버퍼의 일부분을 직접 확인할 수 있는 기능이 필요할 때가 있다. 예를 들어 우리가 데이터를 write 한다면 다음과 순서를 가질 수가 있다.

  • Decide that we want to write some data to a connection; put that data in a buffer.
  • Wait for the connection to become writable.
  • Write as much of the data as we can.
  • Remember how much we wrote, and if we still have more data to write, wait for the connection to become writable again.

This buffered IO pattern is common enough that Libevent provides a generic mechanism for it. A "bufferevent" consists of an underlying transport (like a socket), a read buffer, and a write buffer. Instead of regular events, which give callbacks when the underlying transport is ready to be read or written, a bufferevent invokes its user-supplied callbacks when it has read or written enough data.

There are multiple types of bufferevent that all share a common interface. As of this writing, the following types exist:

  • socket-based bufferevents
A bufferevent that sends and receives data from an underlying stream socket, using the event_* interface as its backend.
  • asynchronous-IO bufferevents
A bufferevent that uses the Windows IOCP interface to send and receive data to an underlying stream socket. (Windows only; experimental.)
  • filtering bufferevents
A bufferevent that processes incoming and outgoing data before passing it to an underlying bufferevent object—for example, to compress or translate data.
  • paired bufferevents
Two bufferevents that transmit data to one another.

현재 bufferevents 인터페이스는 스트림 기반 프로토콜(TCP 같은..)에서만 작동한다. UDP 같은 프로토콜은 후에 추가될 예정이다.

Bufferevents and evbuffers

모든 bufferevent는 input buffer 와 output buffer를 가진다. struct evbuffer 타입이다. write 하고자 하는 데이터가 있다면 output buffer에 입력하고, read 하고 싶은 데이터가 있다면 input buffer 로부터 읽어 들이면 된다.

Callbacks and watermarks

모든 bufferevent 는 data-related read/write 콜백을 가진다. 기본적으로, read 콜백은 어떤때든지 데이터를 read 할 때 호출되고, write 콜백은 데이터를 write 가능 할 때(output buffer 가 write 해도 충분할 정도로 비워졌을때) 호출된다. 이 함수들은 오버라이딩해서 "워터마크"를 읽기/쓰기 를 하도록 수정 가능하다.

모든 bufferevent 들은 4개의 "워터 마크"를 가진다.

  • Read low-water mark
여기에 설정된 레벨 이상의 데이터가 채워지면 해당 bufferevent 의 read 콜백 함수가 호출된다. 기본값은 0 이다. 따라서 어떤 경우에도 read 콜백이 호출된다.
  • Read high-water mark
If the bufferevent’s input buffer ever gets to this level, the bufferevent stops reading until enough data is drained from the input buffer to take us below it again. Defaults to unlimited, so that we never stop reading because of the size of the input buffer.
  • Write low-water mark
Whenever a write occurs that takes us to this level or below, we invoke the write callback. Defaults to 0, so that a write callback is not invoked unless the output buffer is emptied.
  • Write high-water mark
Not used by a bufferevent directly, this watermark can have special meaning when a bufferevent is used as the underlying transport of another bufferevent. See notes on filtering bufferevents below.

이뿐만이 아니라, bufferevent 는 non-data-oriented 이벤트를 위한 "에러" 혹은 "이벤트" 를 위한 콜백을 가지고 있다. 예를 들면 연결이 에러로 인해 종료가 되었을 때 사용된다. 다음의 Flag 들을 설정할 수 있다.

  • BEV_EVENT_READING
eventbuffer 에서 read 작업 중 발생하는 이벤트.
  • BEV_EVENT_WRITING
eventbuffer에서 write 작업 중 발생하는 이벤트.
  • BEV_EVENT_ERROR
eventbuffer 작업중 붕 발생하는 에러. 발생된 에러에 대한 더 자세한 정보를 확인하고 싶다면 EVUTIL_SOCKET_ERROR() 함수를 호출하면 된다.
  • BEV_EVENT_TIMEOUT
bufferevent 에서 timeout 발생
  • BEV_EVENT_EOF
bufferevent 에서 End-of-file 발생
  • BEV_EVENT_CONNECTED
bufferevent 에서의 연결 요청 완료 발생.

Deferred callbacks

기본적으로 bufferevent 콜백은 해당 조건이 충족되면, 그 즉시 호출된다.(이 부분은 evbuffer 역시 동일하다). 이런 즉각 호출은 해당 bufferevent의 의존성으로 인해 문제를 야기할 수 있다. 예를 들어 evbuffer A 로 데이터를 move 하는 콜백이 있다고 가정해보자. 그리고 evbuffer A가 비게되면 다른 콜백이 evbuffer A에 데이터를 입력하자고 해보자. 그렇게되면 모든 콜백들은 스택에 쌓이기 시작하여 마친되는 stack overflow 를 일으킬 것이다.

이를 해결하기 위해서 bufferevent(혹은 evbuffer)의 콜백이 뒤늦게 동작하도록 설정할 수 있다. 콜백이 즉시 호출 대신, 지연 호출되게 되면, 이벤트 루프에 쌓이게 되고, 후에 차례가 왔을 때, 보통의 이벤트 콜백 처럼 사용할 수 있다.

Option flags for bufferevents

bufferevent 생성시, 하나의 혹은 그 이상의 flag를 조합하여 동작 내용을 변경할 수 있다.

  • BEV_OPT_CLOSE_ON_FREE
When the bufferevent is freed, close the underlying transport. This will close an underlying socket, free an underlying bufferevent, etc.
  • BEV_OPT_THREADSAFE
Automatically allocate locks for the bufferevent, so that it’s safe to use from multiple threads.
  • BEV_OPT_DEFER_CALLBACKS
When this flag is set, the bufferevent defers all of its callbacks, as described above.
  • BEV_OPT_UNLOCK_CALLBACKS
By default, when the bufferevent is set up to be threadsafe, the bufferevent’s locks are held whenever the any user-provided callback is invoked. Setting this option makes Libevent release the bufferevent’s lock when it’s invoking your callbacks.

Working with socket-based bufferevents

가장 쉽게 bufferevents 를 사용하는 방법은 그냥 socket-based 타입으로 활용하는 것이다. socket-based 타입의 bufferevent 는 기반 네트워크 소켓이 read/write 명령을 받아들인 준비가 되었는지를 탐지하기 위해 Libevent 의 기반 이벤트 메커니즘을 이용하고, 데이터 송/수신을 위해 기반 네트워크 콜(readv, writev, WSASend, WSARecv)를 사용한다.

Creating a socket-based bufferevent

bufferevent_socket_new() 를 이용하면 socket-based bufferevent 를 생성할 수 있다.

  • Interface

<source lang=c> struct bufferevent *bufferevent_socket_new(

   struct event_base *base,
   evutil_socket_t fd,
   enum bufferevent_options options);

</source>

base 는 event_base, options 는 bufferevent option(BEV_OPT_CLOSE_ON_FREE etc) 의 비트마스트이다. fd 인자는 소켓 파일 디스크립터이더. 나중에 설정하고 싶다면 -1로 설정하면 된다.

bufferevent_socket_new 에서 사용할 socket 설정시, 반드시 non-blocking 모드로 설정해야 한다. non-blocking 모드 설정시, evutil_make_socket_nonblocking() 을 사용하면 된다.

성공시 bufferevent 를 리턴하고, 실패시 NULL 을 리턴한다.

Launching connections on socket-based bufferevents

만약 bufferevent socket이 connection 상태가 아니라면, 새롭게 연결을 설정할 수 있다.

  • Interface

<source lang=c> int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen); </source> address, addrlen 인자 값은 표준 함수 connect()에서 사용되는 것과 같다. 만약 아직 bufferevent에 socket 설정이 안되어 있는 상태라면 이 함수를 호출해서 새로운 socket 을 생성하고 nonblocking 설정까지 같이 해 줄 수 있다.

만약 bufferevent 에 이미 socket 을 설정한 상태라면, bufferevent_socket_connect() 함수를 호출해서 Libevent에 socket 이 아직 connected 상태가 아니고, connection 명령이 정상적으로 종료될 때 까지 read/write 해서는 안된다고 알려주어야 한다. 단, connection 이 종료되기 전, output buffer 에 데이터를 입력하는 것은 괜찬핟.

성공시 0, 실패시 -1을 리턴한다.

  • Example

<source lang=c>

  1. include <event2/event.h>
  2. include <event2/bufferevent.h>
  3. include <sys/socket.h>
  4. include <string.h>

void eventcb(struct bufferevent *bev, short events, void *ptr) {

   if(events & BEV_EVENT_CONNECTED) {
       /* We're connected to 127.0.0.1:8080. Ordinarily we'd do
        * something here, like start reading or writing. */
   } else if(events & BEV_EVENT_ERROR) {
       /* An error occured while connecting. */
   }

}

int main_loop(void) {

   struct event_base *base;
   struct bufferevent *bev;
   struct sockaddr_in sin;
   
   base = event_base_new();
   
   memset(&sin, 0, sizeof(sin));
   sin.sin_family = AF_INET;
   sin.sin_addr.s_addr = htonl(0x7f000001);    /* 127.0.0.1 */
   sin.sin_port = htons(8080); /* port 8080 */
   
   bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
   
   bufferevent_setcb(bev, NULL, NULL, eventcb, NULL);
   
   if(bufferevent_socket_connect(bev, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
       /* Error starting connection */
       bufferevent_free(bev);
       return -1;
   }
   
   event_base_dispatch(base);
   
   return 0;

} </source>

bufferevent_base_connect() 함수는 Libevent-2.0.2-alpha 버전 이후부터 사용가능하다. 그 이전버전이면, connect() 함수 호출 후 연결이 완료되면 bufferevent 에서 write 이벤트로 연결이 완료되었다고 알려준다.

bufferevent_socket_connect()로 connect 시도시, 오직 BEV_EVENT_CONNECTED 이벤트만 수신할 수 있음을 알아두자. connect() 함수 사용시, write 이벤트로 결과를 알려준다.

If you want to call connect() yourself, but still get receive a BEV_EVENT_CONNECTED event when the connection succeeds, call bufferevent_socket_connect(bev, NULL, 0) after connect() returns -1 with errno equal to EAGAIN or EINPROGRESS.

Launching connections by hostname

hostname 으로 접속해야 할 경우가 있다. 다음의 인터페이스를 사용하면 된다.

  • Interface

<source lang=c> int bufferevent_socket_connect_hostname(struct bufferevent *bev,

   struct evdns_base *dns_base, int family, const char *hostname,
   int port);

int bufferevent_socket_get_dns_error(struct bufferevent *bev); </source>

이 함수는 hostname 을 주어진 주소 타입 family 로 분석을 한다.(AF_INET, AF_INET6, AF_UNSPEC 을 지원한다.) 만약 주소 변환이 실패하면, 에러코드와 함께 이벤트 콜백을 호출한다. 성공시 bufferevent_connect() 로 연결을 시도한다.

dns_base 인자는 옵션값이다. NULL 로 설정되면, 주소 변환이 끝날때 까지, block 이 된다. 만약 다른 값들이 오게 되면 event_base 기반, 비 동기 방식으로 동작한다.

bufferevent_socket_connect() 함수는 현재 연결되어 있지 않은 모든 소켓 정보를 리턴한다.

에러 발생시, 대부분은 DNS looup error 를 리턴한다. 가장 최근의 에러를 확인하고 싶다면 bufferevent_socket_get_dns_error() 를 호출하면 된다. 리턴값이 0이라면 현재 아무런 에러가 없다는 뜻이다.

  • Example: Trival HTTP v0 client

<source lang=c> /* Don't actually copy this code: it is a poor way to implement an

* HTTP clint. Have a look at evhttp instead.
*/
  1. include <event2/dns.h>
  2. include <event2/bufferevent.h>
  3. include <event2/buffer.h>
  4. include <event2/util.h>
  5. include <event2/event.h>
  1. include <stdio.h>

void readcb(struct bufferevent *bev, void *ptr) {

   char buf[1024];
   int n;
   struct evbuffer *input = bufferevent_get_input(bev);
   while((n = evbuffer_remove(input, buf, sizeof(buf))) > 0) {
       fwrite(buf, 1, n, stdout);
   }

}

void eventcb(struct bufferevent *bev, short events, void *ptr) {

   if(events & BEV_EVENT_CONNECTED) {
       printf("Connect okay.\n");
   } else if(events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) {
       struct event_base *base = ptr;
       if(events & BEV_EVENT_ERROR) {
           int err = bufferevent_socket_get_dns_error(bev);
           if(err)
               printf("DNS error: %s\n", evutil_gai_strerror(err));
       }
       printf("Closing\n");
       bufferevent_free(bev);
       event_base_loopexit(base, NULL);
   }

}

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

   struct event_base *base;
   struct evdns_base *dns_base;
   struct bufferevent *bev;
   
   if(argc != 3) {
       printf("Trivial hTTP 0.x client\n"
           "Syntax: %s [hostname] [resource]\n"
           "Example: %s www.google.com /\n", argv[0], argv[0]);
       return 1;
   }
   
   base = event_base_new();
   dns_base = evdns_base_new(base, 1);
   
   bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
   bufferevent_setcb(bev, readcb, NULL, eventcb, base);
   bufferevent_enable(bev, EV_READ|EV_WRITE);
   evbuffer_add_printf(bufferevent_get_output(bev), "GET %s\r\n", argv[2]);
   bufferevent_socket_connect_hostname(
       bev, dns_base, AF_UNSPEC, argv[1], 80);
   event_base_dispatch(base);
   return 0;

} </source>

Generic bufferevent operations

여기에서 소개하는 함수들은 다중 bufferevent 환경에서도 동작하는 함수들이다.

Freeing a bufferevent

  • Interface

<source lang=c> void bufferevent_free(struct bufferevent *bev); </source> 위 함수는 생성한 bufferevent를 해제할 때 사용하는 함수이다. Bufferevent 는 내부적으로 관리되는데, 만약 bufferevent 를 해제했을 때, 이미 pending 중인 콜백에서 사용중이라면, 해당 콜백이 종료될 때 까지 실제로 삭제되지 않는다. 즉, 즉시 삭제하지는 않지만, 최대한 빨리 삭제하려고 한다는 것이다. 또, 만약 bufferevent에 쓰기 대기중인 데이터가 있었다면, 해당 bufferevent가 해제되기 전까지는 flush 되지 않는다. 만약 BEV_OPT_CLOSE_ON_FREE 플래그가 설정되었고, 소켓 혹은 기반 transport 와 연동되어있다면, bufferevent 해제시에 같이 close 된다.

Manipulating callbacks, watermarks, and enabled operations

  • Interface

<source lang=c> typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx); typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short events, void *ctx);

void bufferevent_setcb(struct bufferevent *bufev,

   bufferevent_data_cb readcb, bufferevent_data_cb writecb,
   bufferevent_event_cb eventcb, void *cbarg);

void bufferevent_getcb(struct bufferevent *bufev,

   bufferevent_data_cb *readcb_ptr,
   bufferevent_data_cb *write_prt,
   bufferevent_event_cb *eventcb_ptr,
   void **cbarg_ptr);

</source> bufferevent_setcb() 함수는 하나 이상의 bufferevent callback 함수를 변경할 수 있다. reacb, writecb, eventcb 함수들은 각각 읽기, 쓰기, 이벤트 등이 발생하면 호출된다. 첫번째 인자 bufferevent 는 이벤트가 발생하는 bufferevent 를 가리킨다. 마지막 인자는 bufferevent_callcb 에서 사용되는 cbarg 인자를 가리키는데, 콜백함수에 특정 데이터를 넘겨주는 용도로 활용할 수 있다.

bufferevent_event_cb() 의 events 인자는 이벤트 플래그의 비트마스크를 의미하는데, 자세한 내용은 "callbacks and watermarks"를 참고하기 바란다.

또한, 콜백을 비활성화 할 수도 있는데, NULL 로 지정해주면 된다. 주의할 점은, 모든 콜백 함수들이 bufferevent의 cbarg 값을 공유한다는 것이다. 따라서 만약 이 값을 변경하게 되면 모든 콜백 함수에 영향을 주게 된다.


References

<references />