今回はLighttpdについて細かく書いている日本語サイトがないので書いてみました。
CONTENTS
LighttpdはApacheやnginxと比較すると知名度が低く全体の1%も使われていないようですが,省メモリで高速なHTTPサーバとして定評があります。
まず公式サイトからLighttpdをダウンロードしましょう。Lighttpd-1.4.33/src 以下でmodのprefixがついているのがプラグインモジュール郡です。
複数のフックエントリが用意されており,用途にあわせて簡単にプラグインを作成できます。
今回扱うのは以下の内容です。
・イベントループ
・サーバの状態遷移
・httpリクエストの分解
・httpレスポンスの生成
・ソケット通信による送受信
イベントハンドリング
イベントハンドラと言えばjavaScriptで書けるサーバnodejsではlibev, あるいはmemcachedではlibeventが使われているようです。
Lighttpdではlibevというライブラリが使われています。
Lighttpdではプラットフォームによって異なるAPIが使われます。
libevのインターフェイスはfdevent.cで、fdevent_init()内でプラットフォーム別のAPIを呼んでいます。
基本的にはconnect()で接続しにいってpoll() で監視するという流れになります。
linuxならselect(),poll()またLinux Kernel 2.6から追加されたepoll()が使えるシステムコールです。
複数のファイルディスクリプタのモニタリングすることで多重I/Oを可能にしています。
監視対象のファイルディスクリプタに対して TCP ハンドシェイク終了後に反応するようです。
基本的なepoll()の使い方は,epoll_createでepollのfdを作成し,epoll_ctlでfdとepoll_fdを関連づけます。
epoll_waitでI/Oイベントが来るのを待ち,読み込み可能なfdがあればrfdが返ってくるので,それに対して処理を行うという流れになります。
その他のOだとBSDではkqueue、Solarisでは/dev/poll があります。
connection.c ,network.c ,server.c からfdevent.cのfdevent_event_setやfdevent_delを呼んで、そこからさらに各プラットフォームに合ったシステムコールを使ってイベント制御を行っているようです。
もしlinux kernel 2.6以上の場合はepollを使うと効率的です。
libevを呼ぶとev.cからさらにev_epoll.cに処理が移っていく仕組みのようです。
まず、プロセス起動時にfdevent_initが呼ばれます。
ここでfdevent_handler_t typeがFDEVENT_HANDLER_LINUX_SYSEPOLL(epoll環境)の場合だと,fdevent_linux_sysepoll.cのfdevent_linux_sysepoll_initが呼ばれます。
if (-1 == (ev->epoll_fd = epoll_create(ev->maxfds))) {
log_error_write(ev->srv, __FILE__, __LINE__, "SSS",
"epoll_create failed (", strerror(errno), "), try to set server.event-handler = \"poll\" or \"select\"");
return -1;
}
if (-1 == fcntl(ev->epoll_fd, F_SETFD, FD_CLOEXEC)) {
log_error_write(ev->srv, __FILE__, __LINE__, "SSS",
"fcntl on epoll-fd failed (", strerror(errno), "), try to set server.event-handler = \"poll\" or \"select\"");
close(ev->epoll_fd);
return -1;
}
epoll_createでepollのファイルディスクリプタをオープンします。(引数は0以上なら指定しなくても良い?)
fcntlでファイルディスクリプタの操作を行います。F_SETFDでFD_CLOEXEC ビットを0にします。
これは親プロセスが子プロセスを作る際に自分自身をforkしてexecする時に保持しているファイルディスクリプタも子プロセスにコピーされるので、子プロセスで実行したexecに脆弱性があり乗っ取られることを防ぐために,FD_CLOEXECをセットすることで指定されたファイルディスクリプタを自動的にCloseします。
サーバ状態遷移の仕組み
Lighttpdの起動時に、server.cのmain関数が呼ばれ起動オプションの処理やイベントハンドラの起動が行われます。
int main (int argc, char **argv) {
/* code */
while (!srv_shutdown) {
/* プロセスが終了のSIGNALを検出するまでループ */
}
}
connection.cでは接続とその状態を管理しています。関数は以下の通りです。
connection_del : buffer_reset
connection_close : fdevent_event_del,fdevent_unregister
connection_handle_write_prepare : con->request.http_methodによるhttp_statuscodeの設定
connection_handle_write : network_write_chunkqueue
connection_handle_fdevent : pollによる状態の判別
connection_state_machine : 状態遷移の管理
connection_state_machineではcon->stateによって処理を行いconnection_set_stateで次の状態に遷移しています。
例えばCON_STATE_HANDLE_REQUESTの場合はHTTPリクエストヘッダをpasreするhttp_request_parse関数を呼んでいます。
if (http_request_parse(srv, con)) {
/* we have to read some data from the POST request */
connection_set_state(srv, con, CON_STATE_READ_POST);
break;
}
http_request_parseはrequest.cにあります。
HTTP1.1(RFC2616)に対応しており、以下のようにmajor_numとminor_numでHTTPのバージョンのチェックをしています。
if (major_num == 1 && minor_num == 1) {
con->request.http_version = con->conf.allow_http11 ? HTTP_VERSION_1_1 : HTTP_VERSION_1_0;
} else if (major_num == 1 && minor_num == 0) {
con->request.http_version = HTTP_VERSION_1_0;
} else {
con->http_status = 505;
return 0;
}
また,http_request_parseではrequest.http_method, request.content_length, request.uriをリクエストから取得しconにセットしています。
connection_glue.c で状態を取得する関数が用意されています。
* glue : 接着剤の意味?
const char *connection_get_state(connection_state_t state) {
switch (state) {
case CON_STATE_CONNECT: return "connect";
case CON_STATE_READ: return "read";
case CON_STATE_READ_POST: return "readpost";
case CON_STATE_WRITE: return "write";
case CON_STATE_CLOSE: return "close";
case CON_STATE_ERROR: return "error";
case CON_STATE_HANDLE_REQUEST: return "handle-req";
case CON_STATE_REQUEST_START: return "req-start";
case CON_STATE_REQUEST_END: return "req-end";
case CON_STATE_RESPONSE_START: return "resp-start";
case CON_STATE_RESPONSE_END: return "resp-end";
default: return "(unknown)";
}
}
状態遷移を図示すると以下のようになります。
HTTPリクエストの分解
リクエストをparseしているのはrequest.cですが、まずは基本となるリクエスト構造体をみてみます。
http_methodやcontent_lengthなどを管理している構造体です。
// base.h
typedef struct {
/** HEADER */
/* the request-line */
buffer *request;
buffer *uri;
buffer *orig_uri;
http_method_t http_method;
http_version_t http_version;
buffer *request_line;
/* strings to the header */
buffer *http_host; /* not alloced */
const char *http_range;
const char *http_content_type;
const char *http_if_modified_since;
const char *http_if_none_match;
array *headers;
/* CONTENT */
size_t content_length; /* returned by strtoul() */
/* internal representation */
int accept_encoding;
/* internal */
buffer *pathinfo;
} request;
chunkは連結リストになっています。一つのchunkは、データ本体の先頭にそのデータについての情報(データ長やデータの種類、識別子など)を付加した形になっており、これをいくつも連ねてデータ全体を表現するようになっています。
連結リストの簡単な例
簡単に連結リストのおさらいをします。以下のようなlist.cをgccでコンパイルし実行します。
// list.c (MacOS)
#include
#include // other OS
#define N 100
typedef struct _buffer{
char *ptr;
size_t used;
size_t size;
struct _buffer *next;
}buffer;
int setList(buffer *p, char *data);
void allDeleteList(buffer *p);
buffer f_buf;//セルの先頭 実体
int main(int argc, char** argv)
{
f_buf.next = NULL;//先頭のセルの初期化
setList(&f_buf,"");
setList(f_buf.next, "");
char c[N] = {'\0'};
buffer *tmp2;
tmp2 = f_buf.next;
int i;
for (i = 0; i < N; i++){
sprintf(c,"mem is %d",i);
setList(tmp2->next,c);
tmp2 = tmp2->next;
printf("[%d] tmp2->next->ptr : %s\n",i,tmp2->next->ptr);
}
// NULL check
if(tmp2->next->next == 0x00)printf("tmp2->next->next == null\n");
allDeleteList(tmp2);
return 0;
}
int setList(buffer* p, char *data)
{
buffer *tmp;
tmp = (buffer*)malloc(sizeof(buffer));
if(tmp == NULL)return -1;
tmp->next = p->next;
tmp->ptr = data;
p->next = tmp;
return 0;
}
void allDeleteList(buffer *p)
{
if(p == NULL)return;
buffer *tmp;
while (p->next != NULL)
{
tmp = p->next;
p->next = tmp->next;
free(tmp);
}
}
結果は以下のように次々にポインタで連結されるリストができます。
tmp2->next->nextはNULLとなっています。
[90] tmp2->next->ptr : mem is 90
[91] tmp2->next->ptr : mem is 91
[92] tmp2->next->ptr : mem is 92
[93] tmp2->next->ptr : mem is 93
[94] tmp2->next->ptr : mem is 94
[95] tmp2->next->ptr : mem is 95
[96] tmp2->next->ptr : mem is 96
[97] tmp2->next->ptr : mem is 97
[98] tmp2->next->ptr : mem is 98
[99] tmp2->next->ptr : mem is 99
tmp2->next->next == null
話を戻します。chunkにはファイル型とメモリ型があり、buffer型mem、file構造体(一時ファイル)、リスト構造で次のchunkへのポインタを持っています。
このchunkが連鎖しているのがchunkqueue構造体です。
typedef struct chunk {
enum { UNUSED_CHUNK, MEM_CHUNK, FILE_CHUNK } type;
buffer *mem; /* either the storage of the mem-chunk or the read-ahead buffer */
struct {
/* filechunk */
buffer *name; /* name of the file */
off_t start; /* starting offset in the file */
off_t length; /* octets to send from the starting offset */
int fd;
struct {
char *start; /* the start pointer of the mmap'ed area */
size_t length; /* size of the mmap'ed area */
off_t offset; /* start is octet away from the start of the file */
} mmap;
int is_temp; /* file is temporary and will be deleted if on cleanup */
} file;
off_t offset; /* octets sent from this chunk
the size of the chunk is either
- mem-chunk: mem->used - 1
- file-chunk: file.length
*/
struct chunk *next;
} chunk;
typedef struct {
chunk *first;
chunk *last;
chunk *unused;
size_t unused_chunks;
array *tempdirs;
off_t bytes_in, bytes_out;
} chunkqueue;
chunkが持っているmemはbuffer型です。
buffer型はbuffer.hで定義されポインタptr,使用済サイズ,全体のサイズから成ります。
typedef struct {
char *ptr;
size_t used;
size_t size;
} buffer;
リクエストのダンプをしたい場合, chunkqueueのchunkをfor文でfirstからひとつずつ取っていく方法をよく使います。
HTTPレスポンスの生成
基本はbase.hにあるresponse構造体です。
typedef struct {
off_t content_length;
int keep_alive; /* used by the subrequests in proxy, cgi and fcgi to say the subrequest was keep-alive or not */
array *headers;
enum {
HTTP_TRANSFER_ENCODING_IDENTITY, HTTP_TRANSFER_ENCODING_CHUNKED
} transfer_encoding;
} response;
response.cの handler_t http_response_prepare(server *srv, connection *con) では
RFC 2396に沿ったURIの構築やHTTPのヴァージョン、con->keep_aliveの設定を行っています。
そして、最後にCRLF(rnrn)を追加しています。
buffer_append_string_len(b, CONST_STR_LEN("\r\n\r\n"));
また、int http_response_write_header(server *srv, connection *con) ではhttp_statusの設定を行っています。これはhandler_t http_response_prepareの前準備の関数でしょうか。
CGIなどからresponseの中身を書き込むのはbuffer.cの関数群を使うことになります。
ソケット通信による送受信
ソケット通信では、送受信に様々なシステムコールがありますがLighttpdではsendfile,sendfilev,write,writev,read,recvを使うことができます。
“send(2), sendto(2), sendmsg(2) はソケットを通してデータを送信し、 recv(2) recvfrom(2), recvmsg(2) はソケットからデータを受信する。
poll(2) と select(2) はデータの到着を待ったり、データ送信の準備ができるまで待ったりする。
さらに、 write(2), writev(2), sendfile(2), read(2), readv(2) のような標準的な I/O 操作もデータの読み書きに用いることができる。
– man page”
network.cのnetwork_backend_t構造体で書き込みを行うためのシステムコールを列挙しています。
typedef enum {
NETWORK_BACKEND_UNSET,
NETWORK_BACKEND_WRITE,
NETWORK_BACKEND_WRITEV,
NETWORK_BACKEND_LINUX_SENDFILE,
NETWORK_BACKEND_FREEBSD_SENDFILE,
NETWORK_BACKEND_SOLARIS_SENDFILEV
} network_backend_t;
また、network.cのsetsockopt()でソケットに関する設定を行っています。setsockoptは以下になっています。
int setsockopt(ソケットディスクリプタ, ソケットレベル, 設定する項目, 設定する値, 設定する値のバイト数)
network_backend_writeはbase.hにあるserver構造体にあります。
int (* network_backend_write)(struct server *srv, connection *con, int fd, chunkqueue *cq, off_t max_bytes);
このsrv->network_backend_writeに、network.c内でnetwork_backend_t構造体によって分岐する関数を代入しています。
switch(backend) {
case NETWORK_BACKEND_WRITE:
srv->network_backend_write = network_write_chunkqueue_write;
break;
#ifdef USE_WRITEV
case NETWORK_BACKEND_WRITEV:
srv->network_backend_write = network_write_chunkqueue_writev;
break;
#endif
#ifdef USE_LINUX_SENDFILE
case NETWORK_BACKEND_LINUX_SENDFILE:
srv->network_backend_write = network_write_chunkqueue_linuxsendfile;
break;
#endif
#ifdef USE_FREEBSD_SENDFILE
case NETWORK_BACKEND_FREEBSD_SENDFILE:
srv->network_backend_write = network_write_chunkqueue_freebsdsendfile;
break;
#endif
#ifdef USE_SOLARIS_SENDFILEV
case NETWORK_BACKEND_SOLARIS_SENDFILEV:
srv->network_backend_write = network_write_chunkqueue_solarissendfilev;
break;
#endif
default:
return -1;
}
例えば、network_backend_tがNETWORK_BACKEND_WRITEだった場合はnetwork_write.cの network_write_chunkqueue_write が選択されます。
if ((r = write(fd, start + (abs_offset - c->file.mmap.offset), toSend)) < 0) {
switch (errno) {
case EAGAIN:
case EINTR:
r = 0;
break;
case EPIPE:
case ECONNRESET:
return -2;
default:
log_error_write(srv, __FILE__, __LINE__, "ssd",
"write failed:", strerror(errno), fd);
return -1;
}
}
network_server_initではnetwork_backend_writeの戻り値をチェックした後、chunkqueueを解放しています。
chunkqueue_remove_finished_chunks(cq);
ret = chunkqueue_is_empty(cq) ? 0 : 1;
keyvalue.cではHTTPメソッドの文字列とレスポンス時のStatusCodeの対応付けする構造体があります。
static keyvalue http_methods[] = {
{ HTTP_METHOD_GET, "GET" },
{ HTTP_METHOD_POST, "POST" },
{ HTTP_METHOD_HEAD, "HEAD" },
{ HTTP_METHOD_PROPFIND, "PROPFIND" },
{ HTTP_METHOD_PROPPATCH, "PROPPATCH" },
{ HTTP_METHOD_REPORT, "REPORT" },
{ HTTP_METHOD_OPTIONS, "OPTIONS" },
{ HTTP_METHOD_MKCOL, "MKCOL" },
{ HTTP_METHOD_PUT, "PUT" },
{ HTTP_METHOD_PATCH, "PATCH" },
{ HTTP_METHOD_DELETE, "DELETE" },
{ HTTP_METHOD_COPY, "COPY" },
{ HTTP_METHOD_MOVE, "MOVE" },
{ HTTP_METHOD_LABEL, "LABEL" },
{ HTTP_METHOD_CHECKOUT, "CHECKOUT" },
{ HTTP_METHOD_CHECKIN, "CHECKIN" },
{ HTTP_METHOD_MERGE, "MERGE" },
{ HTTP_METHOD_LOCK, "LOCK" },
{ HTTP_METHOD_UNLOCK, "UNLOCK" },
{ HTTP_METHOD_MKACTIVITY, "MKACTIVITY" },
{ HTTP_METHOD_UNCHECKOUT, "UNCHECKOUT" },
{ HTTP_METHOD_VERSION_CONTROL, "VERSION-CONTROL" },
{ HTTP_METHOD_CONNECT, "CONNECT" },
{ HTTP_METHOD_UNSET, NULL }
};
終わりに
base.hのserver構造体にconnections *joblistがあります。
joblistを使うことであらゆるLighttpdに対するイベントをプラグイン内でフックすることができます。
* connection.cの connection_handle_fdevent でイベントを検出できます
joblist->sizeが0の場合、mallocで確保 used == size の場合にreallocでメモリを再割当てし,ポインタ変数ptrに代入しています。
また,configfile.cのconfig_patch_connectionはサーバ起動時に読み込んだ lighttpd.conf のチェックを行い設定項目を適用するための仕組みを提供しています。