curl エクステンションで HTTP/2 リクエストを送信する

  • 50
    Like
  • 2
    Comment

curl のビルドオプション

システムにインストールされている curl のバージョンが 7.3.3 とそれ以降であり、0.6.0 とそれ以降の nghttp2 がシステムにインストールされている状態でビルドオプションに --with-nghttp2=PATH が指定されていれば、PHP の curl エクステンションで HTTP/2 が利用できます。

./configure --prefix=`pwd`/local --with-nghttp2

curl version:     7.42.0-DEV
Host setup:       x86_64-apple-darwin14.1.0
Install prefix:   /Users/masakielastic/test/curl/local
Compiler:         gcc
SSL support:      enabled (OpenSSL)
SSH support:      no      (--with-libssh2)
zlib support:     enabled
GSS-API support:  no      (--with-gssapi)
TLS-SRP support:  no      (--enable-tls-srp)
resolver:         default (--enable-ares / --enable-threaded-resolver)
IPv6 support:     enabled
Unix sockets support: enabled
IDN support:      enabled
Build libcurl:    Shared=yes, Static=yes
Built-in manual:  enabled
--libcurl option: enabled (--disable-libcurl-option)
Verbose errors:   enabled (--disable-verbose)
SSPI support:     no      (--enable-sspi)
ca cert bundle:   no
ca cert path:     no
LDAP support:     enabled (OpenLDAP)
LDAPS support:    enabled
RTSP support:     enabled
RTMP support:     no      (--with-librtmp)
metalink support: no      (--with-libmetalink)
HTTP2 support:    enabled (nghttp2)
Protocols:        DICT FILE FTP FTPS GOPHER HTTP HTTPS IMAP IMAPS LDAP LDAPS POP3 POP3S RTSP SMB SMBS SMTP SMTPS TELNET TFTP

curl -V を実行すれば、nghttp2 のビルドオプションが有効であるかを調べることができます。

curl 7.41.0 (x86_64-apple-darwin14.1.0) libcurl/7.41.0 OpenSSL/1.0.2a zlib/1.2.5 nghttp2/0.7.7
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp 
Features: IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets

curl コマンドによるリクエスト

次のコマンドを実行してレスポンスを目で確認してみましょう。

curl --http2 -v https://http2bin.org/get

ヘッダーだけ出力することもできます。

curl -I --http2 https://http2bin.org/get

結果は次のとおりです。「HTTP/2.0 200」が含まれていることを確認します。

