
この記事は 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の通信は以下のフェーズに分かれて動作します。

セッション確立の詳細
QUIC接続確立
- UDP上でQUICプロトコルによる接続確立
- TLS 1.3による暗号化ハンドシェイク
WebTransportセッション開始
- HTTP/3のCONNECTメソッドを使用
protocol="webtransport"ヘッダーで識別- サーバーが200 OKで応答すればセッション確立
データ通信
- 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に対応しているようです。
NLBはQUICに対応しているとのことなので、WebTransportを利用したい場合はNLBを検討することになりそうです。
まとめ
今回はWebTransportを中心とした双方向通信について学んだことをまとめてみました。WebTransportはQUICベースの比較的新しい技術であり、従来のWebSocketなどの技術の問題を改善していることがわかりました。
一方で、AWS環境ではまだALBがHTTP/3やQUICに対応しておらず、WebTransportを実用するにはいくつかの技術的なハードルがあることも確認できました。現時点では従来のWebSocketやSSEといった技術を組み合わせることが現実的な選択肢となりそうです。