リクエストヘッダの管理

As far as the NGINX.HeadersIn and NGINX.HeadersOut classes of the ngx_http_js_module implemented almost fully now we can talk about this headers_in and headers_out structs a little.

NGINXのHTTPヘッダは2つの部分に分かれます: 入力リクエストヘッダ(headers_in 構造) と出力リクエストヘッダ (headers_out 構造)。応答としてエンティティのようなものはありません。全てのデータは同じ1つのリクエスト構造に格納されます。実際の応答データはリクエストデータと headers_out構造フィールドから構築されます。

NGINX内の全ては高度に最適化されます。No memory overhead caused by strings copying, no memory leaks and alloc/free burden as far as memory is managed with pools, no wasted CPU cycles by comparing those strings again and again, everything is cached in a sane way, complicated things are pre-calculated at configure stage. So are the input (and output) headers and all this optimizations are the root of their complexity and beauty.

HTTPヘッダの柔軟性

HTTPヘッダについて少し話しましょう。私達全員が多くのヘッダを見てきています。そして、HTTPのヘッダがとても柔軟なデータ形式であることを知っています。クライアントは一つの簡単なヘッダ、あるいは独自の各行に同じ名前を持つ多くのヘッダ、あるいは多くの行に分割された1つの大きなヘッダを送信するかも知れません。それは幾分乱雑です。今度はNGINXがその乱雑さを統治する番です。

NGINXは頻繁に使われるよく知られたヘッダに注意します(良く知られたheaders_inのリスト)。NGINXはヘッダをパースし使いやすい場所に格納します(headers_in内の直接のポインタ)。良く知られたヘッダが1つ以上の値(例えばクッキーあるいはCache-Control)から成るかも知れない場合は、NGINXはそれを配列で処理できるかも知れません。数値を持つと知られているヘッダ(Content-Length, Expires) については、NGINXはテキストをパースし、それを直接 headers_in 構造に格納します。ヘッダの残りの全ては注意深く headers_in構造の中の1つの簡単なリストに格納されます。つまり何も紛失されません。

ヘッダ値の取得

とは言っても、値を取得するには少なくとも3つの方法があります。既に知っているように、各入力ヘッダ値は headers_in->headers リスト(typeof ngx_list_t) 内のブルーとフォース照合によって取得されるかも知れません。良く知られたヘッダ値は headers_in 構造の中の簡単なポインターの助けによって見つかるかも知れません。(ヘッダが存在しない場合はNULL)。そして、良く知られた数字のヘッダに関して、値を取得する簡単な方法でさえあります: headers_in内の特別なフィールド

(content_length_n が良い例です)。

コンパイル時にどのヘッダを読み込もうとしているかを知っている場合は、これが良いです。しかし、名前が単なる文字列の場合に実行時に名前でヘッダを取得するにはどうすればよいか?このような種類の状況のために、ヘッダーのスマートなハッシュcmcf->headers_in_hashを持ちます。ヘッダがNGINXに知られている場合、ヘッダ名はこのハッシュ内にキャッシュされ、比較的高速にヘッダの値を見つけることができます。ヘッダがハッシュされていない場合、ヘッダの全体のリストを走り読みし、全てのヘッダ名を比較する必要があります。これは速くありませんが、遅くもありません。残念ながら、名前によってヘッダのデジタル(すでにパースされた)表現のオフセットを取得する方法はないため、必要になる度にテキストの値をパースする必要があります。そして、実行時にランダムなヘッダのデータタイプ表現を知らない限りそれが普通です。

So far, we can get a full list of input headers and run through it to enumerate, direct access a header with its personal field in headers_in structure; and we even may get the already parsed value if the header is of type number, time etc.

以下は幾つかの例です。

指定された名前を持つ1つのヘッダのためのブルートフォース検索

static ngx_table_elt_t *
search_headers_in(ngx_http_request_t *r, u_char *name, size_t len) {
    ngx_list_part_t            *part;
    ngx_table_elt_t            *h;
    ngx_uint_t                  i;

    /*
    Get the first part of the list. There is usual only one part.
    */
    part = &r->headers_in.headers.part;
    h = part->elts;

    /*
    Headers list array may consist of more than one part,
    so loop through all of it
    */
    for (i = 0; /* void */ ; i++) {
        if (i >= part->nelts) {
            if (part->next == NULL) {
                /* The last part, search is done. */
                break;
            }

            part = part->next;
            h = part->elts;
            i = 0;
        }

        /*
        Just compare the lengths and then the names case insensitively.
        */
        if (len != h[i].key.len || ngx_strcasecmp(name, h[i].key.data) != 0) {
            /* This header doesn't match. */
            continue;
        }

        /*
        Ta-da, we got one!
        Note, we'v stop the search at the first matched header
        while more then one header may fit.
        */
        return &h[i];
    }

    /*
    No headers was found
    */
    return NULL;
}

ハッシュを使った高速な検索

ngx_table_elt_t *
search_hashed_headers_in(ngx_http_request_t *r, u_char *name, size_t len) {
    ngx_http_core_main_conf_t  *cmcf;
    ngx_http_header_t          *hh;
    u_char                     *lowcase_key;
    ngx_uint_t                  i, hash;

    /*
    Header names are case-insensitive, so have been hashed by lowercases key
    */
    lowcase_key = ngx_palloc(r->pool, len);
    if (lowcase_key == NULL) {
        return NULL;
    }

    /*
    Calculate a hash of lowercased header name
    */
    hash = 0;
    for (i = 0; i < len; i++) {
        lowcase_key[i] = ngx_tolower(name[i]);
        hash = ngx_hash(hash, lowcase_key[i]);
    }

    /*
    The layout of hashed headers is stored in ngx_http_core_module main config.
    All the hashes, its offsets and handlers are pre-calculated
    at the configuration time in ngx_http_init_headers_in_hash() at ngx_http.c:432
    with data from ngx_http_headers_in at ngx_http_request.c:80.
    */
    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

    /*
    Find the current header description (ngx_http_header_t) by its hash
    */
    hh = ngx_hash_find(&cmcf->headers_in_hash, hash, lowcase_key, len);

    if (hh == NULL) {
        /*
        There header is unknown or is not hashed yet.
        */
        return NULL;
    }

    if (hh->offset == 0) {
        /*
        There header is hashed but not cached yet for some reason.
        */
        return NULL;
    }

    /*
    The header value was already cached in some field
    of the r->headers_in struct (hh->offset tells in which one).
    */

    return *((ngx_table_elt_t **) ((char *) &r->headers_in + hh->offset));
}

