【Go】HTTP/2とHTTP/3を試してみて感じたこと

はじめに


この記事は【カヤック】面白法人グループ Advent Calendar 2024の22日目の記事です。

こんにちは、カヤックボンド所属のサーバーサイドエンジニアの有馬と申します。

本記事のテーマは2022年6月6日に標準化されたHTTP/3についてです。 業務内でHTTP/2のソケット通信について触れる機会があり、「そういえば、HTTP/2とかHTTP/3についてあまり知らないな~」と実感したため、 本記事を書きたいと思いました。

本記事ではO'Reilly Japanさんの「Real World HTTP」 を参考にし、実際にコードを実装してみた所感を書いていきます。

www.oreilly.co.jp

HTTPの簡単な歴史について

2015年にHTTP/1.1からバージョンアップしたHTTP/2が正式な仕様となりました。

HTTP/1.1がRFC(インターネット技術の標準的な仕様を記した文書)になったのは1999年ですので、 16年越しのバージョンアップとなりました。

このHTTP/2は、ほぼHTTP代替プロトコルとしてGoogleが開発した「SPDY」が標準化された形となりました。 このSPDYの目的はHTTPの転送速度を一段と向上させることにあり、導入効果として30%から3倍以上の効果を発揮します。

また、2013年には同じくGoogleからHTTP/3の草案となる「QUIC」というプロトコルが公表されました。 このQUICですが2015年にRFC化のために最初の提案を行ってから、いくつかの変更がされ、 2022年6月にHTTP/3として発行されました。

簡単なHTTPの歴史

lcoalhostでHTTPS通信のための前準備

HTTPSに必要な証明書などを作成するツールmkcertを使用します。 本記事ではWindows環境のため、Chocolatey を使用して、以下のコマンドでインストールします。

choco install mkcert

次に以下のコマンドでローカル認証局を作成します。

mkcert -install

最後に、Go言語のファイルを格納するディレクトリに移動して、以下のコマンドを実行します。

mkcert localhost

これでlocalhostというホスト名に対する証明書(localhost.pem)と秘密鍵(localhost-key.pem)を作成できます。

HTTP/2のGo実装

それでは実際にGo言語でHTTP/2通信を実装しましょう。 といってもGo言語は標準でHTTP/2サポートが組み込まれているので、HTTPS通信を行う設定にするだけで簡単に実現可能です。

package main

import (
    "fmt"
    "net/http"
)

func main() {
    resp, err := http.Get("https://google.com/")

    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    fmt.Printf("Protocol Version: %s\n", resp.Proto)
}

"Protocol Version: HTTP/2.0"の文字列が出力されればOKです。 なお、GODEBUGの"http2client=0"を指定して、 srcを実行すれば、HTTP/1.1の通信が行えます。

HTTP/3のGo実装

続いて、HTTP/3の実装となります。 現時点(2024/12月) ではGo本体はHTTP/3やQUICそのものに対応していませんが、

今後、Go本体の標準ライブラリにQUICやHTTP/3の対応が追加されていく方針となっています。

準標準ライブラリのgolang.org/x/net/quicで育ててAPIを確定させ、 その後にnet/quicとなっていく予定です。

そのため、今すぐにGo言語でHTTP/3やQUICで通信したい場合、"github.com/quic-go/quic-go"パッケージを利用して、 HTTP/3専用サーバーを使うコードを実装しましょう

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync"

    "github.com/quic-go/quic-go/http3"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Add("Alt-Svc", `h3=":443"; ma=2592000`)
        fmt.Fprintf(w, "Hello via, %s\n", r.Proto)
    })

    ctx, close := signal.NotifyContext(context.Background(), os.Interrupt)
    defer close()

    h2server := &http.Server{
        Addr:    "0.0.0.0:8443",
        Handler: mux,
    }
    h3server := &http3.Server{
        Addr:    "0.0.0.0:8443",
        Handler: mux,
    }

    wg := &sync.WaitGroup{}
    wg.Add(2)

    go func() {
        log.Println("start at http/2 server at (TCP)https://localhost:8443")
        log.Println(h2server.ListenAndServeTLS("localhost.pem", "localhost-key.pem"))
        wg.Done()
    }()

    go func() {
        log.Println("start at http/3 server at (UDP)https://localhost:8443")
        log.Println(h3server.ListenAndServeTLS("localhost.pem", "localhost-key.pem"))
        wg.Done()
    }()

    <-ctx.Done()

    h2server.Shutdown(ctx)
    h3server.Close()
}

現在のブラウザはURL入力をするとHTTP/1.1かHTTP/2での接続をしにいってしまうため、 単独でHTTP/3サーバーだけ起動してもエラーになってしまいます。

そこで一旦TCPソケットで待ち受けるHTTP/2サーバーとUDPソケットで待ち受けるHTTP/3のサーバーの両方を起動する必要があります。 さらにもう一つのポイントはAlt-Svcヘッダーフィールドを使って、同一ポートでHTTP/3のサーバーも起動していることをブラウザに伝える必要もあるのです。

使用しているブラウザによってはAlt-SvcヘッダーフィールドだけではHTTP/3接続にならない場合もありますので、 使用ブラウザごとにHTTP/3の設定確認が要ります。

HTTP/2とHTTP/3での相違点

HTTP/2とHTTP/3の大きな相違点は、トランスポート層の違いでしょう。

古いシステムの改修を行う際に仕様書に記載がないが裏で独自ソケットによるTCP通信をしていたが、 そんな事も知らずに目新しいHTTP/3でシステム改修しようとして酷い目にあう可能性がありそうで怖い。。

ほかの相違点としてQUICトランスポートかどうかもありますが、 これもトランスポート層の違いによるところが大きいのであまり意識しなくてよいという認識です。

実際に実装してみた所感

実際にHTTP/2とHTTP/3を実装してみた感想として、 HTTP/3用にヘッダーを設定してブラウザ側の設定もいじって、、

と色々準備が必要なのですが、HTTP/2の方がパフォーマンスが高まるという記事が出たりと、実装コストに見合う効果が出る、と言えない部分もありそうなので実務に取り入れるかどうかは慎重に検討すべきかなというのが正直な感想です。

また、HTTP/2が標準化されて約10年が経ちますが、現在でもHTTP/1.1を使う場面もありますので、 劇的にパフォーマンス向上が見込めるような事態にならない限りはHTTP/2を使っていくでよさそう。


以上、ご覧いただきありがとうございました。

カヤックボンドでは一緒に技術力を高め合えるエンジニアを募集しています!

kayac.bond