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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける、HTTPクライアントがKeep-Alive接続を無効にできるようにする機能追加と関連する修正を含んでいます。具体的には、クライアントが Connection: close ヘッダーを送信することで、サーバーに対して現在のリクエスト処理後に接続を閉じるように要求できるようになります。

コミット

commit cc5cbee1b6a942d2f55c01697f464be9d2a56818
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date:   Mon Apr 23 22:00:16 2012 -0300

    net/http: allow clients to disable keep-alive
    
    Fixes #3540.
    
    R=golang-dev, bradfitz, gustavo
    CC=golang-dev
    https://golang.org/cl/5996044

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

https://github.com/golang/go/commit/cc5cbee1b6a942d2f55c01697f464be9d2a56818

元コミット内容

net/http: allow clients to disable keep-alive (net/http: クライアントがKeep-Aliveを無効にできるようにする)

Fixes #3540. (Issue #3540を修正)

変更の背景

この変更の背景には、Goの net/http パッケージがHTTPの Connection ヘッダー、特に Connection: close の扱いに関する既存の課題がありました。Issue #3540("Connection header in request is ignored by the http server")で議論されているように、GoのHTTPサーバーは、クライアントからのリクエストに含まれる Connection: close ヘッダーを適切に解釈し、それに応じて接続を閉じるべきであるというRFCの要件を満たしていませんでした。

HTTP/1.1では、デフォルトでKeep-Alive接続が有効になっています。これは、複数のリクエスト/レスポンスを同じTCP接続上で送受信することで、接続確立のオーバーヘッドを削減し、パフォーマンスを向上させるための仕組みです。しかし、クライアント側で特定の理由(例えば、リソースの解放、サーバーへの負荷軽減、または単一のリクエストで接続を終了させたい場合など)により、このKeep-Alive動作を無効にし、リクエスト完了後に接続を即座に閉じたい場合があります。

このコミット以前は、GoのHTTPサーバーはクライアントが Connection: close ヘッダーを送信しても、それを無視してKeep-Alive接続を維持しようとする可能性がありました。これにより、クライアントの意図に反して接続が維持され、予期せぬ動作やリソースリークが発生する可能性がありました。このコミットは、この問題を解決し、HTTP仕様に準拠した振る舞いを実現することを目的としています。

前提知識の解説

HTTP Keep-Alive (持続的接続)

HTTP/1.0では、各HTTPリクエスト/レスポンスのペアごとに新しいTCP接続が確立され、リクエスト完了後に閉じられるのが一般的でした。これは、短いリクエストが多数発生する場合に、TCP接続の確立(3ウェイハンドシェイク)と切断(4ウェイハンドシェイク)のオーバーヘッドが大きくなるという問題がありました。

HTTP/1.1では、この問題を解決するために「持続的接続(Persistent Connections)」、一般に「Keep-Alive」と呼ばれる機能が導入されました。これにより、一度確立されたTCP接続を複数のHTTPリクエスト/レスポンスで再利用できるようになり、以下の利点があります。

  • パフォーマンス向上: TCP接続の確立・切断のオーバーヘッドが削減されます。
  • ネットワークリソースの効率化: 接続数が減るため、サーバーとクライアント双方のリソース消費が抑えられます。
  • 輻輳制御の改善: TCPの輻輳制御がより効果的に機能し、スループットが向上します。

Keep-AliveはHTTP/1.1のデフォルトの動作ですが、HTTP/1.0でも Connection: Keep-Alive ヘッダーを送信することで明示的に有効にできます。

Connection ヘッダー

HTTPの Connection ヘッダーは、現在の接続に固有のオプションを指定するために使用されます。特に重要なのは以下の2つの値です。

  • Connection: close: 送信側は、現在のリクエスト/レスポンスの完了後に接続を閉じることを意図していることを示します。これは、クライアントがサーバーに接続を閉じるように要求する場合や、サーバーがクライアントに接続を閉じるように指示する場合に使用されます。
  • Connection: keep-alive: HTTP/1.0において、持続的接続を要求するために使用されます。HTTP/1.1ではデフォルトでKeep-Aliveが有効なため、通常は明示的に指定する必要はありませんが、互換性のために使用されることがあります。

