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


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

```bash
./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 のビルドオプションが有効であるかを調べることができます。

```bash
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 コマンドによるリクエスト

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

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

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

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

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

```bash
*   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 が使われます。

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

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

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

以前の公式 fomula ではビルドオプションが利用できなかったのと最新バージョンを試したかったので、自分の [formula](https://github.com/masakielastic/homebrew-http2) を用意していました。次のコマンドでインストールできます。

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

### linuxbrew

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


```bash
sudo apt-get install pkg-conf python-setuptools
```

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

```
brew install curl --with-nghttp2
```

### Ubuntu 16.04 LTS

再コンパイルすることで HTTP/2 を利用できるようになります。launchpad に [手順](http://t.umblr.com/redirect?z=https%3A%2F%2Fbugs.launchpad.net%2Fubuntu%2F+source%2Fcurl%2F+bug%2F1567697%2Fcomments%2F2&t=YTE2NjU4NjQ5NWNhZmVhZjE3MDQ4YzgwYmEzZTNiYWM4YWY3Y2NmYyw2eVJWVWxIWg%3D%3D) が記されています。

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

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

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

```bash
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分ほどかかります。

```bash
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` を自分で定義する必要があります。

```php
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 で定義されています。

```c
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 エクステンション

```php
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 を対象とします。

```php
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


```php
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` を使います。

```php
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_HTTP2` は [curl/curl.h](https://github.com/bagder/curl/blob/0b1f37e77cc091456f32a8258a261b5a790ebf46/include/curl/curl.h#L2273) で定義されています。

```c
#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` です。

```bash
brew install h2o
ln -sfv /usr/local/opt/h2o/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.h2o.plist
```

```bash: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 ソケット形式で書く必要がありました。

```bash
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` に次のようなコードを追加します。

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

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

```bash
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 が利用できます。

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

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

```bash
# /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 の証明書はあらかじめ用意してあることが前提です。

```bash
nghttpx -f0.0.0.0,8080 -b127.0.0.1,80 server.key server.crt
```
### h2o
h2o の場合、次のような設定ファイルを用意します。

```bash: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 通信を確認しました ([参考記事](http://qiita.com/masakielastic/items/f19b13fcae0f3ac41ce0))。

2016年4月にサーバープッシュに対応されました。PHP のサンプルコードをブログの[記事](https://blog.cloudflare.com/using-http-2-server-push-with-php/)で紹介されています。

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

2016年5月の時点で日本国内の業者は対応していません。ロリポップが今後の対応を[宣言](https://lolipop.jp/nextgen/)しています。海外では [A2 Hosting](https://www.a2hosting.com/http2-hosting) や [SiteGround](https://www.siteground.com/blog/http2-supported/)が対応しています。