LoginSignup
5
5

More than 5 years have passed since last update.

nghttp2を使用する #4

Last updated at Posted at 2015-04-03

HTTP2のライブラリとして使えるnghttp2を使うエントリです。
nghttp2のインストールは#1をご覧ください。

概要

引き続きハイレベルAPI「libnghttp2_asio」をもう少し使ってみたいと思います。
https://nghttp2.org/documentation/libnghttp2_asio.html

今回はクライアントのサンプルを動かしてみます。

GETしてみる

ソースにコメントを書いてみました。

#include <iostream>

#include <nghttp2/asio_http2_client.h>

using boost::asio::ip::tcp;

using namespace nghttp2::asio_http2;
using namespace nghttp2::asio_http2::client;

int main(int argc, char *argv[]) {

  // エラーコードと非同期のインターフェース
  boost::system::error_code ec;
  boost::asio::io_service io_service;

  // セッションオブジェクトを作成
  session sess(io_service, "nghttp2.test", "8080"); 

  // 接続 (自身のsessオブジェクトをキャプチャしてます)
  sess.on_connect([&sess](tcp::resolver::iterator endpoint_it) {

    boost::system::error_code ec;

    // リクエスト
    auto req = sess.submit(ec, "GET", "http://nghttp2.test:8080/"); 

    // ラムダ式でレスポンスを受け取るコールバックを指定
    req->on_response([](const response &res) {

      // status_code()でステータスコードを取得
      std::cerr << "HTTP/2 " << res.status_code() << std::endl;

      // ヘッダのマップをループする
      for (auto &kv : res.header()) {

        // キーとバリューを表示する
        std::cerr << kv.first << ": " << kv.second.value << "\n";
      }
      std::cerr << std::endl;

      // on_dataでラムダ式でDATAフレームを受け取るコールバックを指定.
      res.on_data([](const uint8_t *data, std::size_t len) {
        std::cerr.write(reinterpret_cast<const char *>(data), len);
        std::cerr << std::endl;
      });
    });

    // ストリームがクローズした際に呼ばれる.
    req->on_close([&sess](uint32_t error_code) {
      // shutdown session after first request was done.
      sess.shutdown();
    });
  });

  // エラーの時に呼ばれる
  sess.on_error([](const boost::system::error_code &ec) {
    std::cerr << "error: " << ec.message() << std::endl;
  });

  // ここでrun()してon_connect()が呼ばれる
  io_service.run();
}

ビルド

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

nghttp2のサーバーを起動しておきます。

nghttpd -v -d "" --no-tls 8080

ビルドしたクライアントプログラムを起動してみる。

./client

HTTP/2 200
cache-control: max-age=3600
content-length: 169
date: Fri, 03 Apr 2015 04:40:00 GMT
last-modified: Tue, 31 Mar 2015 04:44:56 GMT
server: nghttpd nghttp2/0.7.10-DEV

<!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>

GETの後にPOSTしてみる

sessionクラスのsubmitにリクエストデータ送信用のインターフェースがあるのでPOSTしてみます。

#include <iostream>

#include <nghttp2/asio_http2_client.h>

using boost::asio::ip::tcp;

using namespace nghttp2::asio_http2;
using namespace nghttp2::asio_http2::client;

