Python で HTTP/2 サーバーを書こう

[nghttp2](https://nghttp2.org) に含まれる Python API を使って Python で HTTP/2 サーバーを書く方法を説明します.
Python API は nghttp2 C ライブラリを使った拡張ライブラリとして提供されています.  HTTP/2 サーバーを提供するコードが `asyncio` モジュールを使っているため, Python 3.4 以降が必要になります.  拡張ライブラリをビルドするには, Python 3.4 の開発パッケージと Cython が必要です.  Ubuntu 14.04 LTS では, python3.4-dev と cython パッケージが利用できます.  configure スクリプトは利用可能な Python 環境を自動で選択しますが, デフォルトの Python が 3.4 でない場合が多いと思いますので明示的に Python 3.4 のインタープリターのパスを `PYTHON` 変数で指定し configure スクリプトに与えます:

```
$ ./configure PYTHON=/usr/bin/python3.4
```

`make install` で nghttp2 C ライブラリと Python の拡張ライブラリがインストールされます.  インストールしたくない場合は, `LD_LIBRARY_PATH` と `PYTHONPATH` でそれぞれのディレクトリを指定することもできます.

準備ができましたので早速サーバーを書いてみましょう.  使用するのは `nghttp2.HTTP2Server` クラスと `nghttp2.BaseRequestHandler` クラスです.  `nghttp2.HTTP2Server` クラスはコネクションの待ち受け, イベントループの運用といったサーバー機能を提供するクラスです.  `nghttp2.BaseRequestHandler` は 1 ストリーム (HTTP リクエスト) をハンドリングするクラスです.  イベント毎に発火するコールバックメソッドがあり, サブクラス化してこれらコールバックメソッドを適宜実装してストリームを処理することになります.  細かい説明は後にすることにして, まずは "nghttp2 FTW" を返す簡単なサーバーの実装を見てみましょう:

```py
import ssl
import nghttp2

class Handler(nghttp2.BaseRequestHandler):

    def on_headers(self):
        res = b'nghttp2 FTW\n'
        self.send_response(status=200,
                           headers = [('content-length', str(len(res)))],
                           body=res)

# SSL/TLS を有効にするには, 証明書を server.crt, プライベートキーを server.key ファイルに
# 保存して, 以下 3 行を有効にし, nghttp2.HTTP2Server の ssl パラメータに ctx を指定します.
# ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
# ctx.options = ssl.OP_ALL | ssl.OP_NO_SSLv2
# ctx.load_cert_chain('server.crt', 'server.key')

server = nghttp2.HTTP2Server(('127.0.0.1', 8080), Handler, ssl=None)
server.serve_forever()
```

実行すると 8080 ポートで待ち受けします.  `nghttp http://127.0.0.1:8080/` でアクセスすると "nghttp2 FTW" の文字が返ってくるはずです.

それでは `nghttp2.BaseRequestHandler` について詳しく見て行きましょう.

`nghttp2.BaseRequestHandler` は以下のインスタンス変数を提供しています:

* client_address: クライアントのアドレス
* stream_id: ストリーム ID
* scheme: リクエスト URI のスキームであり, `:scheme` ヘッダーフィールドの値です.
* method: リクエストメソッドであり, `:method` ヘッダーフィールドの値です.
* host: `:authority` または `host` ヘッダーフィールドの値です.
* path: リクエストパスであり, `:path` ヘッダーフィールドの値です.

次にコールバックメソッドについて説明します.  上記の例では, `on_headers()` メソッドを実装しています.  このメソッドはリクエストヘッダーを受け取ったときに呼ばれます.  そのほかに以下のコールバックメソッドがあります.

* on_data(data): リクエストボディのデータを受信した場合に呼ばれます.  *data* は受信したデータです.
* on_request_done(): リクエストをすべて受信した場合に呼ばれます.
* on_close(error_code): ストリームが閉じる直前に呼ばれます. *error_code* は HTTP/2 のエラーコードであり, ストリームが閉じた原因を示します.  0 の場合はエラーなしを意味します.

応答を返すには, 上記例でもあるように `send_response(status=200, headers=None, body=None)` メソッドを使います.  *status* は HTTP ステータスコードを指定します.  *headers* には追加のレスポンスヘッダーを指定します.  *body* はレスポンスボディで, `str`, `byte` または `io.Base` サブクラスのインスタンスを指定します.

HTTP/2 の特徴の一つである Server Push を行うための `push(path, method='GET', request_headers=None, status=200, headers=None, body=None)` メソッドも提供しています.  Server Push ではサーバーがプッシュするリソースをクライアントがリクエストしたときに使うであろうリクエストパスやメソッド, 追加のリクエストヘッダーフィールドを指定しなければなりません.  それぞれ, *path*, *method*, *request_headers* で指定します.  残りの *status*, *headers*, *body* は, `send_response()` メソッドと同様でレスポンスの内容を指定します.

上記の例に Server Push を追加してみましょう.  リクエストパス /push のリソースをプッシュすることにします:

```py
class Handler(nghttp2.BaseRequestHandler):

    def on_headers(self):
        self.push(path='/push',
                  status=200,
                  body='pushed content')

        res = b'nghttp2-python FTW\n'
        self.send_response(status=200,
                           headers = [('content-length', str(len(res)))],
                           body=res)

...
```

nghttp コマンドラインツールでアクセスすればプッシュされていることがわかります.  多重化されるストリームの数は制限がありますが, プッシュは複数指定することができます.  また, プロトコルの制約により, `push()` は `send_response()` よりも先に呼ぶ必要があります.

ここまで nghttp2 の Python API を使って HTTP/2 サーバーを書く方法を説明しました.  詳しい API については, [ドキュメント](https://nghttp2.org/documentation/python-apiref.html) を参照ください.