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

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

このコミット 3d03ec88963e93d15a9dec53b4ba61fda75c603b は、Go言語の標準ライブラリ net/http パッケージにおける、HTTP接続のKeep-Alive動作に関する変更です。具体的には、以前のコミット 2eec2501961c (CL 6112054) で行われた変更を元に戻す(undoする)ものです。この 2eec2501961c は、さらにその前のコミット 97d027b3aa68 の内容をRevert(元に戻す)していました。

つまり、このコミットは「RevertのRevert」であり、結果としてクライアントがHTTP接続のKeep-Aliveを無効にできる機能(Connection: close ヘッダの適切な処理)を再度有効にするものです。この変更は、以前のRevertの原因となっていた「Expect: 100-continue」ヘッダに関するテストの問題が解決されたため、安全に行えるようになりました。また、Issue 3540「Connection header in request is ignored by the http server」の修正にも関連しています。

コミット

commit 3d03ec88963e93d15a9dec53b4ba61fda75c603b
Author: Russ Cox <rsc@golang.org>
Date:   Tue May 22 13:56:40 2012 -0400

    undo CL 6112054 / 2eec2501961c
    
    Now that we've fixed the Expect: test, this CL should be okay.
    
    ««« original CL description
    net/http: revert 97d027b3aa68
    
    Revert the following change set:
    
            changeset:   13018:97d027b3aa68
            user:        Gustavo Niemeyer <gustavo@niemeyer.net>
            date:        Mon Apr 23 22:00:16 2012 -0300
            summary:     net/http: allow clients to disable keep-alive
    
    This broke a test on Windows 64 and somebody else
    will have to check.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6112054
    »»»
    
    Fixes #3540.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6228046

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

https://github.com/golang/go/commit/3d03ec88963e93d15a9dec53b4ba61fda75c603b

元コミット内容

このコミットは、2eec2501961c (CL 6112054) というコミットを元に戻すものです。 その 2eec2501961c コミットの元の説明には、97d027b3aa68 というコミットをRevertすると記載されています。

97d027b3aa68 のコミット内容は以下の通りです。

  • コミットハッシュ: 97d027b3aa68
  • 概要: net/http: allow clients to disable keep-alive (クライアントがKeep-Aliveを無効にできるようにする)
  • 日付: 2012年4月23日
  • 作者: Gustavo Niemeyer

この 97d027b3aa68 の変更は、クライアントがHTTPリクエストヘッダに Connection: close を含めることで、サーバーに対して接続を閉じるように要求できる機能を追加するものでした。しかし、この変更はWindows 64環境でのテストを壊したため、一時的に 2eec2501961c によってRevertされていました。

今回のコミット 3d03ec88963e93d15a9dec53b4ba61fda75c603b は、この 2eec2501961c のRevertをさらに元に戻すことで、結果的に 97d027b3aa68 で導入された「クライアントがKeep-Aliveを無効にできる機能」を再度有効にしています。

変更の背景