int main(int argc, char *argv[]) {

  // エラーコードと非同期のインターフェース
  boost::system::error_code ec;
  boost::asio::io_service io_service;

  // セッションオブジェクトを作成
  session sess(io_service, "nghttp2.test", "8080");                                                                                                               

  // 接続 (自身のsessオブジェクトをキャプチャしてます)
  sess.on_connect([&sess](tcp::resolver::iterator endpoint_it) {

    boost::system::error_code ec;

    // リクエスト
    auto req = sess.submit(ec, "GET", "http://nghttp2.test:8080/");

    // ラムダ式でレスポンスを受け取るコールバックを指定
    req->on_response([](const response &res) {

      // status_code()でステータスコードを取得
      std::cerr << "HTTP/2 " << res.status_code() << std::endl;

      // ヘッダのマップをループする
      for (auto &kv : res.header()) {

        // キーとバリューを表示する
        std::cerr << kv.first << ": " << kv.second.value << "\n";
      }
      std::cerr << std::endl;

      // on_dataでラムダ式でDATAフレームを受け取るコールバックを指定.
      res.on_data([](const uint8_t *data, std::size_t len) {
        std::cerr.write(reinterpret_cast<const char *>(data), len);
        std::cerr << std::endl;
      });
    });


    // POST
    auto req2 = sess.submit(ec, "POST", "http://nghttp2.test:8080/", "THIS IS POST DATA!!");

    req2->on_response([](const response &res) {
      std::cerr << "HTTP/2 " << res.status_code() << std::endl;

      for (auto &kv : res.header()) {
        std::cerr << kv.first << ": " << kv.second.value << "\n";
      }
      std::cerr << std::endl;

      res.on_data([](const uint8_t *data, std::size_t len) {
        std::cerr.write(reinterpret_cast<const char *>(data), len);
        std::cerr << std::endl;
      });
    });


    // ストリームがクローズした際に呼ばれる.
    req->on_close([&sess](uint32_t error_code) {
      // shutdown session after first request was done.
      sess.shutdown();
    });
  });

  // エラーの時に呼ばれる
  sess.on_error([](const boost::system::error_code &ec) {
    std::cerr << "error: " << ec.message() << std::endl;
  });

  // ここでrun()してon_connect()が呼ばれる
  io_service.run();
}

サーバー側は静的コンテンツを返すだけなので特になにもせずにオウム返し。

./client 
HTTP/2 200
cache-control: max-age=3600
content-length: 169
date: Fri, 03 Apr 2015 04:53:19 GMT
last-modified: Tue, 31 Mar 2015 04:44:56 GMT
server: nghttpd nghttp2/0.7.10-DEV

HTTP/2 200
cache-control: max-age=3600
content-length: 169
date: Fri, 03 Apr 2015 04:53:19 GMT
last-modified: Tue, 31 Mar 2015 04:44:56 GMT
server: nghttpd nghttp2/0.7.10-DEV

<!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>

<!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>

しかしサーバー側でストリームID3で正しくPOSTリクエストをさばいてることは確認。

[id=1] [  3.077] recv (stream_id=3) :method: POST
[id=1] [  3.077] recv (stream_id=3) :scheme: http
[id=1] [  3.077] recv (stream_id=3) :path: /
[id=1] [  3.077] recv (stream_id=3) :authority: nghttp2.test:8080
[id=1] [  3.077] recv HEADERS frame <length=4, flags=0x04, stream_id=3>
          ; END_HEADERS
          (padlen=0)
          ; Open new stream

ストリームIDが次の番号が採用されて3になるようです。
ストリームID1のGETで「END_STREAM | END_HEADERS」にしている。

一つのストリームIDで基本的にはリクエストとレスポンスが1回ずつなのかな。
この辺は理解が浅いのでもう少し勉強。

SSL対応とサーバープッシュの受信

HTTP2は、あらかじめクライアントがGETしにくるであろうリソースをサーバーからプッシュすることができます。
このサーバープッシュはリクエストとの競合を防ぐために、そのリソースのパスを含むボディデータを返す前に、サーバーからPUSH_PROMISEという形で予約されます。そうすることでbodyの中身をGETしにいこうとした時にすでにプッシュが来ているというような事態を防ぎます。

#include <iostream>

#include <nghttp2/asio_http2_client.h>

using boost::asio::ip::tcp;

using namespace nghttp2::asio_http2;
using namespace nghttp2::asio_http2::client;

