every Tech Blog

株式会社エブリーのTech Blogです。

WebTransportをGoで試してみる

この記事は every Tech Blog Advent Calendar 2025 の 12日目の記事です。

はじめに

エブリーでデリッシュキッチンの開発をしている本丸です。
日々、GeminiやClaudeCodeに支えられて業務を行っているのですが、利用する中でチャットのような双方向の通信について気になりました。双方向通信にはいくつか種類がありますが、今回は双方向通信の1つであるWebTransportについてまとめていければと思います。

WebTransportとは

WebTransportは、QUIC(HTTP/3)プロトコルをベースとしたクライアントとサーバー間の双方向通信を実現するAPIです。

WebTransportには以下のような特徴があります。

  • QUIC基盤: UDP上で動作するQUICプロトコルを使用
  • 多重ストリーミング: 単一接続で複数のストリームを並列処理
  • ヘッドオブライン阻害回避: QUICプロトコルを利用するため、パケット損失で他のパケットを待たせることがない
  • 接続移行: QUICプロトコルを利用するため、ネットワーク変更時も接続継続が可能
  • 信頼性/非信頼性の選択: ストリーム(信頼性あり)・データグラム(信頼性なし)の使い分けができる

WebTransportの通信フロー

WebTransportの通信は以下のフェーズに分かれて動作します。

セッション確立の詳細

  1. QUIC接続確立

    • UDP上でQUICプロトコルによる接続確立
    • TLS 1.3による暗号化ハンドシェイク
  2. WebTransportセッション開始

    • HTTP/3のCONNECTメソッドを使用
    • protocol="webtransport"ヘッダーで識別
    • サーバーが200 OKで応答すればセッション確立
  3. データ通信

    • Reliable Streams: 順序保証・到達保証あり
    • Unreliable Datagrams: 低遅延優先・順序保証なし

双方向通信の種類

WebTransportが出現するまでにも双方向通信を実現する技術は存在しており、そちらについても軽く触れていけばと思います。一部、サーバーからクライアントへの単方向通信も含みます。

HTTP Long Polling

従来のHTTPではクライアントからのリクエストに対してサーバーからレスポンス返すため、サーバーから能動的にデータを送信することができませんでした。 あらかじめクライアントからサーバーにリクエストを送信しておき、サーバーが任意のタイミングでレスポンスを返せるようにすることで、双方向通信を実現するのがLong pollingです。

Server-Sent Events (SSE)

HTTPプロトコルを利用して、サーバーからクライアントに向けての単方向通信を実現します。レスポンスのMIMEタイプにtext/event-streamを指定することで接続を維持します。

WebSocket

Long PollingやSSEでも双方向通信を実現することはできますが、それらに利用されているHTTPが双方向通信を目的として作成されたプロトコルではなかったため、双方向通信で利用しやすい形で新しく設計されたのがWebSocketです。 WebSocketはTCP上で動くプロトコルなので、WebTransportのヘッドオブライン阻害回避などのQUIC上で動くメリットは持ち合わせていません。

Web Transportのgolangでの実装

webtransport-goを利用して実装したサンプルを載せておきます。 ローカルでチャットアプリを作って動作確認をしたのですが、WebTransportの処理に関わる箇所だけ抜粋しています。 また、下記には注意していただけると助かります。 - 一部コードの抜粋のためサンプルコードだけでは動作しない - ローカルでの動作確認だけのため、実際のネットワーク環境で動くかまでは未確認

Server

import (
    "context"
    "encoding/json"
    "net/http"
    "time"

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

func StartWebTransportServer() {
    // WebTransportハンドラーを作成
    mux := http.NewServeMux()

    // ①QUIC/HTTP3接続確立
    // WebTransportServer自体をHTTP/3サーバーとして使用
    server := &webtransport.Server{
        H3: http3.Server{
            Handler: mux,
            Addr:    ":8443",
        },
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }

    // WebTransportハンドラーを登録
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // ②WebTransport セッション確立
        // Upgrade()の中でmethodやprotocolが正しいかのチェックなども行っている
        // WebTransportセッションにアップグレード
        session, err := server.Upgrade(w, r)
        if err != nil {
            return
        }

        // ③双方向ストリーミング通信
        // 双方向ストリームセッション処理を開始
        go handleBidirectionalSession(session)
    })

    // WebTransportサーバーを起動
    go func() {
        err := server.ListenAndServeTLS("cert.pem", "key.pem")
        if err != nil {
            return
        }
    }()
}

