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

[インデックス 17070] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける、HTTP/1.0レスポンスでの Connection: close ヘッダの冗長な送信を停止する変更です。これにより、HTTP/1.0のデフォルトの接続動作(リクエスト/レスポンス後に接続が閉じられる)に合わせた、より効率的な通信が実現されます。

コミット

net/http: HTTP/1.0レスポンスで冗長な Connection: close ヘッダを送信しない

HTTP/1.0接続は、特に指定がない限り暗黙的に閉じられます。

この変更は、「リクエストが大きすぎる」レスポンスをテストまたは修正するものではありません。 理由:(a) テストと修正が複雑になる、(b) それらは稀であるべき、(c) これは単なるマイナーなワイヤー最適化であり、この文脈では本当に心配する価値はありません。

Fixes #5955.

R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/12435043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1535727e57f633a0570faa5016b8f34053760b71

元コミット内容

commit 1535727e57f633a0570faa5016b8f34053760b71
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date:   Tue Aug 6 18:37:34 2013 -0700

    net/http: do not send redundant Connection: close header in HTTP/1.0 responses
    
    HTTP/1.0 connections are closed implicitly, unless otherwise specified.
    
    Note that this change does not test or fix "request too large" responses.
    Reasoning: (a) it complicates tests and fixes, (b) they should be rare,
    and (c) this is just a minor wire optimization, and thus not really worth worrying
    about in this context.
    
    Fixes #5955.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12435043

変更の背景

このコミットの背景には、HTTPプロトコルのバージョン間の接続管理の違いがあります。特にHTTP/1.0とHTTP/1.1では、接続の永続性に関するデフォルトの挙動が異なります。

  • HTTP/1.0: デフォルトでは、各リクエスト/レスポンスのペアの後にTCP接続が閉じられます。つまり、接続は非永続的です。もしクライアントが接続を維持したい場合は、明示的に Connection: keep-alive ヘッダを送信する必要があります。
  • HTTP/1.1: デフォルトでは、TCP接続は永続的です。つまり、複数のリクエスト/レスポンスのペアが同じ接続上で送受信されることが期待されます。接続を閉じたい場合は、クライアントまたはサーバーが明示的に Connection: close ヘッダを送信する必要があります。

Goの net/http パッケージは、HTTPサーバーとして動作する際に、HTTP/1.0のリクエストに対しても Connection: close ヘッダを送信していました。しかし、HTTP/1.0の仕様では、接続はデフォルトで閉じられるため、このヘッダは冗長でした。冗長なヘッダを送信することは、わずかながらネットワーク帯域を消費し、処理オーバーヘッドを発生させます。

このコミットは、この冗長性を排除し、HTTP/1.0のセマンティクスに厳密に従うことで、ワイヤープロトコルを最適化することを目的としています。コミットメッセージにもあるように、これは「マイナーなワイヤー最適化」であり、パフォーマンスに劇的な影響を与えるものではありませんが、プロトコル実装の正確性と効率性を向上させます。

また、この変更はGoのIssue #5955に対応するものです。このIssueでは、HTTP/1.0のレスポンスで Connection: close ヘッダが不要であるという点が指摘されていました。

前提知識の解説

HTTPプロトコルにおける接続管理

HTTPプロトコルは、クライアントとサーバー間の通信を定義します。その中で、TCP接続の管理は重要な側面です。

  • 非永続的接続 (Non-persistent Connection):

    • HTTP/1.0のデフォルトの挙動です。
    • クライアントがリクエストを送信し、サーバーがレスポンスを返すと、その直後にTCP接続が閉じられます。
    • 次のリクエストを送信するためには、新しいTCP接続を確立する必要があります。
    • オーバーヘッド: 各リクエストごとにTCP接続の確立(3-wayハンドシェイク)と切断(4-wayハンドシェイク)が発生するため、レイテンシが増加し、サーバーのリソース消費も大きくなります。
  • 永続的接続 (Persistent Connection):

    • HTTP/1.1のデフォルトの挙動です。
    • 一度確立されたTCP接続は、複数のリクエスト/レスポンスのペアのために開いたままになります。
    • クライアントは、同じ接続上で連続してリクエストを送信できます。
    • サーバーは、レスポンスを送信した後も接続を開いたままにし、次のリクエストを待ちます。
    • 利点: TCP接続の確立・切断のオーバーヘッドが削減され、ネットワークの効率が向上し、レイテンシが減少します。

Connection ヘッダ

Connection ヘッダは、HTTPメッセージの送信者(クライアントまたはサーバー)が、現在の接続に適用される接続固有のオプションを制御するために使用されます。これは「ホップバイホップ」ヘッダであり、プロキシによって転送される前に削除される必要があります。

  • Connection: close:

    • 送信者が、現在のTCP接続をレスポンスの送信後に閉じることを意図していることを示します。
    • HTTP/1.1では、永続的接続がデフォルトであるため、接続を明示的に閉じたい場合にこのヘッダが使用されます。
  • Connection: keep-alive:

    • 送信者が、現在のTCP接続をレスポンスの送信後も開いたままにすることを意図していることを示します。
    • HTTP/1.0では、非永続的接続がデフォルトであるため、接続を永続化したい場合にこのヘッダが使用されます。

