LibeventとAPRでイベント駆動型HTTPサーバを作成してみた

イベント駆動型処理フレームワークの定番?であるlibevent(An event notification library)とAPR(Apache ProtableRuntime)を使ってイベント駆動型HTTPサーバを書いてみた。既にこの手の簡易HTTPサーバは書き尽くされてる感があり何ら目新しさはなく、若干車輪の再開発的なものになってしまっているのは否めないがそこは気にしないでlibeventが提供するイベント駆動型HTTP処理機能(evhttp)のサンプルの1つとして読んでいただきたく。ちなみにAPRは主にメモリプールのために使っている。

ソースコード – vs_httpd.c

http://github.com/yokawasa/vs_httpd/
http://github.com/yokawasa/vs_httpd/blob/master/vs_httpd.c

イベント駆動HTTP処理基本コード main @ vs_httpd.c

struct evhttp *httpd;

/* event driven http */
event_init();
httpd = evhttp_start(g_config->svr_addr, g_config->svr_port);
evhttp_set_gencb(httpd, main_request_handler, NULL);
event_dispatch();
evhttp_free(httpd);
  • event_initでlibeventライブラリの初期化処理。event_base_new()でもOK
  • evhttp_start(address, port)でbindするアドレス、listenするポートを指定
  • evhttp_set_gencbでリクエストイベントが通知される度にコールされるコールバック関数(main_request_handler)を登録。
  • event_dispatchでイベントループを開始しイベント通知を開始
  • evhttp_freeで作成されたHTTPサーバリソースを開放
[evhttp_set_gencbの定義 @ evhttp.h]
/** Set a callback for all requests that are not caught by specific callbacks */
void evhttp_set_gencb( struct evhttp *,
              void (*)(struct evhttp_request *, void *), void *);

リクエスト処理用コールバック関数 main_request_handler @ vs_httpd.c

void main_request_handler(struct evhttp_request *r, void *args)
{
    apr_status_t rv;
    struct evbuffer *evbuf;
    const char *path, *mimetype;
    char *complemented_path, *extbuf, *filebuf;
    int filesize = 0;

    /* check reqeust type. currently only suppoert GET */
    if (r->type != EVHTTP_REQ_GET) {
        fprintf(stdout, "only support GET request \n");
        evhttp_send_error(r, HTTP_BADREQUEST, "only support GET request");
        return;
    }
    path = apr_psprintf(g_mem_pool, "%s%s",
                g_config->doc_root, evhttp_request_uri(r));
    if(g_config->verbose) {
        fprintf(stderr, "req uri=%s\n", evhttp_request_uri(r) );
        fprintf(stderr, "req path=%s\n", path );
    }
    /* file or dir existence check */
    if (!exists(path, &complemented_path, &filesize)) {
        evhttp_send_error(r, HTTP_NOTFOUND, "file not found");
        return;
    }
    /* file's extension check */
    mimetype = apr_pstrdup(g_mem_pool, DEFAULT_MIME_TYPE);
    extbuf = strrchr(complemented_path,'.');
    if (extbuf) {
        ++extbuf;
        mimetype = find_mime_type(extbuf);
    }
    /* file read */
    filebuf = apr_palloc(g_mem_pool, filesize + 1);
    apr_file_t *file = NULL;
    rv = apr_file_open(&file, complemented_path,
                       APR_READ|APR_BINARY, APR_OS_DEFAULT, g_mem_pool);
    if (rv != APR_SUCCESS) {
        evhttp_send_error(r, HTTP_SERVUNAVAIL, "failed to open file");
        return;
    }
    apr_size_t len = filesize;
    rv = apr_file_read(file, filebuf, &len);
    if (rv != APR_SUCCESS) {
        evhttp_send_error(r, HTTP_SERVUNAVAIL, "failed to read file");
        return;
    }
    apr_file_close(file);

   if(g_config->verbose) {
        fprintf(stderr, "res mimetype=%s\n", mimetype);
        fprintf(stderr, "res file size=%d\n", len);
        fprintf(stderr, "res file output=%s\n", filebuf);
    }

    evbuf = evbuffer_new();
    if (!evbuf) {
        fprintf(stderr, "failed to create response buffer\n");
        evhttp_send_error(r, HTTP_SERVUNAVAIL, "failed to create response buffer");
        return;
    }
    evhttp_add_header(r->output_headers, "Content-Type",mimetype);
    evhttp_add_header(r->output_headers, "Content-Length", apr_psprintf(g_mem_pool,"%d",filesize));
    evbuffer_add(evbuf, filebuf, len);
    evhttp_send_reply(r, HTTP_OK, "", evbuf);
    evbuffer_free(evbuf);
}
  • GETメソッド(EVHTTP_REQ_GET)のみ処理を行う
  • exists()でリクエストされたuri(evhttp_request_uri(r))がファイルまたはディレクトリとして存在するかをチェック
  • find_mime_typeでファイル拡張子に対応するMIME TYPEを取得。拡張子がなければデフォルトMIME TYPE application/ocet-stream。この部分の処理は600 行のCでCGIをサポートする軽量WEBサーバmattowsのコードを参考、流用。
  • APRのファイルI/Oハンドルライブラリでファイルの読み込み。画像等バイナリファイルのためにopen時にAPR_BINARYフラグを指定している
  • evhttp_add_headerでレスポンス用outputヘッダに”Content-Type”と”Content-Lenght”を指定
  • (struct evbuffe*)evbufに読み込んだファイルデータをボディデータとして書き込む
  • evhttp_send_replyでクライアントにレスポンス

