GoでHTTP/3サーバーを試してみる

はじめに

エンジニアの松原です。今年6月にHTTP/3がRFC 9114として標準化されましたが、主要なWebフレームワークで対応するのはまだ時間がかかりそうなイメージがあります。
この背景にHTTP/3ではQUICという新しいプロトコルを利用すること、HTTP/3で通信するためにはTLSのバージョン1.3が必要など、大幅な変更を取り込む必要があるため、実開発にHTTP/3を用いるにはもう少し先になりそうです。

今回の記事ではとりあえずHTTP/3を触ってみるということを優先して、quic-goを利用してHTTP/3が試せるGoのサーバーを構築してみました。今回はGoでサーバーとクライアントのアプリケーションを簡易的に作って試してみます。

github.com

以下の記事では、go version 1.18 以降が入っていることを前提に書いています。

簡易HTTP/3サーバーを作る

まずは quic-goをインストールするために、以下のコマンドを実行します。

go mod init server
go get github.com/lucas-clemente/quic-go

上記コマンドにより、go.mod と go.sum というファイルが作成され、パッケージの依存解決してくれるようになります。 次に main.go ファイルを作成し、以下のように記述します。 ./localhost.crt./localhost.key に関しては、こちらのQiitaの記事(OpenSSLコマンドの備忘録)を参考にしました。

package main

import (
    "fmt"
    "log"
    "net/http"

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

func main() {
    fmt.Println("The server is started.")

    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Printf("remote: %s\n", req.RemoteAddr)
        fmt.Fprint(w, "hello world!")
    })

    err := http3.ListenAndServeQUIC(
        "localhost:18443",
        "./localhost.crt",
        "./localhost.key",
        mux,
    )
    if err != nil {
        log.Fatal(err)
    }
}

qiita.com

サーバー側は以下のコマンドで実行します。実行後は立ち上がりっぱなしになるため、終了したい場合はCtrl+Cを押します。

go run main.go

簡易HTTP/3クライアントを作る

サーバーと同様、簡易のクライアントを作成します。サーバーとは異なるディレクトリで作成していきます。
サーバーアプリと同じように quic-goをインストールするために、以下のコマンドを実行します。

go mod init client
go get github.com/lucas-clemente/quic-go

次に main.go ファイルを作成し、以下のように記述します。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"

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

func main() {
    rt := http3.RoundTripper{}
    defer rt.Close()
    client := &http.Client{
        Transport: &rt,
    }

    res, err := client.Get("https://localhost:18443/")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("protocol: %s\n", res.Proto)
    fmt.Printf("status code: %d\n", res.StatusCode)

    body, _ := ioutil.ReadAll(res.Body)
    fmt.Printf("body: %s\n", string(body))
}

実行してみる

簡易HTTP/3クライアントを実行します。

go run main.go

うまくいけば以下のようなメッセージが標準入出力に出てくると思います。

protocol: HTTP/3.0
status code: 200
body: hello world!

簡単な解説

サーバー側実装

今回はquic-goを利用して簡易的なHTTP/3サーバーを構築しています。以下のコードは、Go公式のhttp.ListenAndServeTLS()をquic-goの機能に置き換えて、HTTP/3サーバーとして動作させています。このメソッドを利用するとサーバーがTCPではなく、UDPで待ち受けるようになるため、クライアント側のHTTP/3用のプロトコル対応が必須になります。

   err := http3.ListenAndServeQUIC(
        "localhost:18443",
        "./localhost.crt",
        "./localhost.key",
        mux,
    )

クライアント側実装

クライアント側では上記の設定に対応するために、Go公式のhttp.ClientTransport にquic-goで用意されている http3.RoundTripperを指定することで、HTTP/3対応のHTTPクライアントとして動作させています。ただし、HTTP/1.1やHTTP/2には対応しなくなるため、HTTP/3非対応のページは読み込めなくなります(www.google.comはHTTP/3対応のため読み込めますが、www.yahoo.co.jpなどHTTP/2のサイトはエラーが発生します)。

   rt := http3.RoundTripper{}
    defer rt.Close()
    client := &http.Client{
        Transport: &rt,
    }

まとめ

この記事では、quic-goを利用してHTTP/3のサーバークライアントのHelloWorldを体験してみました。quic-goを使うと非常に簡素に書けるので、あまりGoに触り慣れていない方にもコードは追いやすくなっていると思います。

まだ実サービスレベルではHTTP/3は扱いづらい印象はありますが、gRPCがHTTP/3(QUIC)対応に動いているところを見ても、今年から来年にかけて利用する機会が増えてくるのではないかと思います。 次回以降はgRPCも試して、記事にできればと思います。