冗長なヘッダ

プロトコルのデフォルトの挙動と一致するヘッダを明示的に送信することは、技術的には問題ありませんが、冗長です。例えば、HTTP/1.0では接続がデフォルトで閉じられるため、Connection: close を送信することは、すでに暗黙的に行われることを明示しているに過ぎません。これは、わずかながらネットワーク帯域を消費し、パケットサイズを増加させます。

技術的詳細

このコミットの技術的な核心は、Goの net/http パッケージがHTTPレスポンスを生成する際に、HTTPプロトコルのバージョンを考慮して Connection ヘッダの追加を条件付きにすることです。

具体的には、以下の2つの箇所で変更が行われました。

  1. (*chunkWriter).writeHeader メソッド内: このメソッドは、HTTPレスポンスのヘッダを書き込む際に呼び出されます。以前は、w.closeAfterReply が真(つまり、レスポンス後に接続を閉じる必要がある場合)かつ Connection ヘッダに close トークンが含まれていない場合、無条件に Connection: close ヘッダを追加していました。 変更後、この Connection: close ヘッダの追加は、リクエストのプロトコルバージョンがHTTP/1.1以上 (w.req.ProtoAtLeast(1, 1)) である場合にのみ行われるようになりました。これにより、HTTP/1.0のリクエストに対しては、デフォルトの挙動(暗黙的な接続クローズ)に任せ、冗長なヘッダ送信を回避します。

  2. (*ServeMux).ServeHTTP メソッド内: このメソッドは、HTTPリクエストを処理し、適切なハンドラにディスパッチする役割を担います。特に、r.RequestURI == "*" のような特殊なリクエスト(例: OPTIONS *)や、リクエストが大きすぎる場合などに StatusBadRequest を返す際に、以前は無条件に Connection: close ヘッダを設定していました。 変更後、ここでも同様に、Connection: close ヘッダの設定は、リクエストのプロトコルバージョンがHTTP/1.1以上 (r.ProtoAtLeast(1, 1)) である場合にのみ行われるようになりました。

これらの変更により、GoのHTTPサーバーは、HTTP/1.0クライアントに対して、プロトコルのセマンティクスに沿った、よりクリーンなレスポンスを返すようになります。

テストの追加

この変更を検証するために、TestHTTP10ConnectionHeader という新しいテストが src/pkg/net/http/serve_test.go に追加されました。このテストは、httptest.NewServer を使用してHTTPサーバーを起動し、net.Dial を使って手動でHTTP/1.0リクエストを送信します。

テストケースは以下のシナリオをカバーしています。

  • GET / HTTP/1.0\r\n\r\n: 標準的なHTTP/1.0リクエスト。期待される Connection ヘッダは nil(つまり、サーバーは Connection: close を送信しない)。
  • OPTIONS * HTTP/1.0\r\n\r\n: 特殊なHTTP/1.0リクエスト。期待される Connection ヘッダは nil
  • GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n: クライアントが Connection: keep-alive を明示的に要求するHTTP/1.0リクエスト。この場合、サーバーは Connection: keep-alive を返すことが期待される。

このテストは、GoのHTTPサーバーがHTTP/1.0の接続管理ルールを正しく実装していることを確認します。コミットメッセージにあるように、「リクエストが大きすぎる」場合のテストは意図的に省略されています。これは、そのケースが稀であり、この最適化の主要な目的ではないためです。

コアとなるコードの変更箇所

src/pkg/net/http/serve_test.go

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1757,6 +1757,64 @@ func TestWriteAfterHijack(t *testing.T) {
 	}\n }\n \n+// http://code.google.com/p/go/issues/detail?id=5955