APRメモリプールの利用 @ vs_httpd.c
これは本題からそれるが全体的な特徴としてメモリ管理はAPRのメモリプールを利用しているのでここで軽く説明する。

static apr_pool_t *g_mem_pool = NULL;
apr_pool_create(&g_mem_pool, NULL);
...
g_config = apr_pcalloc(g_mem_pool, sizeof(httpsvr_config));
path = apr_psprintf(g_mem_pool, "%s%s", g_config->doc_root, evhttp_request_uri(r));
mimetype = apr_pstrdup(g_mem_pool, DEFAULT_MIME_TYPE);
filebuf = apr_palloc(g_mem_pool, filesize + 1);
rv = apr_file_open(&file, complemented_path, APR_READ|APR_BINARY, APR_OS_DEFAULT, g_mem_pool);
...
apr_pool_destroy(g_mem_pool);
  • apr_pool_createでメモリプールの作成
  • apr_palloc、apr_psprintf, を使用して、メモリプールからメモリを確保します
  • apr_pool_destroyを使用して、メモリプールを破棄

ダウンロード、コンパイル、そしてテスト

git clone でvs_httpdのコードを取得(リポジトリ複製)する。

$ git clone git@github.com:yokawasa/vs_httpd.git

Initialized empty Git repository in /home/m/dev/github/t/vs_httpd/.git/
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (11/11), done.
remote: Total 11 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (11/11), 8.51 KiB, done.

libeventとaprヘッダへのインクルードや両ライブラリのリンクができるようにMakefileのパス調整を行う。もちろんlibeventとaprがインストール済みであることが前提。もしまだならばまずはlibeventとaprをインストールしましょ。

$ vi Makefile
CFLAGS = -I<libeventのincludeパス> -I<apacheのincludeパス -Wall -g
LIBS = -L<libeventのlibパス> -levent -L<apacheのlibパス> -lapr-1

パス変更後にmakeを実行。これでvs_httpdバイナリの出来上がり。vs_httpdの使い方は次のとおり。

$ ./vs_httpd -h
Usage: vs_httpd [-a address] [-p port] [-d documentroot]
               [-D] [-v] [-h]
Options:
  -a address      : define server address (default: "0.0.0.0")
  -p port         : define server port (default: 8080)
  -d documentroot : define document root (default: "./")  
  -D              : daemonize option 0-off,1-on (default: 0)
  -v              : verbose option 0-off,1-on (default: 0)
  -h              : list available command line options (this page)

