[インデックス 15134] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージに、Next Protocol Negotiation (NPN) アップグレードのサポートをサーバー側に追加するものです。これにより、SPDYのような新しいプロトコルを http パッケージと連携させるためのメカニズムが提供されますが、SPDY自体が標準ライブラリに直接組み込まれることはありません。
コミット
commit 92e4645f31c6a766207ce5095b9629f5e77adad5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Feb 4 13:55:38 2013 -0800
net/http: add Next Protocol Negotation upgrade support to the Server
This provides the mechanism to connect SPDY support to the http
package, without pulling SPDY into the standard library.
R=rsc, agl, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/7287045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/92e4645f31c6a766207ce5095b9629f5e77adad5
元コミット内容
このコミットは、net/http パッケージのサーバーにNext Protocol Negotiation (NPN) アップグレードのサポートを追加します。これにより、SPDYのような代替プロトコルを http パッケージと連携させるための汎用的なメカニズムが提供されます。SPDYプロトコル自体はGoの標準ライブラリには含まれませんが、この変更によって外部ライブラリとしてSPDYを実装し、net/http サーバー上で動作させることが可能になります。
具体的には、http.Server 構造体に TLSNextProto という新しいフィールドが追加され、TLSハンドシェイク中にクライアントとネゴシエートされたプロトコルに基づいてカスタムハンドラを登録できるようになります。また、httptest パッケージのTLSテストサーバーもNPNをサポートするように更新され、NPNの動作を検証するための新しいテストファイル npn_test.go が追加されています。
変更の背景
この変更の主な背景は、当時Googleが開発を主導していたSPDYプロトコルへの対応です。SPDYはHTTP/2の先行技術であり、HTTP/1.1のパフォーマンス上の課題を解決するために設計されました。SPDYのような新しいプロトコルを既存のHTTPサーバーインフラストラクチャ上で動作させるためには、TLSハンドシェイク中にクライアントとサーバー間でどのアプリケーションプロトコルを使用するかをネゴシエートするメカニズムが必要でした。
Next Protocol Negotiation (NPN) は、この目的のためにGoogleによって開発されたTLS拡張です。NPNを使用することで、クライアントとサーバーはTLSハンドシェイクの一部として、HTTP/1.1、SPDY、またはその他のカスタムプロトコルなど、サポートするアプリケーション層プロトコルのリストを交換し、合意することができます。
Goの net/http パッケージは、HTTPサーバーの基盤を提供しますが、SPDYのような特定のプロトコルを直接組み込むことは、標準ライブラリのスコープを広げすぎると考えられました。そこで、このコミットでは、SPDY自体を標準ライブラリに含めることなく、NPNを通じて外部のSPDY実装が net/http サーバーと連携できるようにするための汎用的なフックを提供することを目指しました。これにより、GoのHTTPサーバーは将来のプロトコル拡張に対してより柔軟に対応できるようになります。
前提知識の解説
1. HTTP/1.1とSPDY、そしてHTTP/2
- HTTP/1.1: 現在広く使われているHTTPのバージョンです。リクエストごとにTCPコネクションを確立したり、ヘッドオブラインブロッキング(HOLブロッキング)の問題を抱えるなど、パフォーマンス上の課題がありました。
- SPDY (Speedy): Googleが開発した実験的なプロトコルで、HTTP/1.1のパフォーマンス課題を解決するために設計されました。多重化(複数のリクエスト/レスポンスを単一のTCPコネクションで並行して送受信)、ヘッダー圧縮、サーバープッシュなどの機能が特徴です。SPDYはHTTP/2の基礎となりました。
- HTTP/2: SPDYをベースにIETFによって標準化されたHTTPの次世代プロトコルです。SPDYの多くの機能を継承し、Webのパフォーマンスを大幅に向上させました。
2. TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を提供するための暗号化プロトコルです。WebブラウザとWebサーバー間のHTTPS通信などで使用されます。TLSハンドシェイクは、クライアントとサーバーが安全な通信チャネルを確立するために必要な一連のステップです。このハンドシェイク中に、暗号スイートの選択、証明書の交換、鍵の生成などが行われます。
3. Next Protocol Negotiation (NPN) と Application-Layer Protocol Negotiation (ALPN)
- NPN (Next Protocol Negotiation): TLSハンドシェイク中に、クライアントとサーバーがどのアプリケーション層プロトコル(例: HTTP/1.1, SPDY)を使用するかをネゴシエートするためのTLS拡張です。NPNはGoogleによって開発され、SPDYの普及に貢献しました。NPNでは、サーバーがサポートするプロトコルリストをクライアントに提示し、クライアントがその中から選択してサーバーに通知します。
- ALPN (Application-Layer Protocol Negotiation): NPNと同様に、TLSハンドシェイク中にアプリケーション層プロトコルをネゴシエートするためのTLS拡張ですが、NPNの後継としてIETFによって標準化されました。ALPNでは、クライアントがサポートするプロトコルリストをサーバーに提示し、サーバーがその中から選択してクライアントに通知します。NPNとALPNはネゴシエーションの主導権が逆転している点が異なります。現在ではALPNが主流であり、HTTP/2のネゴシエーションに広く使用されています。このコミットが作成された2013年時点では、NPNがSPDYのネゴシエーションに利用されていました。
4. Go言語の net/http パッケージ
Go言語の net/http パッケージは、HTTPクライアントとサーバーを構築するための強力な機能を提供します。http.Server はHTTPサーバーを構成・実行するための主要な構造体であり、http.Handler インターフェースはHTTPリクエストを処理するためのハンドラを定義します。
5. Go言語の crypto/tls パッケージ
Go言語の crypto/tls パッケージは、TLSプロトコルを実装し、安全なネットワーク通信を可能にします。このパッケージは、TLSコネクションの確立、証明書の管理、プロトコルネゴシエーションなどの機能を提供します。
技術的詳細
このコミットの技術的詳細は、主に net/http/server.go と net/http/httptest/server.go の変更、および新しいテストファイル net/http/npn_test.go に集約されています。
net/http/server.go の変更点
-
Server構造体へのTLSNextProtoフィールドの追加:http.Server構造体にTLSNextProto map[string]func(*Server, *tls.Conn, Handler)という新しいフィールドが追加されました。- このマップのキーは、TLSハンドシェイク中にネゴシエートされたプロトコル名(例: "spdy/3.1", "http/1.1")です。
- 値は、そのプロトコルがネゴシエートされたときに呼び出されるカスタムハンドラ関数です。この関数は、
*http.Serverインスタンス、確立された*tls.Conn、そしてHTTPリクエストを処理するためのhttp.Handlerを引数に取ります。 - このメカニズムにより、開発者は特定のプロトコル(例: SPDY)の処理ロジックを
net/httpパッケージの外部で実装し、サーバーに「フック」として登録できるようになります。
-
conn.serve()メソッドでのNPNハンドリング:conn.serve()メソッド(各クライアント接続を処理するゴルーチン)内で、TLSコネクションが確立された後にネゴシエートされたプロトコルをチェックするロジックが追加されました。c.tlsState.NegotiatedProtocolを参照し、ネゴシエートされたプロトコル名を取得します。validNPN(proto)関数(後述)で、そのプロトコルが有効なNPNプロトコルであるかを確認します。空文字列、"http/1.1", "http/1.0" はブラックリスト化されており、カスタムハンドラでオーバーライドすることはできません。- もし有効なNPNプロトコルがネゴシエートされ、かつ
c.server.TLSNextProtoマップにそのプロトコルに対応するハンドラが登録されていれば、そのカスタムハンドラが呼び出されます。 - カスタムハンドラが呼び出された場合、
conn.serve()はそこで処理を終了し、コネクションの所有権はカスタムハンドラに移譲されます。これにより、カスタムプロトコルは独自のI/Oループとリクエスト処理ロジックを実行できます。
-
validNPN関数の追加:func validNPN(proto string) boolというヘルパー関数が追加されました。この関数は、与えられたプロトコル文字列がNPNでカスタムハンドラを登録できる有効なプロトコルであるかを判断します。""(空文字列),"http/1.1","http/1.0"は、Goのnet/httpパッケージがデフォルトで処理するプロトコルであるため、カスタムハンドラでオーバーライドすることはできません。これらのプロトコルがネゴシエートされた場合はfalseを返します。- それ以外のプロトコル(例: "spdy/3.1", "my-custom-protocol")は
trueを返し、カスタムハンドラを登録できることを示します。
-
serverHandler構造体の導入とリファクタリング:serverHandlerという内部構造体が導入され、http.ServerのHandlerフィールド(またはDefaultServeMux)へのリクエストディスパッチロジックと、OPTIONS *リクエストの特殊ハンドリングがこの構造体のServeHTTPメソッドにカプセル化されました。- 以前は
conn.serve()内に直接記述されていたこのロジックがserverHandler.ServeHTTPに移動されたことで、コードの重複が避けられ、NPNハンドラからHTTPリクエストを処理する際に同じロジックを再利用できるようになりました。
- 以前は
-
initNPNRequest構造体の導入:initNPNRequestという内部構造体が追加されました。これは、NPNプロトコルハンドラからhttp.Handlerに渡される*http.Requestオブジェクトの初期化を補助するためのものです。- NPNハンドラは生の
*tls.Connを受け取るため、そこから生成される*http.RequestはTLS状態、Body、RemoteAddrなどのフィールドが未初期化である可能性があります。 initNPNRequest.ServeHTTPは、これらのフィールドがnilまたは空の場合に、*tls.Connから適切な値をコピーして初期化します。これにより、カスタムNPNハンドラがhttp.Handlerを呼び出す際に、完全な*http.Requestオブジェクトを提供できるようになります。
- NPNハンドラは生の
net/http/httptest/server.go の変更点
httptest.ServerのStartTLS()メソッドが更新され、s.TLSフィールドが既に設定されている場合に、その設定を新しいtls.Configにコピーするようになりました。これにより、テストサーバーのユーザーがStartTLS()を呼び出す前にs.TLS.NextProtosなどのフィールドを事前に設定できるようになり、NPNテストの柔軟性が向上しました。
net/http/npn_test.go の追加
- この新しいテストファイルは、NPNアップグレード機能の動作を検証するためのものです。
TestNextProtoUpgrade関数は、httptest.NewUnstartedServerを使用してテストサーバーをセットアップし、TLSNextProtoマップにカスタムプロトコルハンドラ(tls-0.9プロトコルを処理するhandleTLSProtocol09)を登録します。- テストケースには、NPNを使用しない通常のHTTPリクエスト、サーバーが処理しないNPNプロトコルをネゴシエートした場合の挙動、そしてカスタムNPNプロトコル(
tls-0.9)をネゴシエートした場合の挙動が含まれます。 handleTLSProtocol09は、HTTP/0.9プロトコル(非常にシンプルなGETリクエストのみをサポートする古いプロトコル)を模倣したカスタムハンドラであり、NPNを通じてカスタムプロトコルを処理できることを示しています。
コアとなるコードの変更箇所
src/pkg/net/http/server.go
// Server struct:
type Server struct {
// ... existing fields ...
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
}
// conn.serve() method:
func (c *conn) serve() {
// ... existing TLS handshake logic ...
if c.tlsState != nil {
// ... existing TLS state copy ...
if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initNPNRequest{tlsConn, serverHandler{c.server}}
fn(c.server, tlsConn, h)
}
return // Ownership of connection transferred to NPN handler
}
}
// ... existing HTTP/1.x serving logic ...
// Replaced: handler.ServeHTTP(w, w.req)
// With: serverHandler{c.server}.ServeHTTP(w, w.req)
}
// New functions:
func validNPN(proto string) bool {
switch proto {
case "", "http/1.1", "http/1.0":
return false
}
return true
}
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
type initNPNRequest struct {
c *tls.Conn
h serverHandler
}
func (h initNPNRequest) ServeHTTP(rw ResponseWriter, req *Request) {
if req.TLS == nil {
req.TLS = &tls.ConnectionState{}
*req.TLS = h.c.ConnectionState()
}
if req.Body == nil {
req.Body = eofReader
}
if req.RemoteAddr == "" {
req.RemoteAddr = h.c.RemoteAddr().String()
}
h.h.ServeHTTP(rw, req)
}
src/pkg/net/http/httptest/server.go
func (s *Server) StartTLS() {
// ... existing cert generation ...
existingConfig := s.TLS
s.TLS = new(tls.Config)
if existingConfig != nil {
*s.TLS = *existingConfig // Copy existing config
}
if s.TLS.NextProtos == nil {
s.TLS.NextProtos = []string{"http/1.1"}
}
if len(s.TLS.Certificates) == 0 {
s.TLS.Certificates = []tls.Certificate{cert}
}
// ... existing TLS listener setup ...
}
src/pkg/net/http/npn_test.go (新規ファイル)
package http_test
import (
// ... imports ...
)
func TestNextProtoUpgrade(t *testing.T) {
ts := httptest.NewUnstartedServer(HandlerFunc(func(w ResponseWriter, r *Request) {
// ... handler logic ...
}))
ts.TLS = &tls.Config{
NextProtos: []string{"unhandled-proto", "tls-0.9"},
}
ts.Config.TLSNextProto = map[string]func(*Server, *tls.Conn, Handler){
"tls-0.9": handleTLSProtocol09,
}
ts.StartTLS()
defer ts.Close()
// ... test cases for NPN ...
}
func handleTLSProtocol09(srv *Server, conn *tls.Conn, h Handler) {
// ... HTTP/0.9 protocol implementation ...
req, _ := NewRequest("GET", path, nil)
// ... set req.Proto, req.ProtoMajor, req.ProtoMinor ...
rw := &http09Writer{conn, make(Header)}
h.ServeHTTP(rw, req) // Delegate to standard HTTP handler
}
type http09Writer struct {
io.Writer
h Header
}
func (w http09Writer) Header() Header { return w.h }
func (w http09Writer) WriteHeader(int) {} // no headers
コアとなるコードの解説
このコミットの核心は、net/http サーバーがTLSハンドシェイク中にネゴシエートされたアプリケーションプロトコルに基づいて、カスタムの処理ロジックに制御を移譲できるようになった点です。
-
http.Server.TLSNextProto: これは、サーバーがサポートするカスタムプロトコルハンドラを登録するための「フック」です。開発者は、SPDYのような特定のプロトコルを処理する関数をこのマップに登録することで、net/httpサーバーがそのプロトコルを認識し、適切なハンドラに接続の制御を渡すように設定できます。これにより、net/httpパッケージ自体にSPDYの実装を含めることなく、SPDYサポートを外部から追加することが可能になります。 -
conn.serve()内のNPNハンドリングロジック: TLSハンドシェイクが完了し、tls.ConnectionStateからNegotiatedProtocolが取得されると、このロジックが発動します。validNPN関数は、ネゴシエートされたプロトコルがhttp/1.1やhttp/1.0のような標準プロトコルではないことを確認します。これは、標準プロトコルはnet/httpパッケージが内部で処理するため、カスタムハンドラでオーバーライドすべきではないという設計思想に基づいています。- もしカスタムプロトコルがネゴシエートされ、かつ
TLSNextProtoマップにそのプロトコルに対応するハンドラが登録されていれば、そのハンドラが呼び出されます。この際、initNPNRequestを介して、カスタムハンドラがhttp.Handlerを呼び出す際に必要な*http.RequestのTLS状態、ボディ、リモートアドレスなどの情報が適切に初期化されます。 - カスタムハンドラが呼び出されると、
conn.serve()はreturnし、その後のコネクションのI/Oとプロトコル処理は完全にカスタムハンドラに委ねられます。これにより、SPDYのような非HTTP/1.1プロトコルが、net/httpの既存のHTTP/1.1処理ロジックと干渉することなく、独自のライフサイクルで動作できるようになります。
-
serverHandlerの導入: これは主にリファクタリングであり、http.ServerのHandlerディスパッチロジックを独立した構造体に切り出すことで、コードの再利用性を高めています。NPNハンドラが最終的に標準のhttp.Handlerを呼び出す場合でも、このserverHandlerを通じて一貫したディスパッチロジックが適用されます。 -
npn_test.go: このテストファイルは、NPN機能が意図通りに動作することを確認するための重要な役割を果たします。特にhandleTLSProtocol09のようなカスタムプロトコルハンドラの実装例は、TLSNextProtoをどのように利用して独自のプロトコルを処理するかを示す良い例となっています。これにより、開発者はこの新機能の利用方法を理解しやすくなります。
これらの変更により、Goの net/http サーバーは、HTTP/1.1だけでなく、NPNを通じてネゴシエートされた他のアプリケーション層プロトコル(当時のSPDYなど)も柔軟にサポートできる、より汎用的な基盤へと進化しました。
関連リンク
- Go言語の
net/httpパッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
crypto/tlsパッケージドキュメント: https://pkg.go.dev/crypto/tls - SPDYプロトコル (Wikipedia): https://ja.wikipedia.org/wiki/SPDY
- HTTP/2 (Wikipedia): https://ja.wikipedia.org/wiki/HTTP/2
- Next Protocol Negotiation (NPN) (Wikipedia): https://en.wikipedia.org/wiki/Next_Protocol_Negotiation (日本語版はALPNにリダイレクトされることが多いです)
- Application-Layer Protocol Negotiation (ALPN) (Wikipedia): https://ja.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation
参考にした情報源リンク
- Goのコミットログと変更されたソースコード
- Goの公式ドキュメント (pkg.go.dev)
- Wikipedia (SPDY, HTTP/2, NPN, ALPN)
- 各種技術ブログや記事 (NPNとALPNの違い、SPDYの歴史など)
- 例: https://blog.cloudflare.com/introducing-alpn/ (ALPNに関するCloudflareのブログ記事、NPNとの比較に言及)
- 例: https://hpbn.co/optimizing-for-http2/#protocol-negotiation (High Performance Browser Networkingのプロトコルネゴシエーションに関する章)
- GoのIssueトラッカーやコードレビュー (CL 7287045など) - 公開されている場合は参照。