このコミットの背景には、Go言語の net/http パッケージにおけるHTTP接続の管理、特にKeep-Aliveと接続終了のメカニズムに関する課題がありました。

  1. 初期の機能追加 (97d027b3aa68): 当初、Gustavo Niemeyer氏によって、クライアントがHTTPリクエストヘッダに Connection: close を含めることで、サーバーに対して接続を閉じるように明示的に要求できる機能が追加されました。これは、HTTPプロトコルにおける標準的な動作であり、クライアントがリソースを解放したり、特定のシナリオで接続の再利用を避けたい場合に重要です。この変更は、GoのIssue 3540「Connection header in request is ignored by the http server」の修正を意図していました。

  2. 一時的なRevert (2eec2501961c): しかし、この機能追加はWindows 64環境でのテストを壊すという予期せぬ副作用をもたらしました。具体的なテストの失敗内容はコミットメッセージからは不明ですが、安定性を優先するため、Russ Cox氏によってこの変更は一時的にRevertされました。これにより、クライアントが Connection: close を指定しても、サーバーがそれを適切に処理しない状態に戻されました。

  3. RevertのRevert(今回のコミット 3d03ec88963e93d15a9dec53b4ba61fda75c603b: 今回のコミットは、以前のRevert (2eec2501961c) を元に戻すものです。この「RevertのRevert」が可能になったのは、コミットメッセージに「Now that we've fixed the Expect: test, this CL should be okay.」とあるように、Expect: 100-continue ヘッダに関連するテストの問題が解決されたためです。この「Expect: test」の修正が、以前のWindows 64でのテスト失敗の根本原因であった可能性が高いです。この修正により、クライアントが Connection: close を指定した場合の動作が再び正しくなり、Issue 3540が最終的に解決されることになります。

要するに、このコミットは、一時的に無効化されていた「クライアントによる接続終了要求」の機能を、関連するバグが修正されたことで安全に再有効化し、HTTPプロトコル仕様への準拠と、より柔軟な接続管理を実現することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のHTTPプロトコルとGo言語の net/http パッケージに関する知識が必要です。

  1. HTTP Keep-Alive (持続的接続): HTTP/1.1では、デフォルトでKeep-Alive(持続的接続)が有効になっています。これは、一つのTCP接続上で複数のHTTPリクエストとレスポンスをやり取りできる機能です。これにより、リクエストごとに新しいTCP接続を確立・切断するオーバーヘッドが削減され、Webページのロード時間短縮やサーバー負荷軽減に貢献します。 クライアントやサーバーは、Connection: keep-alive ヘッダを送信することで、接続を維持したい意図を示します。

  2. HTTP Connection: close ヘッダ: HTTP/1.0では、デフォルトで接続はリクエストごとに閉じられます。HTTP/1.1でKeep-Aliveがデフォルトになった後も、クライアントやサーバーは Connection: close ヘッダを送信することで、現在のリクエスト/レスポンスの送受信が完了した後にTCP接続を閉じるように明示的に要求できます。これは、リソースの解放、サーバーの過負荷回避、または特定のプロトコルハンドシェイクの完了など、様々なシナリオで利用されます。

  3. HTTP Expect: 100-continue ヘッダ: これは、主に大きなリクエストボディ(例: ファイルアップロード)を送信する際にクライアントが使用するヘッダです。クライアントはまず、リクエストヘッダに Expect: 100-continue を含めてサーバーに送信します。サーバーはこれを受け取ると、リクエストボディを受け入れる準備ができている場合に 100 Continue という中間レスポンスを返します。クライアントはこの 100 Continue を受け取ってから、初めてリクエストボディの送信を開始します。 もしサーバーが 100 Continue 以外のレスポンス(例: 401 Unauthorized403 Forbidden)を返した場合、クライアントはリクエストボディを送信せずに接続を閉じることができます。これにより、不要なデータ転送を避け、帯域幅を節約できます。 このメカニズムは、サーバーがリクエストボディを処理する前に認証や認可のチェックを行いたい場合などに有用です。しかし、実装が複雑であり、特にエラーハンドリングやタイムアウト処理が不適切だと、接続がハングアップしたり、予期せぬ動作を引き起こす可能性があります。今回のコミットの背景にある「Expect: test」の問題も、このような複雑さに起因していたと考えられます。

  4. Go言語の net/http パッケージ: Go言語の net/http パッケージは、HTTPクライアントとサーバーを構築するための基本的な機能を提供します。このパッケージは、HTTPプロトコルの詳細を抽象化し、開発者が簡単にWebアプリケーションを構築できるように設計されています。

    • http.Request: 受信したHTTPリクエストを表す構造体。ヘッダ、メソッド、URL、ボディなどの情報を含みます。
    • http.ResponseWriter: HTTPレスポンスを書き込むためのインターフェース。ヘッダの設定やボディの書き込みに使用します。
    • http.Handler: HTTPリクエストを処理するためのインターフェース。ServeHTTP メソッドを実装します。
  5. HTTPヘッダのトークン解析: HTTPヘッダの値は、しばしばカンマ区切りで複数の「トークン」を含むことがあります(例: Connection: keep-alive, Upgrade)。これらのトークンを正しく解析するには、単に strings.Contains で部分文字列を検索するだけでは不十分な場合があります。RFC 7230などのHTTP仕様では、トークンは特定の文字セットで構成され、空白やカンマで区切られると定義されています。今回のコミットで導入された hasToken 関数は、このトークン解析の必要性を示唆しています。コミット内の TODO コメントにあるように、strings.Contains を使用している点は、まだRFCに完全に準拠した実装ではないことを示しています。

技術的詳細

このコミットで行われた技術的な変更は、主に net/http パッケージ内のHTTPヘッダの解析と、それに基づく接続管理のロジックに焦点を当てています。

  1. hasToken ヘルパー関数の導入: src/pkg/net/http/request.go に新しいプライベート関数 hasToken(s, token string) bool が追加されました。この関数は、与えられた文字列 s(HTTPヘッダの値)が特定の token を含んでいるかどうかをチェックします。 以前は strings.Contains(strings.ToLower(r.Header.Get("Expect")), "100-continue") のように直接 strings.Contains を使用していましたが、hasToken を導入することで、ヘッダ値の正規化(小文字化)とトークンチェックのロジックをカプセル化しています。 ただし、この関数には // TODO This is a poor implementation of the RFC. See http://golang.org/issue/3535 というコメントが付いています。これは、HTTPヘッダのトークン解析がRFCの仕様(例: カンマ区切りの複数のトークン、空白の扱いなど)に厳密には準拠しておらず、より堅牢な実装が必要であることを示唆しています。Issue 3535は、この hasToken の実装が不十分であるという課題を追跡しているものと思われます。

  2. Request 構造体への wantsClose() メソッドの追加: src/pkg/net/http/request.gofunc (r *Request) wantsClose() bool メソッドが追加されました。このメソッドは、リクエストの Connection ヘッダに close トークンが含まれているかどうかをチェックします。これにより、クライアントが明示的に接続の終了を要求しているかどうかを簡単に判断できるようになります。このメソッドも内部で hasToken を利用しています。

  3. expectsContinue()wantsHttp10KeepAlive() の変更: src/pkg/net/http/request.go 内の既存のメソッド expectsContinue()wantsHttp10KeepAlive() も、内部で hasToken 関数を使用するように変更されました。これにより、HTTPヘッダのトークンチェックが一貫した方法で行われるようになります。

  4. サーバー側の接続終了ロジックの修正 (src/pkg/net/http/server.go): response 構造体の WriteHeader メソッド(HTTPレスポンスヘッダを書き込む際に呼び出される)内のロジックが変更されました。 変更前:

    } else if !w.req.ProtoAtLeast(1, 1) {
        // Client did not ask to keep connection alive.
        w.closeAfterReply = true
    }
    

    変更後:

    } else if !w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() {
        w.closeAfterReply = true
    }
    

    この変更により、サーバーは以下のいずれかの条件が満たされた場合に、レスポンス送信後に接続を閉じるように設定されます。

    • リクエストがHTTP/1.1以上ではない場合(つまりHTTP/1.0の場合)。HTTP/1.0ではデフォルトで接続は閉じられます。
    • または、クライアントが Connection: close ヘッダを送信して接続の終了を要求している場合(w.req.wantsClose()true を返す場合)。 この修正により、サーバーはクライアントの Connection: close 要求を適切に尊重し、接続を終了するようになります。
  5. テストケースの追加と修正 (src/pkg/net/http/serve_test.go):

    • testTcpConnectionCloses 関数が testTCPConnectionCloses にリネームされました(Goの命名規約に合わせた変更)。
    • 新しいテストケース TestClientCanClose が追加されました。このテストは、クライアントが Connection: close ヘッダを送信した場合に、サーバーが実際に接続を閉じることを検証します。これは、今回のコミットで再有効化された機能が正しく動作することを確認するための重要なテストです。