使い方の説明にあるようにオプションを指定しない場合はデフォルトサーバアドレス”0.0.0.0″、ポート番号 8080、ドキュメントルート “./”、デーモンモードOFF、VerboseモードOFFで起動される。尚、デフォルトインデックスファイルをindex.htmlとしているので http://hostname:port/path/のようにファイル名を指定しないでアクセスした場合はvs_httpdがドキュメントルート /path/配下のindex.htmlを表示させようとする。試しにvs_httpdをポート(8888)、ドキュメントルート(/home /vs_httpd/docs)、デーモンモードON、VerboseモードONで起動させてみる。

$ ./vs_httpd -p 8888 -d /home/vs_httpd/docs -D -v
svr_addr=0.0.0.0
svr_port=8888
doc_root=/home/vs_httpd/docs
verbose=1
daemonize=1
$ ※

※ daemonized! fork成功、親プロセスからはexitで子プロセスに処理が移る

ページ表示テスト用にvs_httpd/pages下にhtml、css、jpegファイルを用意してあるのでこれらを/home/vs_httpd /docs配下に配置して実際にブラウザでindex.htmlを表示させてみる。成功すると以下のようなページが表示させる。

VSHTTPD

ベンチマーク結果

ab(apache bench)でベンチマークテストをしてみる。並列数100、リクエスト数10000。比較のためにapache2(mpm prefork)でもテストをしてみる。まずはvs_httpdのテスト結果。

$ ab -c 100 -n 10000 http://127.0.0.1:8888/

Server Software:        
Server Hostname:        127.0.0.1
Server Port:            8888

Document Path:          /
Document Length:        285 bytes

Concurrency Level:      100
Time taken for tests:   2.295104 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      3490440 bytes
HTML transferred:       2858550 bytes
Requests per second:    4357.10 [#/sec] (mean)
Time per request:       22.951 [ms] (mean)
Time per request:       0.230 [ms] (mean, across all concurrent requests)
Transfer rate:          1484.90 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   4.2      4      27
Processing:     5   17   6.4     15      46
Waiting:        1   13   6.5     11      45
Total:         10   22   7.0     21      47

Percentage of the requests served within a certain time (ms)
  50%     21
  66%     24
  75%     26
  80%     27
  90%     32
  95%     37
  98%     41
  99%     42
 100%     47 (longest request)

次にapache2(mpm prefork)でも同様のテストを行う。利用するページはvs_httpdと同じでテスト環境のprefork MPM設定値は次のとおり。

[prefork MPMの設定値]
StartServers          5
MinSpareServers       5
MaxSpareServers      10
MaxClients          150
MaxRequestsPerChild   0

以下、apache2のテスト結果。

$ ab -c 100 -n 10000 http://127.0.0.1/

Server Software:        Apache/2.2.2
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /
Document Length:        285 bytes

Concurrency Level:      100
Time taken for tests:   3.580232 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      5310000 bytes
HTML transferred:       2850000 bytes
Requests per second:    2793.12 [#/sec] (mean)
Time per request:       35.802 [ms] (mean)
Time per request:       0.358 [ms] (mean, across all concurrent requests)
Transfer rate:          1448.23 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.8      0      10
Processing:     8   35   7.4     31      66
Waiting:        2   34   7.4     31      66
Total:         12   35   7.4     31      66

Percentage of the requests served within a certain time (ms)
  50%     31
  66%     36
  75%     40
  80%     43
  90%     47
  95%     50
  98%     53
  99%     54
 100%     66 (longest request)

単純すぎるベンチマークテストなのでアレなのですが、10000リクエストの処理時間だけをみるとvs_httpd(2.295104 seconds)のほうがapache2(3.580232 seconds)よりも約35%速いといえる。余計な処理はな何も行わないで静的ファイル処理に特化しているので速いのは当たり前なんだけど、うれしい。この世界、速いは美徳。

おわり。

Related posts:

  1. LibeventのRPCフレームワークによるC/Sプログラミング
  2. AWK HTTPサーバ

Posted in: Programming

Tags: , , , , , , , ,



Sorry, the comment form is closed at this time.