[インデックス 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など) - 公開されている場合は参照。