*   Trying 104.131.161.90...
* Connected to http2bin.org (104.131.161.90) port 443 (#0)
* ALPN, offering h2-14, http/1.1
* successfully set certificate verify locations:
*   CAfile: /usr/local/etc/openssl/cert.pem
  CApath: none
* NPN, negotiated HTTP2 (h2-14)
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*    subject: OU=Domain Control Validated; OU=EssentialSSL; CN=www.http2bin.org
*    start date: 2015-02-21 00:00:00 GMT
*    expire date: 2016-02-21 23:59:59 GMT
*    issuer: C=GB; ST=Greater Manchester; L=Salford; O=COMODO CA Limited; CN=COMODO RSA Domain Validation Secure Server CA
*    SSL certificate verify ok.
* Using HTTP2
* http2_send len=54
* before_frame_send() was called
* on_frame_send() was called
* before_frame_send() was called
* on_frame_send() was called
> GET /get HTTP/1.1
Host: http2bin.org
Accept: */*

* http2_recv: 16384 bytes buffer
* nread=27
* on_frame_recv() was called with header 4
* nghttp2_session_mem_recv() returns 27
* before_frame_send() was called
* on_frame_send() was called
* http2_recv: 16384 bytes buffer
* nread=9
* on_frame_recv() was called with header 4
* nghttp2_session_mem_recv() returns 9
* http2_recv: 16384 bytes buffer
* nread=345
* on_begin_headers() was called
* got http2 header: server: h2o/1.1.1
* got http2 header: date: Mon, 16 Mar 2015 21:47:58 GMT
* got http2 header: content-type: application/json
* got http2 header: access-control-allow-origin: *
* got http2 header: access-control-allow-credentials: true
* got http2 header: x-clacks-overhead: GNU Terry Pratchett
* on_frame_recv() was called with header 1
* on_data_chunk_recv() len = 218, stream = 1
* 218 data written
* on_frame_recv() was called with header 0
* on_stream_close() was called, error_code = 0
* nghttp2_session_mem_recv() returns 345
< HTTP/2.0 200
< server:h2o/1.1.1
< date:Mon, 16 Mar 2015 21:47:58 GMT
< content-type:application/json
< access-control-allow-origin:*
< access-control-allow-credentials:true
< x-clacks-overhead:GNU Terry Pratchett
< 
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Connection": "keep-alive", 
    "Host": "http2bin.org", 
    "Via": "2 http2bin.org"
  }, 
  "origin": "106.184.75.63", 
  "url": "https://http2bin.org/get"
}
* Connection #0 to host http2bin.org left intact

インストール

homebrew

あらかじめ最新の OpenSSL が使えるようにしておきます。brew link コマンドを実行しておかないと、Mac OS X にインストールされてあるデフォルトの OpenSSL が使われます。

brew update
brew install openssl
brew link openssl --force
openssl version

curl をインストールする際のビルドオプションに --with-nghttp2 を指定します。

brew install curl --with-nghttp2 --with-openssl
brew link curl --force

以前の公式 fomula ではビルドオプションが利用できなかったのと最新バージョンを試したかったので、自分の formula を用意していました。次のコマンドでインストールできます。

brew update
brew install masakielastic/http2/curl --with-openssl
brew link curl --force

linuxbrew

主要な Linux ディストリビューションで個人のディレクトリの領域に curl をインストールして試すのであれば、パッケージ管理ツールの linuxbrew が便利です。linuxbrew は Mac OSX の homebrew の Linux 対応バージョンです。Ubunutu に導入した際に次のパッケージを追加で導入する必要がありました。

sudo apt-get install pkg-conf python-setuptools

次のコマンドで curl がインストールされます。

brew install curl --with-nghttp2

Ubuntu 16.04 LTS

再コンパイルすることで HTTP/2 を利用できるようになります。launchpad に 手順 が記されています。

最初に nghttp2 とビルドに必要なツールをインストールします。

sudo apt install libnghttp2-dev
sudo apt build-dep curl

次にソースコードをダウンロードします。

apt source curl
cd curl-7.47.0

debian/control を編集して Build-Depends の項目で libnghttp2-dev を追加します。

Build-Depends: debhelper (>= 9),
 autoconf,
 automake,
 ca-certificates,
 groff-base,
 libgnutls-dev,
 libidn11-dev,
 libkrb5-dev,
 libldap2-dev,
 libnss3-dev,
 librtmp-dev (>= 2.4+20131018.git79459a2-3~),
 libssl-dev,
 libnghttp2-dev,
...

ビルドしてインストールします。テストに5分から10分ほどかかります。

sudo debian/rules binary
sudo dpkg -i ../*deb

CURL_HTTP_VERSION_2_0 の定義

HTTP/2 リクエストを送信するために必要なことは CURLOPT_HTTP_VERSION に対応する値として CURL_HTTP_VERSION_2_0 を指定することだけです。PHP 5.6.8、5.5.24 系から使えるようになります。それ以前のバージョンでは CURL_HTTP_VERSION_2_0 を自分で定義する必要があります。

if (!defined('CURL_HTTP_VERSION_2_0')) {
    define('CURL_HTTP_VERSION_2_0', CURL_HTTP_VERSION_1_1 + 1);
}

なお、curl/curl.h では CURL_HTTP_VERSION_2_0 は次のように enum で定義されています。

enum {
  CURL_HTTP_VERSION_NONE, /* setting this means we don't care, and that we'd
                             like the library to choose the best possible
                             for us! */
  CURL_HTTP_VERSION_1_0,  /* please use HTTP 1.0 in the request */
  CURL_HTTP_VERSION_1_1,  /* please use HTTP 1.1 in the request */
  CURL_HTTP_VERSION_2_0,  /* please use HTTP 2.0 in the request */

  CURL_HTTP_VERSION_LAST /* *ILLEGAL* http version */
};

curl エクステンション

if (!defined('CURL_HTTP_VERSION_2_0')) {
    define('CURL_HTTP_VERSION_2_0', CURL_HTTP_VERSION_1_1 + 1);
}

$url = 'https://http2bin.org/get';

$opts = [
    CURLOPT_VERBOSE => true,
    CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
    CURLOPT_SSL_VERIFYHOST => false,
    CURLOPT_SSL_VERIFYPEER => false
];

$ch = curl_init($url);
curl_setopt_array($ch, $opts);
curl_exec($ch);
curl_close($ch);

Guzzle

バージョン 5 を対象とします。

use GuzzleHttp\Client;

if (!defined('CURL_HTTP_VERSION_2_0')) {
    define('CURL_HTTP_VERSION_2_0', CURL_HTTP_VERSION_1_1 + 1);
}

$client = new Client();
$client
  ->get(
    'https://http2bin.org/get', [
    'future' => true,
    'config' => [
        'curl' => [
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0
        ]
    ],
    'debug' => true
    ]
  )
  ->then(function($response) {
    var_dump(
      $response->getStatusCode(),
      $response->getHeader('Content-Type'),
      (string) $response->getBody()
    );
  });

RingPHP

use GuzzleHttp\Ring\Client\CurlHandler;

if (!defined('CURL_HTTP_VERSION_2_0')) {
    define('CURL_HTTP_VERSION_2_0', CURL_HTTP_VERSION_1_1 + 1);
}

$handler = new CurlHandler();
$response = $handler([
    'http_method' => 'GET',
    'scheme'      => 'https',
    'uri'         => '/get',
    'headers' => [
        'host' => ['http2bin.org']
    ],
    'client' => [
        'curl' => [
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0
        ],
        'debug' => true
    ]
]);

