nghttp2を使用する #3

HTTP2のライブラリ「nghttp2」を紹介するエントリです。

nghttp2のインストールは[#1](http://qiita.com/0xfffffff7/items/c8f195c9f1782ca64e92)をご覧ください。

# 概要

今回はC++から呼び出すことができるHTTP2のハイレベルなAPI「libnghttp2_asio」を使います。
これを使うと簡単にC++でHTTP2サーバを立ててリクエストをさばくことができます。

boostをインストールする必要があります。
configureを見るとboost1.54以上があれば大丈夫なようなので、ソースではなくパッケージでインストールします。


# Boostのインストール

```
sudo apt-get install libboost-all-dev libboost-dev
```

# リビルド

nghttp2のディレクトリでオプションをつけてリビルドします。
vagrantだとメモリ不足でコンパイル中に落ちる可能性があるので、その場合はvagrant仮想マシンのメモリをvagrantfileでメモリを増やします。

```
./configure --enable-asio-lib
sudo make
sudo make install
```

正しくインストールされたことを確認する。

```
ll /usr/local/lib/                                                                                                                     
libnghttp2.a              libnghttp2.so.5           libnghttp2_asio.la        libnghttp2_asio.so.1.0.0  python3.4/                
libnghttp2.la             libnghttp2.so.5.7.2       libnghttp2_asio.so        pkgconfig/                
libnghttp2.so             libnghttp2_asio.a         libnghttp2_asio.so.1      python2.7/
```

# ソースを準備する

nghttp2の公式から、下記のサンプルを用意します。

ホストとポートを書き換えています。
server.cpp

```
#include <nghttp2/asio_http2_server.h>

using namespace nghttp2::asio_http2;
using namespace nghttp2::asio_http2::server;

int main(int argc, char *argv[]) {
  boost::system::error_code ec;
  http2 server;

  server.handle("/", [](const request &req, const response &res) {
    res.write_head(200);
    res.end("hello, world\n");
  });

  if (server.listen_and_serve(ec, "nghttp2.test", "8080")) {
    std::cerr << "error: " << ec.message() << std::endl;
  }
}
```

コンパイル

```
g++ -o server server.cpp -lnghttp2_asio -lboost_system -std=c++11
```

gccのバージョンの問題なのかstd::cerrでエラーが出たので、ソースの頭に「```#include <iostream>```」を追加しました。


```
g++ -o server server.cpp -lnghttp2_asio -lboost_system -std=c++11
```

SSLのメソッドがないと怒られたので、

```
 g++ -o server server.cpp -lnghttp2_asio -lboost_system -std=c++11 -lssl -lcrypto 
```

pthread系がないと怒られたので、

```
g++ -o server server.cpp -lnghttp2_asio -lboost_system -std=c++11 -lssl -lcrypto -lpthread  
```

成功しました。

ライブラリパスを通しておきます。

```
vi ~/.bashrc
export LD_LIBRARY_PATH=/usr/local/lib/
source ~/.bashrc  
```

実行します。

```
./server
```

クライアントから確認します。

```
 nghttp -nv http://nghttp2.test:8080
[  0.001] Connected
[  0.001] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.003] send HEADERS frame <length=39, flags=0x05, stream_id=1>
          ; END_STREAM | END_HEADERS
          (padlen=0)
          ; Open new stream
          :method: GET
          :path: /
          :scheme: http
          :authority: nghttp2.test:8080
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/0.7.10-DEV
[  0.007] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[  0.007] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.008] recv (stream_id=1) :status: 200
[  0.008] recv (stream_id=1) date: Tue, 31 Mar 2015 03:55:18 GMT
[  0.009] recv HEADERS frame <length=25, flags=0x04, stream_id=1>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.009] recv DATA frame <length=13, flags=0x01, stream_id=1>
          ; END_STREAM
[  0.010] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.010] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])
```

使い方はページリクエストをさばいて、リクエストコンテキストが参照できて、レスポンスオブジェクトに書き込む普通のWebフレームワークインターフェースのようです。


# SSLに対応させる

server_ssl.cpp

```
#include <iostream>
#include <nghttp2/asio_http2_server.h>

using namespace nghttp2::asio_http2;
using namespace nghttp2::asio_http2::server;

int main(int argc, char *argv[]) {
  boost::system::error_code ec;
  boost::asio::ssl::context tls(boost::asio::ssl::context::sslv23);

  tls.use_private_key_file("server.key", boost::asio::ssl::context::pem);
  tls.use_certificate_chain_file("server.crt");

  configure_tls_context_easy(ec, tls);

  http2 server;

  server.handle("/index.html", [](const request &req, const response &res) {
    res.write_head(200);
    res.end(file_generator("index.html"));
  });

  if (server.listen_and_serve(ec, tls, "nghttp2.test", "8080")) {
    std::cerr << "error: " << ec.message() << std::endl;
  }
}
```

index.htmlを用意します。

```
<html>
<body>
Hello nghttp2 asio API!
</body>
</html>
```

ビルドして実行

```
g++ -o server server_ssl.cpp -lnghttp2_asio -lboost_system -std=c++11 -lssl -lcrypto -lpthread  
./server
```

確認 (Chromeからでも確認できます)

```
nghttp -nv https://nghttp2.test:8080/index.html
```


# サーバープッシュ

サーバープッシュにチャンレジしてみましょう。
公式にサンプルが載っていますので、ホストとポートだけ書き換えます。
また、server.num_threads(4);を追加してスレッド数を4に増加させています。

server_push.cpp

```
#include <iostream>
#include <nghttp2/asio_http2_server.h>

using namespace nghttp2::asio_http2;
using namespace nghttp2::asio_http2::server;

int main(int argc, char *argv[]) {
  boost::system::error_code ec;
  boost::asio::ssl::context tls(boost::asio::ssl::context::sslv23);

  tls.use_private_key_file("server.key", boost::asio::ssl::context::pem);
  tls.use_certificate_chain_file("server.crt");

  configure_tls_context_easy(ec, tls);

  http2 server;
  server.num_threads(4);  // スレッド数4

  std::string style_css = "h1 { color: green; }";

  server.handle("/", [&style_css](const request &req, const response &res) {
    boost::system::error_code ec;
    auto push = res.push(ec, "GET", "/style.css");
    push->write_head(200);
    push->end(style_css);

    res.write_head(200);
    res.end(R"(
<!DOCTYPE html><html lang="en">
<title>HTTP/2 FTW</title><body>
<link href="/style.css" rel="stylesheet" type="text/css">
<h1>This should be green</h1>
</body></html>
)");
  });

  server.handle("/style.css",
                [&style_css](const request &req, const response &res) {
    res.write_head(200);
    res.end(style_css);
  });

  if (server.listen_and_serve(ec, tls, "nghttp2.test", "8080")) {
    std::cerr << "error: " << ec.message() << std::endl;
  }
}
```

```
g++ -o server server_push.cpp -lnghttp2_asio -lboost_system -std=c++11 -lssl -lcrypto -lpthread
./server
```

フレームを確認します。

```
nghttp -nv https://nghttp2.test:8080/  
[  0.002] Connected
[  0.008][NPN] server offers:
          * h2
          * h2-16
          * h2-14
The negotiated protocol: h2
[  0.013] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.014] send HEADERS frame <length=39, flags=0x05, stream_id=1>
          ; END_STREAM | END_HEADERS
          (padlen=0)
          ; Open new stream
          :method: GET
          :path: /
          :scheme: https
          :authority: nghttp2.test:8080
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/0.7.10-DEV
[  0.017] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
          (niv=1)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[  0.018] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.018] recv (stream_id=1) :method: GET
[  0.018] recv (stream_id=1) :scheme: https
[  0.018] recv (stream_id=1) :authority: nghttp2.test:8080
[  0.018] recv (stream_id=1) :path: /style.css
[  0.018] recv PUSH_PROMISE frame <length=29, flags=0x04, stream_id=1>
          ; END_HEADERS
          (padlen=0, promised_stream_id=2)
[  0.018] recv (stream_id=1) :status: 200
[  0.018] recv (stream_id=1) date: Tue, 31 Mar 2015 04:35:46 GMT
[  0.018] recv HEADERS frame <length=25, flags=0x04, stream_id=1>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  0.019] recv DATA frame <length=168, flags=0x01, stream_id=1>
          ; END_STREAM
[  0.019] recv (stream_id=2) :status: 200
[  0.019] recv (stream_id=2) date: Tue, 31 Mar 2015 04:35:46 GMT
[  0.019] recv HEADERS frame <length=2, flags=0x04, stream_id=2>
          ; END_HEADERS
          (padlen=0)
          ; First push response header
[  0.019] recv DATA frame <length=20, flags=0x01, stream_id=2>
          ; END_STREAM
[  0.020] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.020] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=2, error_code=NO_ERROR(0x00), opaque_data(0)=[])
```

スタイルシートがストリームID2でPUSH_PROMISEで予約され、その後にGETしていないのにデータがプッシュされているのが分かります。

```
[  0.018] recv (stream_id=1) :path: /style.css
[  0.018] recv PUSH_PROMISE frame <length=29, flags=0x04, stream_id=1>
          ; END_HEADERS
          (padlen=0, promised_stream_id=2)
[  0.019] recv HEADERS frame <length=2, flags=0x04, stream_id=2>
[  0.019] recv DATA frame <length=20, flags=0x01, stream_id=2>
```

![sample.png](https://qiita-image-store.s3.amazonaws.com/0/33116/c9844f28-f643-2ea3-aab3-590d2f6982aa.png)