[インデックス 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.Contains
と hasToken
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
トークンが含まれているかどうかを適切に検出するロジックを追加し、それに基づいてサーバーが接続を閉じるべきかどうかを判断するようにしたことです。
具体的には、以下の変更が行われています。
-
hasToken
ヘルパー関数の導入:src/pkg/net/http/request.go
にhasToken(s, token string) bool
という新しい関数が追加されました。この関数は、与えられた文字列s
(HTTPヘッダーの値)が特定のtoken
(例: "close", "keep-alive", "100-continue")を含んでいるかをチェックします。既存のstrings.ToLower(s), token)
とstrings.Contains
を組み合わせたロジックをカプセル化しています。コメントには「これはRFCの貧弱な実装である」と明記されており、将来的な改善の余地があることが示されています(Issue #3535に関連)。 -
Request.wantsClose()
メソッドの追加:src/pkg/net/http/request.go
に(r *Request) wantsClose() bool
メソッドが追加されました。このメソッドは、リクエストのConnection
ヘッダーにclose
トークンが含まれている場合にtrue
を返します。これにより、クライアントが接続を閉じることを要求しているかどうかを簡単に判断できるようになります。 -
Request.expectsContinue()
とRequest.wantsHttp10KeepAlive()
の修正: 既存のexpectsContinue()
とwantsHttp10KeepAlive()
メソッドも、strings.Contains
を直接使用する代わりに、新しく導入されたhasToken
関数を使用するように変更されました。これにより、ヘッダー解析の一貫性が向上します。 -
サーバー側の接続管理ロジックの更新:
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
を要求した場合にサーバーが接続を閉じるようになります。
-
テストケースの追加と修正:
src/pkg/net/http/serve_test.go
に、クライアントがConnection: close
ヘッダーを送信した場合に接続が適切に閉じられることを検証する新しいテストケースTestClientCanClose
が追加されました。 また、既存のテスト関数名testTcpConnectionCloses
がtestTCPConnectionCloses
にリネームされ、それに伴い呼び出し箇所も修正されています。これは、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プロトコルの仕様、特に接続管理に関する部分に、より正確に準拠するための重要なステップです。
関連リンク
- Go Issue #3540: https://github.com/golang/go/issues/3540
- Go Code Review 5996044: https://golang.org/cl/5996044
参考にした情報源リンク
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 14.10 Connection): https://www.w3.org/Protocols/rfc2616/rfc2616.html#sec14.10
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 8.1 Persistent Connections): https://www.w3.org/Protocols/rfc2616/rfc2616.html#sec8.1
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 14.20 Expect): https://www.w3.org/Protocols/rfc2616/rfc2616.html#sec14.20
- Go Issue #3535: https://github.com/golang/go/issues/3535 (hasTokenのコメントで参照されているIssue)
- Goの命名規則 (Effective Go - Names): https://go.dev/doc/effective_go#names