構造フィールドを使った超高速ヘッダアクセス

ngx_table_elt_t *
get_host_from_headers_in(ngx_http_request_t *r) {
    /*
    Returns NULL if there is no such a header.
    */
    return r->headers_in.host;
}

あらかじめパースされた値を使った超々高速ヘッダアクセス

off_t
get_content_length_n_from_headers_in(ngx_http_request_t *r) {
    /*
    Returns -1 if the Content-Length wasn't set.
    */
    return r->headers_in.content_length_n;
}

この例は、あらかじめハッシュされたキーを使った検索に比べて、キャッシュおよび最適化されたヘッダアクセスがどれほど高速かの幻想を与えます。

ハッシュされた検索はどのように動作するか?

設定ステージにおいて、NGINXは(上で述べたように)良く知られたHTTPヘッダのハッシュngx_hash_t) を生成します。各ペアの中で、キーはヘッダ名でその値はNGIXNヘッダハンドラー構造(とても洗練された構造です。分かるでしょう)です。In this structure we can see the header name, its handler on a stage of headers parsing (for internal use) and, the most interesting, the offset of the header value in the headers_in struct. リクエスト値が追加された時に、このオフセットがリクエスト構造の中の適切なフィールドを満たすために使われます。パースのステージにおいて、NGINXは小文字のヘッダ名(HTTPヘッダ名は大文字小文字を気にしません)のハッシュを計算し、このハッシュ(メインのconfヘッダがあれば)を使ってヘッダのハンドラーを検索します。ハンドラーが見つかると、NGINXはそれを起動します。そうでなければ単にキー/値ペアをヘッダーの単純なリスト(headers_in.headers)に追加します。どのように生成されるかを知ると、とても単純です ;)

出力ヘッダについてはどうか?

If you’ve red the post you do know almost everything about``headers_out``. 唯一の違いはheaders_outは実行時に名前で出力ヘッダを見つけるためのハッシュを持ちません。

どうやってヘッダーを設定するか?

NGINXが多くの場所でヘッダ値を格納するかも知れない限り、ヘッダの設定には注意しなければなりません。全ての良く知られたヘッダは設定するために特別な方法が必要です。数字のヘッダの場合、3度設定することができます: リスト内での簡単なキー/値ペア、headers_in構造内でのポインター、そして、headers_inの特別なフィールド内での実際の数値の値。Every step reflects the way you get the header value.

headers_out 内のContent-Length

例えばheaders_out内のContent-Lengthを設定してみましょう。

ngx_int_t
set_content_length_n_in_headers_out(ngx_http_request_t *r, ngx_str_t *length, off_t length_n) {
    ngx_table_elt_t   *h;

    h = r->headers_out.content_length;
    if (h == NULL) {
        /*
        The header is not present at all. We have to allocate it...
        */
        h = ngx_list_push(&r->headers_out.headers);
        if (h == NULL) {
            return NGX_ERROR;
        }

        /*
        ... setup the header key ...
        */
        h->key.data = (u_char *) "Content-Length";
        h->key.len = sizeof("Content-Length") - 1;

        /*
        ... and then set the headers_out field to tell others
        that the header is already set.
        */
        r->headers_out.content_length = h;
    }

    /*
    So far we have the header and are able to set its value.
    Do not forget to allocate the length.data memory in such
    place where the memory will survive till the request ends.
    The best place to store the data is the request pool (r->pool),
    of course.
    */
    h->value = *length;

    /*
    This trick tells ngx_http_header_module to reflect the header value
    in the actual response. Otherwise the header will be ignored and client
    will never see it. To date the value must be just non zero.
    */
    h->hash = 1;

    /*
    And do not forget to set up the numeric field.
    */
    r->headers_out.content_length_n = length_n;

    return NGX_OK;
}

未知のヘッダ

未知のヘッダ(独自のもの)は単にリストheaders_out.headers) に入れられ、忘れさられるかも知れません:

ngx_int_t
set_custom_header_in_headers_out(ngx_http_request_t *r, ngx_str_t *key, ngx_str_t *value) {
    ngx_table_elt_t   *h;

    /*
    All we have to do is just to allocate the header...
    */
    h = ngx_list_push(&r->headers_out.headers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    /*
    ... setup the header key ...
    */
    h->key = *key;

    /*
    ... and the value.
    */
    h->value = *value;

    /*
    Mark the header as not deleted.
    */
    h->hash = 1;

    return NGX_OK;
}

headers_in と proxy_pass

HTTPプロキシモジュールはヘッダが小文字にされたキー値を持つと期待され、そうでなければモジュールはクラッシュするだろうことに注意してください。つまり、もしproxy_passディレクティブを使ってサブリクエストをlocationに発行し、幾つかの独自のヘッダを設定したい場合は、以下のように適切に小文字にされたヘッダ名をセットアップしてください:

header->key = (u_char *) "X-La-La-La";
header->lowcase_key = (u_char *) "x-la-la-la";