// WebTransport用メッセージ構造体
type WebTransportMessage struct {
    Content   string    `json:"content"`
    Timestamp time.Time `json:"timestamp"`
    Type      string    `json:"type"`
}

// 双方向ストリームでの受信処理
func handleBidirectionalReceive(stream webtransport.Stream) {
    buf := make([]byte, 4096)
    for {
        n, err := stream.Read(buf)
        if err != nil {
            return
        }

        var msg WebTransportMessage
        if err := json.Unmarshal(buf[:n], &msg); err != nil {
            continue
        }

        // 応答送信(同じ双方向ストリーム)
        response := map[string]interface{}{
            "status": "message_received",
        }
        responseData, _ := json.Marshal(response)
        if _, err := stream.Write(responseData); err != nil {
            return
        }
    }
}

// 双方向ストリームでの送信処理
func sendToBidirectionalStream(stream webtransport.Stream, data []byte) error {
    _, err := stream.Write(data)
    if err != nil {
        return err
    }
    return nil
}

// 双方向ストリームベースのセッション処理
func handleBidirectionalSession(session *webtransport.Session) {
    defer session.CloseWithError(0, "bidirectional session ended")

    ctx, cancel := context.WithCancel(session.Context())
    defer cancel()

    // クライアントからの双方向ストリームを待機
    stream, err := session.AcceptStream(ctx)
    if err != nil {
        return
    }

    // 双方向ストリームでの受信と送信を並行処理
    go handleBidirectionalReceive(stream)

}

Client

import (
    "context"
    "crypto/tls"
    "net/http"
    "net/url"
    "time"

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

func main() {

    u, err := url.Parse(serverURL)
    if err != nil {
        return
    }

    u.Path = "/" //

    // WebTransport Dialer設定
    dialer := &webtransport.Dialer{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,
            NextProtos:         []string{"h3"},
            ServerName:         "localhost",
        },
        QUICConfig: &quic.Config{
            MaxIdleTimeout:       30 * time.Second,
            HandshakeIdleTimeout: 10 * time.Second,
            KeepAlivePeriod:      15 * time.Second,
            EnableDatagrams:      true, // WebTransport requires datagram support
        },
        // StreamReorderingTimeoutを設定
        StreamReorderingTimeout: 10 * time.Second,
    }

    ctx := context.Background()

    // WebTransportリクエストヘッダーを設定
    reqHeader := make(http.Header)
    reqHeader.Set("Sec-WebTransport-Http3-Draft", "draft02")
    reqHeader.Set("Origin", serverURL)

    // より長いタイムアウト付きコンテキスト
    dialCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    // Dial()でWebTransportプロトコルでのセッションを開始する
    httpResp, session, err := dialer.Dial(dialCtx, u.String(), reqHeader)
    if err != nil {
        return
    }
    defer session.CloseWithError(0, "client disconnecting")

    if httpResp.StatusCode != 200 {
        return
    }

    // 双方向ストリームを作成して永続的に接続
    biStream, err := session.OpenStream()
    if err != nil {
        return
    }
    defer biStream.Close()

    // 双方向ストリームでの受信処理(サーバー応答用)
    go handleBidirectionalReceive(biStream)

    // 以降の処理はbiStreamを利用してserverと同じような処理になる

}

AWS上で利用する上での注意点

弊社のシステムはAWS上で動作しているものが多く、ロードバランサーとしてはALBを利用していることが多いのですが、WebTransport(の基盤技術であるQUIC)がALBに対応していないようです。 ドキュメントによるとALBでは、HTTP/2とWebSocketに対応しているようです。

docs.aws.amazon.com

NLBはQUICに対応しているとのことなので、WebTransportを利用したい場合はNLBを検討することになりそうです。

docs.aws.amazon.com

まとめ

今回はWebTransportを中心とした双方向通信について学んだことをまとめてみました。WebTransportはQUICベースの比較的新しい技術であり、従来のWebSocketなどの技術の問題を改善していることがわかりました。

一方で、AWS環境ではまだALBがHTTP/3やQUICに対応しておらず、WebTransportを実用するにはいくつかの技術的なハードルがあることも確認できました。現時点では従来のWebSocketやSSEといった技術を組み合わせることが現実的な選択肢となりそうです。

参考文献