背景
外部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.ResponseのProto
フィールドを出力すれば、以下のように分かります。
$ 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を使う方法についてです。
主に
- http2.Transportを使う
- http.TransportでForceAttemptHTTP2をtrueにする
- 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を有効にしたい場合はForceAttemptHTTP2
をtrue
にします。
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
なので
- Goのnet/httpのkeep-aliveで気をつけること - Carpe Diem で推奨したようなカスタムhttp.Transportを使う
- 独自のルート証明書を使う
といった際にfalse
にならないよう、設定漏れに注意しましょう。
DialContextなどを使わない場合
設定しない場合HTTP/2はデフォルトで有効になります。
なので勘違いしやすいですが、ForceAttemptHTTP2
がfalse
でも以下のような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
が設定されていますが、ForceAttemptHTTP2
はtrue
になっているので問題ありません。
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になります。
しかしながら
で説明したようにタイムアウトが無制限なので、基本的にhttp.DefaultClient
は使うべきじゃありません。
サンプルコード
今回使ったサンプルコードはこちら↓
まとめ
goのhttp clientでHTTP/2に対応する方法を説明しました。
特にパラメータを気にしていなければ勝手にHTTP/2対応されていたでしょうし、逆にkeepaliveなどのためパラメータを細かく設定しているとForceAttemptHTTP2
が未設定で意図せずHTTP/1.1になってしまう罠があることが分かりました。