RFC 2616 (HTTP/1.1の仕様) では、Connection ヘッダーに close が含まれている場合、その接続は現在のリクエストの完了後に閉じられるべきであると明確に規定されています。

Expect: 100-continue ヘッダー

Expect: 100-continue ヘッダーは、クライアントが大きなリクエストボディ(例えば、ファイルアップロード)を送信する前に、サーバーがそのリクエストを受け入れる準備ができているかを確認するために使用されます。クライアントはまずヘッダーのみを送信し、サーバーが 100 Continue ステータスコードを返した場合にのみ、残りのリクエストボディを送信します。これにより、サーバーがリクエストを拒否する場合に、不要なデータ転送を避けることができます。

strings.ContainshasToken

Go言語の strings.Contains 関数は、ある文字列が別の文字列の部分文字列として含まれているかどうかをチェックします。 このコミットでは、HTTPヘッダーの値(例: Connection ヘッダー)を解析する際に、strings.Contains を直接使用する代わりに hasToken という新しいヘルパー関数を導入しています。これは、HTTPヘッダーのトークンリスト(例: Connection: close, Upgrade のようにカンマ区切りで複数の値が指定される場合)をより正確に解析するための一歩です。strings.Contains は単純な部分文字列マッチングであるため、例えば Connection: keep-alive-foo のような値に対しても keep-alive が含まれると誤判定する可能性があります。hasToken は、より厳密なトークン解析の必要性を示唆しています(ただし、コメントにあるように、この時点ではまだRFCに完全に準拠した実装ではありません)。

技術的詳細

このコミットの主要な技術的変更点は、HTTPリクエストの Connection ヘッダーに close トークンが含まれているかどうかを適切に検出するロジックを追加し、それに基づいてサーバーが接続を閉じるべきかどうかを判断するようにしたことです。

具体的には、以下の変更が行われています。

  1. hasToken ヘルパー関数の導入: src/pkg/net/http/request.gohasToken(s, token string) bool という新しい関数が追加されました。この関数は、与えられた文字列 s(HTTPヘッダーの値)が特定の token(例: "close", "keep-alive", "100-continue")を含んでいるかをチェックします。既存の strings.ToLower(s), token)strings.Contains を組み合わせたロジックをカプセル化しています。コメントには「これはRFCの貧弱な実装である」と明記されており、将来的な改善の余地があることが示されています(Issue #3535に関連)。

  2. Request.wantsClose() メソッドの追加: src/pkg/net/http/request.go(r *Request) wantsClose() bool メソッドが追加されました。このメソッドは、リクエストの Connection ヘッダーに close トークンが含まれている場合に true を返します。これにより、クライアントが接続を閉じることを要求しているかどうかを簡単に判断できるようになります。

  3. Request.expectsContinue()Request.wantsHttp10KeepAlive() の修正: 既存の expectsContinue()wantsHttp10KeepAlive() メソッドも、strings.Contains を直接使用する代わりに、新しく導入された hasToken 関数を使用するように変更されました。これにより、ヘッダー解析の一貫性が向上します。

  4. サーバー側の接続管理ロジックの更新: src/pkg/net/http/server.go(w *response) WriteHeader(code int) メソッド内で、レスポンスヘッダーを書き込む際の接続管理ロジックが更新されました。 以前は、HTTP/1.1未満のプロトコルバージョン(HTTP/1.0など)の場合にのみ w.closeAfterReply = true が設定され、接続が閉じられていました。 変更後、!w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() という条件が追加されました。これは、以下のいずれかの条件が満たされた場合に接続を閉じることを意味します。

    • リクエストのプロトコルバージョンがHTTP/1.1未満である(HTTP/1.0など)。
    • クライアントが Connection: close ヘッダーを送信して接続を閉じることを要求している。 この修正により、HTTP/1.1リクエストであっても、クライアントが明示的に Connection: close を要求した場合にサーバーが接続を閉じるようになります。
  5. テストケースの追加と修正: src/pkg/net/http/serve_test.go に、クライアントが Connection: close ヘッダーを送信した場合に接続が適切に閉じられることを検証する新しいテストケース TestClientCanClose が追加されました。 また、既存のテスト関数名 testTcpConnectionClosestestTCPConnectionCloses にリネームされ、それに伴い呼び出し箇所も修正されています。これは、Goの命名規則(アクロニムは大文字で統一)に合わせたものと考えられます。

これらの変更により、Goの net/http パッケージはHTTP仕様により厳密に準拠し、クライアントがKeep-Alive接続の動作をより細かく制御できるようになりました。

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

src/pkg/net/http/request.go

--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -732,12 +732,24 @@ func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, e
 }
 
 func (r *Request) expectsContinue() bool {
-	return strings.ToLower(r.Header.Get("Expect")) == "100-continue"
+	return hasToken(r.Header.Get("Expect"), "100-continue")
 }
 
 func (r *Request) wantsHttp10KeepAlive() bool {
 	if r.ProtoMajor != 1 || r.ProtoMinor != 0 {
 		return false
 	}
-	return strings.Contains(strings.ToLower(r.Header.Get("Connection")), "keep-alive")
+	return hasToken(r.Header.Get("Connection"), "keep-alive")
+}
+
+func (r *Request) wantsClose() bool {
+	return hasToken(r.Header.Get("Connection"), "close")
+}
+
+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)
 }

