Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.gonet/http/httptest/server.go の変更、および新しいテストファイル net/http/npn_test.go に集約されています。

net/http/server.go の変更点

  1. 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 パッケージの外部で実装し、サーバーに「フック」として登録できるようになります。
  2. 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ループとリクエスト処理ロジックを実行できます。
  3. validNPN 関数の追加: func validNPN(proto string) bool というヘルパー関数が追加されました。この関数は、与えられたプロトコル文字列がNPNでカスタムハンドラを登録できる有効なプロトコルであるかを判断します。

    • "" (空文字列), "http/1.1", "http/1.0" は、Goの net/http パッケージがデフォルトで処理するプロトコルであるため、カスタムハンドラでオーバーライドすることはできません。これらのプロトコルがネゴシエートされた場合は false を返します。
    • それ以外のプロトコル(例: "spdy/3.1", "my-custom-protocol")は true を返し、カスタムハンドラを登録できることを示します。
  4. serverHandler 構造体の導入とリファクタリング: serverHandler という内部構造体が導入され、http.ServerHandler フィールド(または DefaultServeMux)へのリクエストディスパッチロジックと、OPTIONS * リクエストの特殊ハンドリングがこの構造体の ServeHTTP メソッドにカプセル化されました。

    • 以前は conn.serve() 内に直接記述されていたこのロジックが serverHandler.ServeHTTP に移動されたことで、コードの重複が避けられ、NPNハンドラからHTTPリクエストを処理する際に同じロジックを再利用できるようになりました。
  5. initNPNRequest 構造体の導入: initNPNRequest という内部構造体が追加されました。これは、NPNプロトコルハンドラから http.Handler に渡される *http.Request オブジェクトの初期化を補助するためのものです。

    • NPNハンドラは生の *tls.Conn を受け取るため、そこから生成される *http.RequestTLS 状態、BodyRemoteAddr などのフィールドが未初期化である可能性があります。
    • initNPNRequest.ServeHTTP は、これらのフィールドが nil または空の場合に、*tls.Conn から適切な値をコピーして初期化します。これにより、カスタムNPNハンドラが http.Handler を呼び出す際に、完全な *http.Request オブジェクトを提供できるようになります。

net/http/httptest/server.go の変更点

  • httptest.ServerStartTLS() メソッドが更新され、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ハンドシェイク中にネゴシエートされたアプリケーションプロトコルに基づいて、カスタムの処理ロジックに制御を移譲できるようになった点です。

  1. http.Server.TLSNextProto: これは、サーバーがサポートするカスタムプロトコルハンドラを登録するための「フック」です。開発者は、SPDYのような特定のプロトコルを処理する関数をこのマップに登録することで、net/http サーバーがそのプロトコルを認識し、適切なハンドラに接続の制御を渡すように設定できます。これにより、net/http パッケージ自体にSPDYの実装を含めることなく、SPDYサポートを外部から追加することが可能になります。

  2. conn.serve() 内のNPNハンドリングロジック: TLSハンドシェイクが完了し、tls.ConnectionState から NegotiatedProtocol が取得されると、このロジックが発動します。

    • validNPN 関数は、ネゴシエートされたプロトコルが http/1.1http/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処理ロジックと干渉することなく、独自のライフサイクルで動作できるようになります。
  3. serverHandler の導入: これは主にリファクタリングであり、http.ServerHandler ディスパッチロジックを独立した構造体に切り出すことで、コードの再利用性を高めています。NPNハンドラが最終的に標準の http.Handler を呼び出す場合でも、この serverHandler を通じて一貫したディスパッチロジックが適用されます。

  4. npn_test.go: このテストファイルは、NPN機能が意図通りに動作することを確認するための重要な役割を果たします。特に handleTLSProtocol09 のようなカスタムプロトコルハンドラの実装例は、TLSNextProto をどのように利用して独自のプロトコルを処理するかを示す良い例となっています。これにより、開発者はこの新機能の利用方法を理解しやすくなります。

これらの変更により、Goの net/http サーバーは、HTTP/1.1だけでなく、NPNを通じてネゴシエートされた他のアプリケーション層プロトコル(当時のSPDYなど)も柔軟にサポートできる、より汎用的な基盤へと進化しました。

関連リンク

参考にした情報源リンク

  • Goのコミットログと変更されたソースコード
  • Goの公式ドキュメント (pkg.go.dev)
  • Wikipedia (SPDY, HTTP/2, NPN, ALPN)
  • 各種技術ブログや記事 (NPNとALPNの違い、SPDYの歴史など)
  • GoのIssueトラッカーやコードレビュー (CL 7287045など) - 公開されている場合は参照。