Posted at
http2Day 6

The nghttp2 advanced C programming

More than 1 year has passed since last update.

nghttp2はRFC 7540 HTTP/2のC言語による実装である。

誰に聞いても教えてくれないlibnghttp2 C APIプログラミングの高度な機能について学習しよう。


Trailer fieldを送信する

HTTP headerはボディよりも先に送るのでヘッダーとよばれている。trailer fieldは、ボディの後に送信するのでトレーラーと呼ばれている。HTTP/1.1のときは、chunked transfer encodingを使った時だけ送信することができた。HTTP/2では、transfer encodingは廃止となり、trailerは常に利用可能となった。

libnghttp2でtrailerを送信するには、まず、nghttp2_submit_requestまたはnghttp2_submit_responseで、ボディを送信しなければならない。libnghttp2ではボディの送信のためにnghttp2_data_providerをnghttp2_submit_requestまたはnghttp2_submit_responseに渡す。nghttp2_data_provider.read_callbackがlibnghttp2から呼ばれるので、アプリケーションは送信するデータをlibnghttp2に渡す。read_callbackの内部で、すべてのデータを渡し終わったあと、*data_flagsNGHTTP2_DATA_FLAG_EOF | NGHTTP2_DATA_NO_END_STREAMをセットする。そして、nghttp2_submit_trailerを呼び出す。trailer fieldは、nvaで引き渡す。


HPACKダイナミックテーブルサイズを変更する

HPACKはHTTPヘッダーフィールドの転送量を削減する圧縮技法であり、RFC 7541に定義されている。圧縮に一役かっている機能は、ダイナミックテーブルと呼ばれているもので、一度送ったヘッダーは送受信側がテーブルに蓄えておき、次回の送信時は、テーブル内の該当する番号を送信し、転送量を削減するというものである。ダイナミックテーブルは上限サイズが限られている。サイズの計算は、ヘッダー当たりの名前と値のバイト数+オーバーヘッド32バイトで計算される。上限サイズを超える場合は、最も古いエントリが削除される(FIFO)。デフォルトの上限サイズは4096バイトである。

libnghttp2ではダイナミックテーブルの上限サイズを変更することができる。上限サイズはヘッダー受信側(伸長する側)が希望の最大サイズを提出し、送信側(圧縮する側)が選択する。すなわち、クライアントからサーバーに送信する場合と、サーバーからクライアントへ送信する場合とで別々に上限サイズを決める必要がある。

受信側が最大テーブルサイズを送信側に通知する。libnghttp2では、nghttp2_submit_settingsivにsetting_idがNGHTTP2_SETTINGS_HEADER_TABLE_SIZE、valueを最大テーブルサイズとするエントリを含める。nghttp2_submit_settingsはHTTP/2セッション開始時に必ず送らなければならないので、それに含めれば良い。送信側は、このようにして伝えられた最大テーブルサイズを上限として、サイズを選択することができる。libnghttp2では、nghttp2_optionオブジェクトを生成し、nghttp2_option_set_max_deflate_dynamic_table_sizeで最大値を設定し、nghttp2_session_client_new2またはnghttp2_session_server_new2に渡してnghttp2_sessionを生成する。そうすると、libnghttp2が送信側からの最大テーブルサイズと、nghttp2_option_set_max_deflate_dynamic_table_sizeで指定したサイズのうち小さい方を上限サイズとして選択する。


Informationalレスポンスを送信する

HTTPステータスコード100番代は、informationalレスポンスコードと呼ばれている。200番代以降はfinalレスポンスコードと呼ばれる。サーバーは、finalレスポンスはただ一度のみ送信できる。finalレスポンスの前に0回以上のinformationalレスポンスを送信することができる。別の言葉でいうと、informationalレスポンスは何度も連続で送ってもよいが、必ず最後にfinalレスポンスを返さなくてはならない。informationalレスポンスはボディを含めることができないのでヘッダーフィールドだけで構成される。HTTP/2におけるinformationalレスポンスをHTTP/1.1のヘッダーフィールド風に書いた例である:

:status: 103

link: </style.css>; rel=preload

libnghttp2で、このようなinformationalレスポンスを送信するには、nghttp2_submit_headersを使用する。flagsに、NGHTTP2_FLAG_END_STREAMを含めてはいけない。


ボディを送信後にRST_STREAMを送信する