src/pkg/net/http/serve_test.go

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -370,7 +370,7 @@ func TestIdentityResponse(t *testing.T) {
 	})\n }\n \n-func testTcpConnectionCloses(t *testing.T, req string, h Handler) {
+func testTCPConnectionCloses(t *testing.T, req string, h Handler) {
 	s := httptest.NewServer(h)\n 	defer s.Close()\n \n@@ -410,21 +410,28 @@ func testTcpConnectionCloses(t *testing.T, req string, h Handler) {
 \n // TestServeHTTP10Close verifies that HTTP/1.0 requests won't be kept alive.\n func TestServeHTTP10Close(t *testing.T) {
-\ttestTcpConnectionCloses(t, "GET / HTTP/1.0\\r\\n\\r\\n", HandlerFunc(func(w ResponseWriter, r *Request) {
+\ttestTCPConnectionCloses(t, "GET / HTTP/1.0\\r\\n\\r\\n", HandlerFunc(func(w ResponseWriter, r *Request) {
 \t\tServeFile(w, r, "testdata/file")
 \t}))
 }\n \n+// TestClientCanClose verifies that clients can also force a connection to close.\n+func TestClientCanClose(t *testing.T) {
+\ttestTCPConnectionCloses(t, "GET / HTTP/1.1\\r\\nConnection: close\\r\\n\\r\\n", HandlerFunc(func(w ResponseWriter, r *Request) {
+\t\t// Nothing.\n+\t}))
+}\n+\n // TestHandlersCanSetConnectionClose verifies that handlers can force a connection to close,\n // even for HTTP/1.1 requests.\n func TestHandlersCanSetConnectionClose11(t *testing.T) {
-\ttestTcpConnectionCloses(t, "GET / HTTP/1.1\\r\\n\\r\\n", HandlerFunc(func(w ResponseWriter, r *Request) {
+\ttestTCPConnectionCloses(t, "GET / HTTP/1.1\\r\\n\\r\\n", HandlerFunc(func(w ResponseWriter, r *Request) {
 \t\tw.Header().Set("Connection", "close")
 \t}))
 }\n \n func TestHandlersCanSetConnectionClose10(t *testing.T) {
-\ttestTcpConnectionCloses(t, "GET / HTTP/1.0\\r\\nConnection: keep-alive\\r\\n\\r\\n", HandlerFunc(func(w ResponseWriter, r *Request) {
+\ttestTCPConnectionCloses(t, "GET / HTTP/1.0\\r\\nConnection: keep-alive\\r\\n\\r\\n", HandlerFunc(func(w ResponseWriter, r *Request) {
 \t\tw.Header().Set("Connection", "close")
 \t}))
 }

src/pkg/net/http/server.go

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -303,8 +303,7 @@ func (w *response) WriteHeader(code int) {
 		if !connectionHeaderSet {
 			w.header.Set("Connection", "keep-alive")
 		}
-	} else if !w.req.ProtoAtLeast(1, 1) {
-		// Client did not ask to keep connection alive.
+	} else if !w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() {
 		w.closeAfterReply = true
 	}
 

コアとなるコードの解説

src/pkg/net/http/request.go の変更

  • hasToken 関数の追加: この関数は、HTTPヘッダーの値(s)が特定のトークン(token)を含んでいるかを、大文字・小文字を区別せずにチェックします。strings.Contains(strings.ToLower(s), token) のロジックを抽象化し、再利用性を高めています。コメントにあるように、これはRFCの完全な実装ではないため、将来的に改善される可能性があります。HTTPヘッダーのトークン解析は、空白やカンマ区切りなど、より複雑なルールを持つため、単純な Contains では不十分な場合があります。

  • expectsContinue()wantsHttp10KeepAlive() の修正: これらの関数は、それぞれ Expect: 100-continue ヘッダーとHTTP/1.0の Connection: keep-alive ヘッダーの存在をチェックします。変更前は strings.Contains を直接使用していましたが、hasToken 関数を使用するように変更され、コードの重複が減り、一貫性が保たれています。

  • wantsClose() 関数の追加: この新しい関数は、リクエストの Connection ヘッダーに close トークンが含まれているかどうかを判断します。これにより、サーバーはクライアントが接続を閉じることを要求しているかどうかを明確に知ることができます。

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

  • response.WriteHeader メソッド内の接続管理ロジックの更新: このメソッドは、HTTPレスポンスのヘッダーを書き込む際に、接続をKeep-Aliveにするか、それともリクエスト完了後に閉じるかを決定する重要な部分です。 変更前は、HTTP/1.1未満のプロトコル(主にHTTP/1.0)の場合にのみ w.closeAfterReply = true が設定され、接続が閉じられていました。これは、HTTP/1.0ではKeep-Aliveがデフォルトではないためです。 変更後、条件が !w.req.ProtoAtLeast(1, 1) || w.req.wantsClose() となりました。
    • !w.req.ProtoAtLeast(1, 1): これは以前のロジックと同じで、リクエストがHTTP/1.0の場合に true となります。
    • w.req.wantsClose(): これは新しく追加された条件で、クライアントが Connection: close ヘッダーを送信している場合に true となります。 この || (OR) 条件により、HTTP/1.1リクエストであっても、クライアントが明示的に Connection: close を要求した場合には、サーバーはリクエスト処理後に接続を閉じるように動作します。これにより、HTTP仕様への準拠が強化され、クライアントの意図が尊重されるようになりました。

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

  • testTcpConnectionCloses から testTCPConnectionCloses へのリネーム: Goの慣例に従い、アクロニム(TCP)は大文字で統一されるように関数名が変更されました。これは機能的な変更ではなく、コードスタイルの改善です。

  • TestClientCanClose テストケースの追加: この新しいテストは、HTTP/1.1リクエストで Connection: close ヘッダーを送信した場合に、サーバーが接続を適切に閉じることを検証します。これは、このコミットの主要な機能追加が正しく動作することを確認するための重要なテストです。テストは testTCPConnectionCloses ヘルパー関数を使用し、指定されたリクエストを送信した後にTCP接続が閉じられることを確認します。

これらの変更は、Goの net/http パッケージがHTTPプロトコルの仕様、特に接続管理に関する部分に、より正確に準拠するための重要なステップです。

関連リンク

参考にした情報源リンク