int main(int argc, char *argv[]) {

  // エラーコードと非同期のインターフェース
  boost::system::error_code ec;
  boost::asio::io_service io_service;

  boost::asio::ssl::context tls(boost::asio::ssl::context::sslv23);
  tls.set_default_verify_paths();

  // disabled to make development easier...
  // tls_ctx.set_verify_mode(boost::asio::ssl::verify_peer);
  configure_tls_context(ec, tls);

  // セッションオブジェクトを作成
  session sess(io_service, tls, "nghttp2.test", "8080");                                                                                                               

  // 接続 (自身のsessオブジェクトをキャプチャしてます)
  sess.on_connect([&sess](tcp::resolver::iterator endpoint_it) {

    boost::system::error_code ec;

    // リクエスト
    auto req = sess.submit(ec, "GET", "https://nghttp2.test:8080/");

    // ラムダ式でレスポンスを受け取るコールバックを指定
    req->on_response([](const response &res) {

      // status_code()でステータスコードを取得
      std::cerr << "HTTP/2 " << res.status_code() << std::endl;

      // ヘッダのマップをループする
      for (auto &kv : res.header()) {

        // キーとバリューを表示する
        std::cerr << kv.first << ": " << kv.second.value << "\n";
      }
      std::cerr << std::endl;

      // on_dataでラムダ式でDATAフレームを受け取るコールバックを指定.
      res.on_data([](const uint8_t *data, std::size_t len) {
        std::cerr.write(reinterpret_cast<const char *>(data), len);
        std::cerr << std::endl;
      });
    });

    // サーバープッシュを受け取る.
    req->on_push([](const request &push) {

      std::cerr << "push request received!" << std::endl;

      push.on_response([](const response &res) {

        std::cerr << "push response received!" << std::endl;

        res.on_data([](const uint8_t *data, std::size_t len) {
          std::cerr.write(reinterpret_cast<const char *>(data), len);
          std::cerr << std::endl;
        });
      });
    });

    // セッションがクローズした際に呼ばれる.
    req->on_close([&sess](uint32_t error_code) {
      // shutdown session after first request was done.
      sess.shutdown();
    });
  });

  // エラーの時に呼ばれる
  sess.on_error([](const boost::system::error_code &ec) {
    std::cerr << "error: " << ec.message() << std::endl;
  });

  // ここでrun()してon_connect()が呼ばれる
  io_service.run();
}

サーバーの起動

-pオプションでサーバープッシュのパスとリソースの場所を指定。
-htdocsでドキュメントルートを指定するが、指定しなかった場合はカレントがドキュメントルートになる。

nghttpd -v -d "" 8080 -p/=/style.css server.key server.crt 

結果

./client 
push request received!
HTTP/2 200
cache-control: max-age=3600
content-length: 169
date: Fri, 03 Apr 2015 05:41:05 GMT
last-modified: Tue, 31 Mar 2015 04:44:56 GMT
server: nghttpd nghttp2/0.7.10-DEV

<!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>

サーバープッシュが受け取れていない。
pushのon_response()が呼ばれていないようだ。

「push request received!」を表示しているのでPUSH_PROMISEまでは受信できている。

サーバー側の出力を見てみるとストリームID2で送信まで完了してる。
今回用意した「style.css」は67バイトで、送信してるDATAフレームもlength=67なので確かに送ってる。

[id=3] [803.311] send DATA frame <length=67, flags=0x01, stream_id=2>
          ; END_STREAM

nghttp2のAPIのドキュメントをよく読んだら、on_close()の中でsess.shutdown();してるのが原因だと判明。(コピペせずに前のサンプルに書き加えたからこの部分を削っていなかった)

このon_close()はreqに結びついてるので、たぶんストリームが閉じられた時(END_STREAMフラグを受信した後?)に呼ばれる。
なので、予約されたプッシュのDATAフレームを受信する前にshutdown()してしまっていただけだった。

無事にサーバープッシュを受信できました。

push request received!
HTTP/2 200
cache-control: max-age=3600
content-length: 169
date: Fri, 03 Apr 2015 05:55:58 GMT
last-modified: Tue, 31 Mar 2015 04:44:56 GMT
server: nghttpd nghttp2/0.7.10-DEV

<!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>

push response received!
h1 {
  font-size: 200%;
  color: #556644;
  padding: 10px 10px;
}

他には通信resume()ができたり、パスに応じて動的コンテンツを生成して返すことができる。その場合にwrite_trailer()でヘッダーを追加することも可能らしい。

nghttp2のハイレベルAPI便利です。

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5