$response->then(function (array $response) {
    var_dump($response);
});

HTTP/2 が利用できるかの判定方法

HTTP/2 が利用できるかどうかで curl のオプションの構成を変えたい場合、curl_version を使います。

if (!defined('CURL_VERSION_HTTP2')) {
    define('CURL_VERSION_HTTP2', 1<<16);
}

if (curl_version()['features'] & CURL_VERSION_HTTP2) {
    echo "HTTP/2 は利用できます。\n";
} else {
    echo "HTTP/2 は利用できません。\n";
}

CURL_VERSION_HTTP2curl/curl.h で定義されています。

#define CURL_VERSION_HTTP2        (1<<16) /* HTTP2 support built-in */
#define CURL_VERSION_GSSAPI       (1<<17) /* Built against a GSS-API library */
#define CURL_VERSION_KERBEROS5    (1<<18) /* Kerberos V5 auth is supported */
#define CURL_VERSION_UNIX_SOCKETS (1<<19) /* Unix domain sockets support */

HTTP/2 対応のサーバーを利用する

h2o

h2o バージョン 1.3 から php-fpm に対応しています。Mac OS X であれば、homebrew を使ってインストールおよび起動できます。設定ファイルのパスは /usr/local/etc/h2o/h2o.conf で、ポート番号は 8080 です。

brew install h2o
ln -sfv /usr/local/opt/h2o/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.h2o.plist
h2o.conf
file.custom-handler:
  extension: .php
  fastcgi.connect:
    host: 127.0.0.1
    port: 9000
    type: tcp
listen:
  port: 8080
  ssl:
    certificate-file: /path/to/server.crt
    key-file: /path/to/server.key
hosts:
  "127.0.0.1.xip.io:8080":
    paths:
      /:
        file.dir: /usr/local/var/h2o/

Ubuntu の php-fpm を利用する場合、Unix ソケット形式で書く必要がありました。

file.custom-handler:
  extension: .php
  fastcgi.connect:
    port: /var/run/php5-fpm.sock
    type: unix

PHP の開発バージョンを試すためにシステムにあらかじめ導入されている php-fpm 以外の Fast CGI のプロセスが必要であれば、h2o に生成と管理を任せることができます。次の設定は Ubuntu で確認しました。

file.custom-handler:
  extension: .php
  fastcgi.spawn:
    command: "PHP_FCGI_CHILDREN=10 exec /usr/bin/php-cgi"
    user: nobody

nginx

nginx 1.9.5 から利用できるようになりました。ソースコードからビルドする場合、--with-http_v2_module オプションを指定します。Debian/Ubuntu の場合、nginx.org のリポジトリを利用することができます。/etc/apt/sources.list に次のようなコードを追加します。

deb http://nginx.org/packages/ubuntu/ trusty nginx
deb-src http://nginx.org/packages/ubuntu/ trusty nginx

listen ディレクティブに指定します。

server {
    listen 443 ssl http2 default_server;

    ssl_certificate    server.crt;
    ssl_certificate_key server.key;

    #...
}

Apache

Apache 2.4.17 で mod_http2 モジュールが利用できるようになりました。ビルドオプションに --enable-http2 を指定します。Ubuntu の場合、PPA が利用できます。

sudo add-apt-repository -y ppa:ondrej/apache2

設定ファイルに Protocols ディレクティブを追加します。

# /etc/apache2/sites-available/000-default.conf
Protocols h2c http/1.1

# /etc/apache2/sites-available/default-ssl.conf
Protocols h2 http/1.1

リバースプロキシーによる HTTP/2 対応

ポート番号 80 で HTTP1 のサーバーが起動しており、ポート番号 8080 から HTTP2 クライアントでアクセスできるようにしてみましょう。

nghttpx

nghttpx であれば、次のコマンドを実行します。SSL/TLS の証明書はあらかじめ用意してあることが前提です。

nghttpx -f0.0.0.0,8080 -b127.0.0.1,80 server.key server.crt

h2o

h2o の場合、次のような設定ファイルを用意します。

proxy.conf
listen:
  port: 8080
  ssl:
    certificate-file: examples/h2o/server.crt
    key-file: examples/h2o/server.key
hosts:
   default:
    paths:
      /:
        proxy.reverse.url: http://127.0.0.1:80/
    access-log: /dev/stdout

h2o を起動させてみましょう。

h2o -c proxy.conf

CDN サービス

CloudFlare は HTTP/2 に対応しており、独自の SSL/TLS を設定できない共有ホスティングサービスで利用できます。私はウェブクロウの無料プランで HTTP/2 通信を確認しました (参考記事)。

2016年4月にサーバープッシュに対応されました。PHP のサンプルコードをブログの記事で紹介されています。

共有ホスティングサービス

2016年5月の時点で日本国内の業者は対応していません。ロリポップが今後の対応を宣言しています。海外では A2 HostingSiteGroundが対応しています。