Carpe Diem

備忘録

http clientでHTTP/2を使う方法

背景

外部APIを叩く時に利用するhttp clientですが、サーバ側がHTTP/2対応しているのであればコネクションの有効活用ができるようHTTP/2を使いたいものです。

その際にhttp client側で設定する点、気をつける点を説明していきます。

環境

  • Go 1.15.6
  • curl 7.64.1

HTTP/2対応しているか確認する方法

まずは対象とするサーバやhttp clientがHTTP/2対応になっているかを確認する方法を紹介します。

サーバ側の確認

サーバ側が対応しているかどうかはcurl-vオプションで手軽に確認できます。

$ curl -v https://google.com
*   Trying 2404:6800:4004:809::200e...
* TCP_NODELAY set
* Connected to google.com (2404:6800:4004:809::200e) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
...
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
...
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7f9a5e808200)
> GET / HTTP/2
> Host: google.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 301
< location: https://www.google.com/
< content-type: text/html; charset=UTF-8
...

ALPNでTLSハンドシェイク開始時にクライアント側が利用可能なプロトコル一覧(今回だとh2とhttp/1.1)をオファーし、

* ALPN, offering h2
* ALPN, offering http/1.1

サーバ側はh2で承諾しています。

* ALPN, server accepted to use h2

また以下のように各所でHTTP/2が見られます。

* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
> GET / HTTP/2
< HTTP/2 301

http clientがHTTP/2を利用したかどうかの確認

クライアント側のリクエストがHTTP/2で処理されたかどうかを確認する方法です。

req, err := http.NewRequest("GET", url, nil)
if err != nil {
        panic(err)
}
req = req.WithContext(ctx)

resp, err := client.Do(req)
if err != nil {
        return err
}
defer resp.Body.Close()
fmt.Println("Protocol:", resp.Proto) // ココ
return nil

このようにhttp.ResponseProtoフィールドを出力すれば、以下のように分かります。

$ go run main.go https://www.google.com
Connecting to https://www.google.com...
Protocol: HTTP/2.0

未対応サイトに接続すると↓

$ go run main.go http://christina04.blog.fc2.com
Connecting to http://christina04.blog.fc2.com...
Protocol: HTTP/1.1

GODEBUG=http2debug=2を使う

GODEBUG=http2debug=2を使うとHTTP/2の通信を標準出力に出してくれるので、こちらでも簡単に確認できます。

$ GODEBUG=http2debug=2 go run main.go https://www.google.com
Connecting to https://www.google.com...
2021/01/09 07:41:11 http2: Transport failed to get client conn for www.google.com:443: http2: no cached connection was available
2021/01/09 07:41:12 http2: Transport creating client conn 0xc000001680 to [2404:6800:4004:811::2004]:443
2021/01/09 07:41:12 http2: Framer 0xc00038f880: wrote SETTINGS len=18, settings: ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=4194304, MAX_HEADER_LIST_SIZE=10485760
2021/01/09 07:41:12 http2: Framer 0xc00038f880: wrote WINDOW_UPDATE len=4 (conn) incr=1073741824
2021/01/09 07:41:12 http2: Transport encoding header ":authority" = "www.google.com"
2021/01/09 07:41:12 http2: Transport encoding header ":method" = "GET"
2021/01/09 07:41:12 http2: Transport encoding header ":path" = "/"
2021/01/09 07:41:12 http2: Transport encoding header ":scheme" = "https"
2021/01/09 07:41:12 http2: Transport encoding header "accept-encoding" = "gzip"
2021/01/09 07:41:12 http2: Transport encoding header "user-agent" = "Go-http-client/2.0"
...

HTTP/1.1サイトだと出力されません。

$ GODEBUG=http2debug=2 go run main.go http://christina04.blog.fc2.com/
Connecting to http://christina04.blog.fc2.com/...
Protocol: HTTP/1.1

http clientでHTTP/2を使う方法

では本題のHTTP/2を使う方法についてです。