あるユースケースを考えよう。クライアントはまだアップロードの最中だが、サーバーはレスポンスを返して、ストリームをリセットしたいと考えている。このような場合、libnghttp2ではどのようにすればよいか。

nghttp2_submit_responseのすぐ後に、nghttp2_submit_rst_streamを呼ぶと、レスポンスボディがある場合、レスポンスボディよりも先にRST_STREAMフレームが送信されてボディが送信されない。これはlibnghttp2の仕様である。ボディを送信後にRST_STREAMを送信するには、nghttp2_submit_responseに渡したnghttp2_data_provider.read_callbackの中で、すべてのデータを送った後に、nghttp2_submit_rst_streamを呼べば良い。

関連issue: https://github.com/nghttp2/nghttp2/issues/692


フローコントロールを細かく制御する

HTTP/2ではクレジットベースのフローコントロール機能が有効になっている。受信側が受信可能なバイト数を送信側に伝える。このサイズはウインドウサイズと呼ばれる。送信側は、その範囲を超えないようにデータを送信する。フローコントロール対象となっているのは、主にリクエストボディやレスポンスボディを運ぶことになっているDATAフレームだけである。フローコントロールは各ストリームで独立に勘定されている(ストリームレベルフローコントロール)。また、接続全体でもフローコントロールが行われている(コネクションレベルフローコントロール)。よって常に二重にフローコントロールされていると考えてもらいたい。送信可能バイト数は、どちらもデフォルトで64KiBである。

libnghttp2ではデフォルトでは、ライブラリがフローコントロールの制御をすべて行う。アプリケーションは通常何も追加でコードを必要はない。

もっともデフォルトの64KiBは少なすぎる可能性がある。特にレイテンシが高く、バンド幅の広い回線ではそのポテンシャルを活かしきれない。ウインドウサイズを変更する方法は、ストリームとコネクションレベルフローコントロールで異なる。

ストリームレベルのウインドウサイズの初期値を変更するには、nghttp2_submit_settingsivsettings_idNGHTTP2_SETTINGS_INITIAL_WINDOW_SIZEvalueをウインドウサイズとなるエントリを含めてHTTP/2セッション開始直後に送る。

コネクションレベルのウインドウサイズを変更するには、nghttp2_session_set_local_window_sizeで、stream_idに0、window_sizeに希望のウインドウサイズを設定する。

ストリームレベルのウインドウサイズ変更も、nghttp2_session_set_local_window_sizeで行える。stream_idにストリームIDを渡す。nghttp2_submit_settingsとnghttp2_session_set_local_window_sizeの違いは、前者は通常接続直後に行って送信側に伝えるのに対し、後者は各ストリームごとに必要なら追加のHTTP/2 WINDOW_UPDATEフレームを送信するということである。

libnghttp2では、ストリームレベル、コネクションレベルともに、デフォルト値よりもウインドウサイズを小さくすることもできる。

ウインドウサイズの大小にかかわらず、libnghttp2は受信したデータ量が規定量を超えると自動で、WINDOW UPDATEフレームを送信し、相手側にもっとデータを送って良いというシグナルを送信する。プロキシーサーバーでデータをフォワードするときは、フォワードしてからWINDW UPDATEフレームを送信したいと思うだろう。そうしないとフローコントロールの存在意味がない。libnghttp2にはこれを実現する方法が存在する。nghttp2_optionオブジェクトを生成し、nghttp2_option_set_no_auto_window_updatevalに非ゼロを設定する。nghttp2_optionオブジェクトをnghttp2_session_client_new2またはnghttp2_session_server_new2に渡してnghttp2_sessionを生成する。アプリケーションは、受信したデータの処理が終わりバッファーに空きが出来た場合は、nghttp2_session_consumeを呼び出し、libnghttp2に処理が完了したバイト数を知らせる。nghttp2_session_consumeはストリームレベル、コネクションレベル両方に対して影響を与える。libnghttp2はあるこれによって知らされたバイト数がウインドウサイズのある一定割合に達したら、WINDOW_UPDATEを送信する。よってアプリケーションはWINDOW_UPDATEフレームを送信する必要はない。

nghttp2_session_consumeはストリームレベル、コネクションレベル両方に影響を与えるが、一方のみに作用する関数は別に用意されている。


パディングを付ける

HTTP/2でのパディングは、送信するバイト数を意図的に増やして、実際の送信バイト数を攻撃者から隠すためのセキュリティ機能である。HEADERSフレームとDATAフレームに付加することができる。最大256バイトのパディングが付加できる。パディングを付ける機能はあるのだが、どのようにパディングをつければよいのかは、RFC 7540には記載されていない。