これらの変更は、HTTPヘッダの解析をより堅牢にし、特に Connection ヘッダに基づく接続管理のロジックを改善することで、HTTPプロトコル仕様への準拠を高め、クライアントが接続のライフサイクルをより細かく制御できるようにすることを目的としています。

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

このコミットにおける主要なコード変更は以下の3つのファイルにわたります。

  1. src/pkg/net/http/request.go:

    • expectsContinue() 関数が strings.ToLower(r.Header.Get("Expect")) == "100-continue" から hasToken(r.Header.Get("Expect"), "100-continue") に変更。
    • wantsHttp10KeepAlive() 関数が strings.Contains(strings.ToLower(r.Header.Get("Connection")), "keep-alive") から hasToken(r.Header.Get("Connection"), "keep-alive") に変更。
    • 新しい関数 wantsClose() bool が追加。これは hasToken(r.Header.Get("Connection"), "close") を呼び出す。
    • 新しいヘルパー関数 hasToken(s, token string) bool が追加。
  2. src/pkg/net/http/serve_test.go:

    • testTcpConnectionCloses 関数が testTCPConnectionCloses にリネーム。
    • TestServeHTTP10Close, TestHandlersCanSetConnectionClose11, TestHandlersCanSetConnectionClose10 内で呼び出される関数名も testTCPConnectionCloses に変更。
    • 新しいテスト関数 TestClientCanClose() が追加。このテストは testTCPConnectionCloses を使用して、クライアントが Connection: close を指定した場合の動作を検証。
  3. src/pkg/net/http/server.go:

    • response 構造体の WriteHeader メソッド内の接続終了ロジックが変更。
    • !w.req.ProtoAtLeast(1, 1) の条件に || w.req.wantsClose() が追加。

コアとなるコードの解説