主に

  1. http2.Transportを使う
  2. http.TransportでForceAttemptHTTP2をtrueにする
  3. DefaultClientを使う

の3通りあります。それぞれ説明します。

1. http2.Transportを使う

検索するとよく出てくる方法です。

以下のようにTransportをhttp2.Transportにします。

cli1 = &http.Client{
        Transport: &http2.Transport{},
}

こうすれば強制的にHTTP/2を使うようになります。

$ go run main.go https://google.com
Connecting to https://google.com...
Protocol: HTTP/2.0

しかし逆に言うとHTTP/2対応していなければコケてしまいます。

$ go run main.go http://christina04.blog.fc2.com/
Connecting to http://christina04.blog.fc2.com/...
panic: Get "http://christina04.blog.fc2.com/": http2: unsupported scheme

なので

  • サーバ側が何らかの理由でHTTP/2→HTTP/1.1になった場合にコケる
  • http clientを汎用的に利用できない

といった課題があります。

2. http.TransportでForceAttemptHTTP2をtrueにする

ForceAttemptHTTP2はGo 1.13から入ったパラメータです。

http.Transportで

  • Dial
  • DialTLS
  • DialContext
  • TLSClientConfig

設定した場合、デフォルトではHTTP/2は無効になります。

設定してもHTTP/2を有効にしたい場合はForceAttemptHTTP2trueにします。

cli2 = &http.Client{
        Transport: &http.Transport{
                DialContext: (&net.Dialer{
                        Timeout:   30 * time.Second,
                        KeepAlive: 10 * time.Second,
                        DualStack: true,
                }).DialContext,
                ForceAttemptHTTP2: true,
        },
}

HTTP/2対応サイトではHTTP/2.0で接続し、

$ go run main.go https://www.google.com
Connecting to https://www.google.com...
Protocol: HTTP/2.0

そうでなければHTTP/1.1を使います。

$ go run main.go http://christina04.blog.fc2.com/
Connecting to http://christina04.blog.fc2.com/...
Protocol: HTTP/1.1

DialContextなどを使っているのにfalseにすると

falseで設定したclientだと、サーバ側が対応していても必ずHTTP/1.1になってしまいます。

cli2 = &http.Client{
        Transport: &http.Transport{
                DialContext: (&net.Dialer{
                        Timeout:   30 * time.Second,
                        KeepAlive: 10 * time.Second,
                        DualStack: true,
                }).DialContext,
                ForceAttemptHTTP2: false,
        },
}
$ go run main.go https://www.google.com
Connecting to https://www.google.com...
Protocol: HTTP/1.1

なので

といった際にfalseにならないよう、設定漏れに注意しましょう。

DialContextなどを使わない場合

設定しない場合HTTP/2はデフォルトで有効になります。

なので勘違いしやすいですが、ForceAttemptHTTP2falseでも以下のようなhttp clientであればHTTP/2は有効です。

cli2 = &http.Client{
        Transport: &http.Transport{
                ForceAttemptHTTP2: false,
        },
}
$ go run main.go https://www.google.com
Connecting to https://www.google.com...
Protocol: HTTP/2.0

DefaultTransportは?

DefaultTransportではDialContextが設定されていますが、ForceAttemptHTTP2trueになっているので問題ありません。

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }).DialContext,
    ForceAttemptHTTP2:     true,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

3. DefaultClientを使う

http.DefaultClientではDefaultTransportを使うので特に設定しなくてもサイトが対応していればHTTP/2になります。

しかしながら

christina04.hatenablog.com

で説明したようにタイムアウトが無制限なので、基本的にhttp.DefaultClientは使うべきじゃありません

サンプルコード

今回使ったサンプルコードはこちら↓

github.com

まとめ

goのhttp clientでHTTP/2に対応する方法を説明しました。

特にパラメータを気にしていなければ勝手にHTTP/2対応されていたでしょうし、逆にkeepaliveなどのためパラメータを細かく設定しているとForceAttemptHTTP2が未設定で意図せずHTTP/1.1になってしまう罠があることが分かりました。

参考