とにかく、パディングを付けたいんだ、という場合、libnghttp2はパディングを付与する機能を持っている。

nghttp2_sessionオブジェクトを生成する際に渡すnghttp2_session_callbacksオブジェクトに対して、nghttp2_session_callbacks_set_select_padding_callbackを使ってnghttp2_select_padding_callbackを指定する。このコールバック関数は、HEADERS、DATAフレーム送信時に呼ばれ、パディングを何バイトつけるかを決定することができる。コールバック引数のmax_payloadlenはパディング込の最大ペイロード長である。パディングなしのペイロード長がframe->hd.lengthで分かるので、frame->hd.lengthからmax_payloadlenの間で、パディング込のペイロード長を決定して、返却する。frame->hd.lengthを返すとパディング無しという意味である。max_payloadlenを返すと、max_payloadlen - frame->hd.lengthバイトのパディングが追加される。


送信HTTPヘッダーフィールドをコピーを回避する

libnghttp2では、nghttp2_submit_requestnghttp2_submit_responsenva引数にHTTPヘッダーフィールドを受け取るようになっている。デフォルトでは、nvaで指定されてヘッダーフィールドはlibnghttp2がコピーを作る。この利点としては、アプリケーションは関数呼び出しの後は、ヘッダーのメモリを動的に確保した場合は破棄できることである。

ヘッダーは静的に確保されている場合は少なくない。文字列リテラルで渡す場合は、コピーは不要である。また、ストリームの終了までヘッダーを保持し続けるようなアプリケーションでもコピーは不要である。コピーが不要な場合は、nghttp2_nvのflagsNGHTTP2_NV_FLAG_NO_COPY_NAMENGHTTP2_NV_FLAG_NO_COPY_VALUEを指定する。NGHTTP2_NV_FLAG_NO_COPY_NAMEを指定すると、ヘッダー名(nghttp2_nv.name)のコピーをしなくなる。NGHTTP2_NV_FLAG_NO_COPY_VALUEを指定すると、ヘッダー値(nghttp2_nv.value)のコピーをしなくなる。これらフラグを使った場合は、アプリケーションはすくなくとも、nghttp2_on_frame_send_callbackやnghttp2_on_frame_not_send_callbackが呼ばれるまでヘッダーで使用しているメモリ領域は保持しておく責任を持つ。また、デフォルトでは、libnghttp2はヘッダー名をアルファベット小文字に変換しているが、NGHTTP2_NV_FLAG_NO_COPY_NAMEを付けた場合は、それをしないので、アプリケーションが責任をもってヘッダー名をアルファベット小文字にしなければならない。


ユーザ定義フレームを送信する

HTTP/2は拡張を許可しており、その中の一つにユーザ定義フレームを送信するというものがある。RFC 7540では10種類の標準フレームが定義されている。ユーザ定義フレームは定義されていないフレームタイプを使う。

libnghttp2ではユーザ定義フレームの送受信を行うことができる。

ユーザ定義フレームを送信するには、nghttp2_session_callbacksにnghttp2_session_callbacks_set_pack_extension_callbackを使って、ユーザ定義フレームをエンコードするコールバック関数をセットする。そしてnghttp2_submit_extensionを使ってフレームの送信を行う。

ユーザ定義フレームを受信するには、nghttp2_session_callbacksにnghttp2_session_callbacks_set_unpack_extension_callbacknghttp2_session_callbacks_set_on_extension_chunk_recv_callbackを使って2つのコールバック関数をセットする。それぞれフレームのペイロードをデコードする関数、フレームペイロードのデータを受信するたびに呼ばれる関数である。後者の関数が別に存在するのは、libnghttp2がペイロードデータをバッファリングしないからである。さらに、nghttp2_optionオブジェクトにnghttp2_option_set_user_recv_extension_typeを使って受信したいフレームのフレームタイプを指定する。nghttp2_optionオブジェクトをnghttp2_session_client_new2またはnghttp2_session_server_new2に渡してnghttp2_sessionを生成する。

nghttp2_unpack_extension_callbackで*payloadにセットしたポインターは、nghttp2_on_frame_recv_callbackのframe->ext.payloadで参照可能である。

詳しいコード付きの解説は、Implement user defined HTTP/2 non-critical extensionsを参照。