+// Note that this does not test the "request too large"
+// exit path from the http server. This is intentional;
+// not sending Connection: close is just a minor wire
+// optimization and is pointless if dealing with a
+// badly behaved client.
+func TestHTTP10ConnectionHeader(t *testing.T) {
+	defer afterTest(t)\n\n 	mux := NewServeMux()\n 	mux.Handle("/", HandlerFunc(func(resp ResponseWriter, req *Request) {}))\n 	ts := httptest.NewServer(mux)\n 	defer ts.Close()\n\n 	// net/http uses HTTP/1.1 for requests, so write requests manually
+	tests := []struct {
+		req    string   // raw http request
+		expect []string // expected Connection header(s)
+	}{
+		{
+			req:    "GET / HTTP/1.0\\r\\n\\r\\n",
+			expect: nil,
+		},
+		{
+			req:    "OPTIONS * HTTP/1.0\\r\\n\\r\\n",
+			expect: nil,
+		},
+		{
+			req:    "GET / HTTP/1.0\\r\\nConnection: keep-alive\\r\\n\\r\\n",
+			expect: []string{"keep-alive"},
+		},
+	}\n\n 	for _, tt := range tests {
+		conn, err := net.Dial("tcp", ts.Listener.Addr().String())
+		if err != nil {
+			t.Fatal("dial err:", err)
+		}
+
+		_, err = fmt.Fprint(conn, tt.req)
+		if err != nil {
+			t.Fatal("conn write err:", err)
+		}
+
+		resp, err := ReadResponse(bufio.NewReader(conn), &Request{Method: "GET"})
+		if err != nil {
+			t.Fatal("ReadResponse err:", err)
+		}
+		conn.Close()
+		resp.Body.Close()
+
+		got := resp.Header["Connection"]
+		if !reflect.DeepEqual(got, tt.expect) {
+			t.Errorf("wrong Connection headers for request %q. Got %q expect %q", got, tt.expect)
+		}\n 	}\n }\n\n func BenchmarkClientServer(b *testing.B) {

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -850,7 +850,9 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 
 	if w.closeAfterReply && !hasToken(cw.header.get("Connection"), "close") {
 		delHeader("Connection")
-		setHeader.connection = "close"
+		if w.req.ProtoAtLeast(1, 1) {
+			setHeader.connection = "close"
+		}
 	}
 
 	w.conn.buf.WriteString(statusLine(w.req, code))
@@ -1458,7 +1460,9 @@ func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
 // pattern most closely matches the request URL.
 func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
 	if r.RequestURI == "*" {
-		w.Header().Set("Connection", "close")
+		if r.ProtoAtLeast(1, 1) {
+			w.Header().Set("Connection", "close")
+		}
 		w.WriteHeader(StatusBadRequest)
 		return
 	}

コアとなるコードの解説

src/pkg/net/http/server.go の変更点

  1. (*chunkWriter).writeHeader メソッド: このメソッドは、HTTPレスポンスのヘッダを実際に書き出す部分です。 変更前:

    if w.closeAfterReply && !hasToken(cw.header.get("Connection"), "close") {
        delHeader("Connection")
        setHeader.connection = "close"
    }
    

    変更後:

    if w.closeAfterReply && !hasToken(cw.header.get("Connection"), "close") {
        delHeader("Connection")
        if w.req.ProtoAtLeast(1, 1) { // ここが追加された条件
            setHeader.connection = "close"
        }
    }
    

    w.closeAfterReply は、サーバーがレスポンス後に接続を閉じるべきかどうかを示すフラグです。このフラグが真であり、かつ既存の Connection ヘッダに close トークンが含まれていない場合に、以前は無条件に Connection: close ヘッダを追加していました。 追加された if w.req.ProtoAtLeast(1, 1) 条件は、リクエストのプロトコルバージョンがHTTP/1.1以上である場合にのみ Connection: close ヘッダを設定するようにします。これにより、HTTP/1.0のリクエストに対しては、この冗長なヘッダが送信されなくなります。

  2. (*ServeMux).ServeHTTP メソッド: このメソッドは、HTTPリクエストのルーティングと基本的な処理を行います。 変更前:

    if r.RequestURI == "*" {
        w.Header().Set("Connection", "close")
        w.WriteHeader(StatusBadRequest)
        return
    }
    

    変更後:

    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) { // ここが追加された条件
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    

    r.RequestURI == "*" は、主に OPTIONS * のようなリクエストや、不正なリクエストURIの場合に発生します。このような場合、サーバーは 400 Bad Request を返すことがありますが、その際に Connection: close ヘッダも設定していました。 ここでも同様に、if r.ProtoAtLeast(1, 1) 条件が追加され、HTTP/1.1以上のプロトコルバージョンでのみ Connection: close ヘッダが設定されるようになりました。これにより、HTTP/1.0のリクエストに対しては、このヘッダが送信されなくなります。

src/pkg/net/http/serve_test.go の変更点

TestHTTP10ConnectionHeader 関数が追加されました。 このテストは、httptest.NewServer を使ってテスト用のHTTPサーバーを立ち上げ、net.Dialfmt.Fprint を使って生のHTTP/1.0リクエストをサーバーに送信します。 その後、ReadResponse を使ってサーバーからのレスポンスを読み込み、resp.Header["Connection"] をチェックして、期待される Connection ヘッダが返されているか(または返されていないか)を検証します。 特に、GET / HTTP/1.0\r\n\r\n のようなリクエストに対しては、expect: nil となっており、サーバーが Connection ヘッダを送信しないことを期待しています。これは、HTTP/1.0では接続が暗黙的に閉じられるため、Connection: close が不要であることを確認するものです。

関連リンク

参考にした情報源リンク