src/pkg/net/http/request.go

hasToken(s, token string) bool

func hasToken(s, token string) bool {
	if s == "" {
		return false
	}
	// TODO This is a poor implementation of the RFC. See http://golang.org/issue/3535
	return strings.Contains(strings.ToLower(s), token)
}

この関数は、HTTPヘッダの値 s の中に特定の token が含まれているかをチェックするためのヘルパーです。HTTPヘッダは通常、大文字小文字を区別しないため、strings.ToLower(s) で小文字に変換してから strings.Contains で部分文字列を検索しています。 しかし、TODO コメントにあるように、これはRFC(Request For Comments、インターネット標準を定義する文書)に準拠した厳密なトークン解析ではありません。HTTPヘッダのトークンはカンマで区切られたり、空白が含まれたりすることがあり、単なる部分文字列検索では誤った結果を返す可能性があります。例えば、Connection: keep-alive, close のようなヘッダで hasToken("keep-alive, close", "close")true を返しますが、Connection: not-close のようなヘッダでも hasToken("not-close", "close")true を返してしまう可能性があります。Issue 3535は、この hasToken のより堅牢な実装を求めるものです。

func (r *Request) expectsContinue() bool

func (r *Request) expectsContinue() bool {
	return hasToken(r.Header.Get("Expect"), "100-continue")
}

このメソッドは、リクエストの Expect ヘッダが 100-continue を含んでいるかどうかをチェックします。以前は strings.ToLowerstrings.Contains を直接使用していましたが、hasToken を利用することでコードの重複を避け、意図を明確にしています。

func (r *Request) wantsHttp10KeepAlive() bool

func (r *Request) wantsHttp10KeepAlive() bool {
	if r.ProtoMajor != 1 || r.ProtoMinor != 0 {
		return false
	}
	return hasToken(r.Header.Get("Connection"), "keep-alive")
}

このメソッドは、HTTP/1.0のリクエストが Connection: keep-alive ヘッダを送信しているかどうかをチェックします。HTTP/1.0ではKeep-Aliveはデフォルトではないため、明示的に keep-alive が指定されているかを確認します。ここでも hasToken が使用されています。

func (r *Request) wantsClose() bool

func (r *Request) wantsClose() bool {
	return hasToken(r.Header.Get("Connection"), "close")
}

新しく追加されたこのメソッドは、リクエストの Connection ヘッダが close トークンを含んでいるかどうかをチェックします。これにより、クライアントが接続の終了を明示的に要求しているかを簡単に判断できるようになります。

src/pkg/net/http/serve_test.go

func testTCPConnectionCloses(t *testing.T, req string, h Handler)

この関数は、HTTPリクエスト req を送信し、その後にTCP接続が閉じられることを検証するためのヘルパーテスト関数です。元の名前 testTcpConnectionCloses から testTCPConnectionCloses に変更されました。これはGoの命名規約(アクロニムは大文字で統一する)に合わせたものです。

func TestClientCanClose(t *testing.T)

// TestClientCanClose verifies that clients can also force a connection to close.
func TestClientCanClose(t *testing.T) {
	testTCPConnectionCloses(t, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n", HandlerFunc(func(w ResponseWriter, r *Request) {
		// Nothing.
	}))
}

この新しいテストケースは、クライアントがHTTP/1.1リクエストで Connection: close ヘッダを送信した場合に、サーバーがその要求を尊重して接続を閉じることを検証します。testTCPConnectionCloses を呼び出し、Connection: close ヘッダを含むリクエストを送信し、接続が閉じられることを確認します。これは、今回のコミットで再有効化された機能の動作保証となります。

src/pkg/net/http/server.go

func (w *response) WriteHeader(code int)

// ...
	} else if !w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() {
		w.closeAfterReply = true
	}
// ...

この変更は、サーバーがレスポンスヘッダを書き込む際に、接続を閉じるべきかどうかを判断するロジックに影響します。 変更前は、リクエストがHTTP/1.1未満(つまりHTTP/1.0)の場合にのみ w.closeAfterReply = true と設定されていました。 変更後は、以下のいずれかの条件が満たされた場合に接続を閉じるように設定されます。

  1. リクエストがHTTP/1.1未満である(!w.req.ProtoAtLeast(1, 1))。
  2. または、クライアントが Connection: close ヘッダを送信して接続の終了を要求している(w.req.wantsClose())。

この || w.req.wantsClose() の追加により、サーバーはクライアントの明示的な接続終了要求を適切に処理し、HTTPプロトコル仕様に準拠した動作を実現します。

関連リンク

参考にした情報源リンク