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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける、HTTPコネクションのクローズ処理に関するバグ修正と改善を目的としています。具体的には、Request.Close または Response.Closetrue に設定されている場合、あるいはサーバーが Connection: close ヘッダーを送信した場合に、コネクションが適切にクローズされない問題を解決します。

コミット

net/http: 非キープアライブコネクションが正常にクローズされるように修正

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

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

元コミット内容

commit ccd63c3c19b1598d8e717c1575a01f77dd83a499
Author: James Gray <james@james4k.com>
Date:   Fri May 18 10:34:37 2012 -0700

    net/http: non-keepalive connections close successfully
    
    Connections did not close if Request.Close or Response.Close was true. This meant that if the user wanted the connection to close, or if the server requested it via "Connection: close", the connection would not be closed.
    
    Fixes #1967.
    
    R=golang-dev, rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/6201044
---
 src/pkg/net/http/transport.go      |  4 ++\n src/pkg/net/http/transport_test.go | 86 +++++++++++++++++++++++++++++++++++++-\n 2 files changed, 88 insertions(+), 2 deletions(-)\n\ndiff --git a/src/pkg/net/http/transport.go b/src/pkg/net/http/transport.go\nindex 024975946e..5f3d3fbfb1 100644\n--- a/src/pkg/net/http/transport.go\n+++ b/src/pkg/net/http/transport.go\n@@ -599,6 +599,10 @@ func (pc *persistConn) readLoop() {\n \t\t// before we race and peek on the underlying bufio reader.\n \t\tif waitForBodyRead != nil {\n \t\t\t<-waitForBodyRead\n+\t\t} else if !alive {\n+\t\t\t// If waitForBodyRead is nil, and we\'re not alive, we\n+\t\t\t// must close the connection before we leave the loop.\n+\t\t\tpc.close()\n \t\t}\n \t}\n }\ndiff --git a/src/pkg/net/http/transport_test.go b/src/pkg/net/http/transport_test.go\nindex a9e401de58..ebf4a8102d 100644\n--- a/src/pkg/net/http/transport_test.go\n+++ b/src/pkg/net/http/transport_test.go\n@@ -13,6 +13,7 @@ import (\n \t\"fmt\"\n \t\"io\"\n \t\"io/ioutil\"\n+\t\"net\"\n \t. \"net/http\"\n \t\"net/http/httptest\"\n \t\"net/url\"\n@@ -20,6 +21,7 @@ import (\n \t\"runtime\"\n \t\"strconv\"\n \t\"strings\"\n+\t\"sync\"\n \t\"testing\"\n \t\"time\"\n )\n@@ -35,6 +37,64 @@ var hostPortHandler = HandlerFunc(func(w ResponseWriter, r *Request) {\n \tw.Write([]byte(r.RemoteAddr))\n })\n \n+type testCloseConn struct {\n+\tnet.Conn\n+\tset *testConnSet\n+}\n+\n+func (conn *testCloseConn) Close() error {\n+\tconn.set.remove(conn)\n+\treturn conn.Conn.Close()\n+}\n+\n+type testConnSet struct {\n+\tset   map[net.Conn]bool\n+\tmutex sync.Mutex\n+}\n+\n+func (tcs *testConnSet) insert(c net.Conn) {\n+\ttcs.mutex.Lock()\n+\tdefer tcs.mutex.Unlock()\n+\ttcs.set[c] = true\n+}\n+\n+func (tcs *testConnSet) remove(c net.Conn) {\n+\ttcs.mutex.Lock()\n+\tdefer tcs.mutex.Unlock()\n+\t// just change to false, so we have a full set of opened connections\n+\ttcs.set[c] = false\n+}\n+\n+// some tests use this to manage raw tcp connections for later inspection\n+func makeTestDial() (*testConnSet, func(n, addr string) (net.Conn, error)) {\n+\tconnSet := &testConnSet{\n+\t\tset: make(map[net.Conn]bool),\n+\t}\n+\tdial := func(n, addr string) (net.Conn, error) {\n+\t\tc, err := net.Dial(n, addr)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\ttc := &testCloseConn{c, connSet}\n+\t\tconnSet.insert(tc)\n+\t\treturn tc, nil\n+\t}\n+\treturn connSet, dial\n+}\n+\n+func (tcs *testConnSet) countClosed() (closed, total int) {\n+\ttcs.mutex.Lock()\n+\tdefer tcs.mutex.Unlock()\n+\n+\ttotal = len(tcs.set)\n+\tfor _, open := range tcs.set {\n+\t\tif !open {\n+\t\t\tclosed += 1\n+\t\t}\n+\t}\n+\treturn\n+}\n+\n // Two subsequent requests and verify their response is the same.\n // The response from the server is our own IP:port\n func TestTransportKeepAlives(t *testing.T) {\n@@ -72,8 +132,12 @@ func TestTransportConnectionCloseOnResponse(t *testing.T) {\n \tts := httptest.NewServer(hostPortHandler)\n \tdefer ts.Close()\n \n+\tconnSet, testDial := makeTestDial()\n+\n \tfor _, connectionClose := range []bool{false, true} {\n-\t\ttr := &Transport{}\n+\t\ttr := &Transport{\n+\t\t\tDial: testDial,\n+\t\t}\n \t\tc := &Client{Transport: tr}\n \n \t\tfetch := func(n int) string {\n@@ -107,6 +171,13 @@ func TestTransportConnectionCloseOnResponse(t *testing.T) {\n \t\t\tt.Errorf(\"error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q\",\n \t\t\t\tconnectionClose, bodiesDiffer, body1, body2)\n \t\t}\n+\n+\t\ttr.CloseIdleConnections()\n+\t}\n+\n+\tclosed, total := connSet.countClosed()\n+\tif closed < total {\n+\t\tt.Errorf(\"%d out of %d tcp connections were not closed\", total-closed, total)\n \t}\n }\n \n@@ -114,8 +185,12 @@ func TestTransportConnectionCloseOnRequest(t *testing.T) {\n \tts := httptest.NewServer(hostPortHandler)\n \tdefer ts.Close()\n \n+\tconnSet, testDial := makeTestDial()\n+\n \tfor _, connectionClose := range []bool{false, true} {\n-\t\ttr := &Transport{}\n+\t\ttr := &Transport{\n+\t\t\tDial: testDial,\n+\t\t}\n \t\tc := &Client{Transport: tr}\n \n \t\tfetch := func(n int) string {\n@@ -149,6 +224,13 @@ func TestTransportConnectionCloseOnRequest(t *testing.T) {\n \t\t\tt.Errorf(\"error in connectionClose=%v. unexpected bodiesDiffer=%v; body1=%q; body2=%q\",\n \t\t\t\tconnectionClose, bodiesDiffer, body1, body2)\n \t\t}\n+\n+\t\ttr.CloseIdleConnections()\n+\t}\n+\n+\tclosed, total := connSet.countClosed()\n+\tif closed < total {\n+\t\tt.Errorf(\"%d out of %d tcp connections were not closed\", total-closed, total)\n \t}\n }\n \n```

## 変更の背景

このコミットは、Go言語の `net/http` パッケージにおける重要なバグ修正に対応しています。具体的には、Issue #1967 で報告された問題に対処しています。この問題は、HTTPクライアントが明示的にコネクションをクローズしたいと要求した場合(`Request.Close = true`)や、HTTPサーバーがレスポンスヘッダーでコネクションのクローズを指示した場合(`Connection: close`)に、`net/http` の `Transport` が基盤となるTCPコネクションを適切にクローズしないというものでした。

HTTP/1.1では、デフォルトでコネクションの再利用(キープアライブ)が有効になっていますが、特定のシナリオではコネクションをクローズする必要があります。例えば、クライアントが単一のリクエストのみを送信し、その後コネクションを解放したい場合や、サーバーがリソースの制約やプロトコルの都合上、コネクションを維持できない場合に `Connection: close` ヘッダーを送信します。

このバグが存在すると、これらのシナリオでコネクションが閉じられず、以下のような問題が発生する可能性がありました。

*   **リソースリーク**: クライアント側で不要なTCPコネクションが開きっぱなしになり、ファイルディスクリプタやメモリなどのシステムリソースを消費し続ける。
*   **サーバー側の負荷増大**: サーバー側でも不要なコネクションが維持され、リソースを圧迫し、スケーラビリティに影響を与える。
*   **予期せぬ動作**: コネクションが閉じられることを期待しているアプリケーションが、閉じられないことによってデッドロックやタイムアウトなどの予期せぬ動作を引き起こす。
*   **プロトコル違反**: `Connection: close` ヘッダーが無視されることで、HTTPプロトコルの意図に反する動作となる。

このコミットは、これらの問題を解決し、`net/http` パッケージがHTTPプロトコルの仕様に厳密に従い、コネクション管理をより堅牢に行うことを保証します。

## 前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

### 1. HTTP/1.1におけるコネクション管理

*   **キープアライブ (Keep-Alive)**: HTTP/1.1のデフォルトの動作です。クライアントとサーバー間で一度TCPコネクションを確立すると、複数のHTTPリクエスト/レスポンスをそのコネクション上で送受信できます。これにより、コネクション確立のオーバーヘッド(TCPハンドシェイク、TLSハンドシェイクなど)を削減し、パフォーマンスを向上させます。
*   **非キープアライブ (Non-Keep-Alive)**: 各HTTPリクエスト/レスポンスの後にTCPコネクションをクローズする動作です。これは、HTTP/1.0のデフォルト動作でした。HTTP/1.1でも、特定のヘッダーを使用することで非キープアライブ動作を明示的に要求できます。
*   **`Connection` ヘッダー**: HTTPヘッダーの一つで、コネクションに関する制御情報を提供します。
    *   `Connection: close`: 送信側が、現在のリクエスト/レスポンスの処理後にコネクションをクローズすることを意図していることを示します。
    *   `Connection: keep-alive`: コネクションを維持することを意図していることを示します(HTTP/1.1では通常省略されますが、HTTP/1.0でキープアライブを要求する際に使用されました)。
*   **`Request.Close` と `Response.Close`**: Goの `net/http` パッケージにおけるフィールドで、それぞれクライアントがリクエスト送信後にコネクションをクローズしたいか、サーバーがレスポンス送信後にコネクションをクローズしたいかを示すフラグです。これらが `true` に設定されている場合、コネクションはクローズされるべきです。

### 2. Go言語の `net/http` パッケージ

*   **`http.Client`**: HTTPリクエストを送信するためのクライアントです。
*   **`http.Transport`**: `http.Client` の内部で使用され、実際のHTTPリクエストの送信、コネクションの管理(キープアライブ、プロキシなど)、TLSハンドシェイクなどを担当します。`Transport` はコネクションプールを管理し、効率的なコネクション再利用を可能にします。
*   **`persistConn`**: `http.Transport` の内部で使用される構造体で、単一の永続的なHTTPコネクション(TCPコネクション)を表します。この構造体は、リクエストの送信、レスポンスの受信、コネクションのライフサイクル管理を行います。
*   **`readLoop()`**: `persistConn` のメソッドで、バックグラウンドでコネクションからレスポンスを読み取るためのゴルーチン内で実行されます。このループは、新しいリクエストが来るのを待ったり、レスポンスボディの読み取りが完了するのを待ったりします。

### 3. ゴルーチンとチャネル

*   **ゴルーチン (Goroutine)**: Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。
*   **チャネル (Channel)**: ゴルーチン間で安全にデータを送受信するための通信メカニズムです。チャネルは、ゴルーチン間の同期にも使用されます。このコミットでは、`waitForBodyRead` というチャネルが使用されており、レスポンスボディの読み取りが完了するのを待つために利用されています。

### 4. テスト駆動開発 (TDD) とテストコード

*   Go言語の標準ライブラリは、堅牢なテストコードによって品質が保証されています。このコミットでも、バグ修正と同時に、その修正が正しく機能することを検証するための新しいテストケースが追加されています。
*   `net.Conn`: Go言語のネットワークコネクションを表すインターフェースです。TCPコネクションなどもこのインターフェースを実装します。
*   `httptest.NewServer`: テスト目的でHTTPサーバーを簡単に起動するためのユーティリティ関数です。

## 技術的詳細

このコミットの核心は、`net/http/transport.go` 内の `persistConn` 構造体の `readLoop()` メソッドの修正にあります。

`readLoop()` は、`persistConn` が管理するHTTPコネクション上で、レスポンスを継続的に読み取るためのゴルーチンです。このループは、新しいリクエストが来るのを待機し、レスポンスヘッダーを解析し、レスポンスボディの読み取りを処理します。

修正前のコードでは、`readLoop()` は `waitForBodyRead` チャネルが閉じられるのを待つか、または `alive` フラグが `false` になるまでループを継続していました。しかし、`waitForBodyRead` が `nil` であり、かつ `alive` が `false` の場合に、コネクションを明示的にクローズするロジックが欠けていました。

具体的には、以下の条件が揃った場合に問題が発生していました。

1.  `waitForBodyRead` が `nil` である: これは、レスポンスボディの読み取りが不要な場合(例: HEADリクエスト)や、既にボディが読み終わっている場合などに発生します。
2.  `alive` が `false` である: これは、クライアントが `Request.Close = true` を設定した場合、またはサーバーが `Connection: close` ヘッダーを送信した場合に設定されます。つまり、コネクションをクローズすべきであると判断された状態です。

この二つの条件が同時に満たされた場合、`readLoop()` は `waitForBodyRead` を待つこともなく、また `alive` が `false` であるためループを継続する意味もありませんでした。しかし、明示的な `pc.close()` 呼び出しがなかったため、コネクションが閉じられずにリソースリークが発生していました。

このコミットでは、この特定のケースを捕捉し、`pc.close()` を呼び出すことで、コネクションが適切にクローズされるように修正しています。

また、`transport_test.go` には、この修正を検証するための包括的なテストケースが追加されています。
*   `testCloseConn` と `testConnSet` というヘルパー構造体が導入され、`net.Conn` の `Close()` メソッドが呼び出されたかどうかを追跡できるようになっています。これにより、テスト中に実際にTCPコネクションが閉じられたかどうかを検証できます。
*   `makeTestDial` 関数は、カスタムの `Dial` 関数を生成し、`http.Transport` に設定することで、テスト対象のコネクションのライフサイクルを監視できるようにします。
*   `TestTransportConnectionCloseOnResponse` と `TestTransportConnectionCloseOnRequest` という新しいテスト関数が追加され、それぞれレスポンス側とリクエスト側でコネクションクローズが要求された場合に、コネクションが正しく閉じられることを検証しています。これらのテストは、`Connection: close` ヘッダーの有無や `Request.Close` フラグの設定を変えながら、複数回リクエストを送信し、最終的に開かれたTCPコネクションがすべて閉じられていることを確認します。

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

### `src/pkg/net/http/transport.go`

```diff
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -599,6 +599,10 @@ func (pc *persistConn) readLoop() {
 		// before we race and peek on the underlying bufio reader.
 		if waitForBodyRead != nil {
 			<-waitForBodyRead
+		} else if !alive {
+			// If waitForBodyRead is nil, and we're not alive, we
+			// must close the connection before we leave the loop.
+			pc.close()
 		}
 	}
 }

src/pkg/net/http/transport_test.go

テストコードの変更は多岐にわたりますが、主要な追加は以下の通りです。

  • testCloseConn 構造体と Close() メソッドの追加。
  • testConnSet 構造体と insert(), remove(), countClosed() メソッドの追加。
  • makeTestDial() 関数の追加。
  • TestTransportConnectionCloseOnResponse テスト関数の追加。
  • TestTransportConnectionCloseOnRequest テスト関数の追加。

これらのテストは、http.TransportDial フィールドにカスタムのダイヤラーを設定し、コネクションの開閉を監視することで、バグが修正されたことを検証しています。

コアとなるコードの解説

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

readLoop() メソッド内の変更は非常に小さいですが、その影響は大きいです。

		if waitForBodyRead != nil {
			<-waitForBodyRead
		} else if !alive {
			// If waitForBodyRead is nil, and we're not alive, we
			// must close the connection before we leave the loop.
			pc.close()
		}
  • if waitForBodyRead != nil: これは、レスポンスボディがまだ完全に読み込まれていない場合、またはボディの読み込みを待機する必要がある場合に実行される既存のロジックです。<-waitForBodyRead は、ボディの読み込みが完了するまでゴルーチンをブロックします。
  • else if !alive: この else if ブロックが新たに追加された部分です。
    • waitForBodyReadnil であるということは、ボディの読み込みが不要であるか、既に完了していることを意味します。
    • !alive は、この persistConn がもはやキープアライブ状態ではなく、クローズされるべきであることを示します。これは、Request.Close = trueConnection: close ヘッダーによって設定されます。
    • この両方の条件が満たされた場合、つまり「ボディの読み込みを待つ必要がなく、かつコネクションをクローズすべきである」という状況で、以前はコネクションが閉じられずにループを抜けてしまっていました。
    • pc.close(): この行が追加されたことで、上記の条件が満たされた場合に persistConn が管理する基盤となるTCPコネクションが明示的にクローズされるようになりました。これにより、リソースリークが防止され、HTTPプロトコルの意図通りにコネクションが管理されるようになります。

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

テストコードは、この修正が正しく機能することを保証するためのものです。

  • testCloseConntestConnSet:
    • testCloseConnnet.Conn をラップし、その Close() メソッドが呼び出された際に、testConnSet にその情報を記録します。
    • testConnSet は、開かれたコネクションと閉じられたコネクションの状態を追跡するためのマップとミューテックス(並行アクセス保護のため)を保持します。
    • insert() はコネクションが開かれたときにマップに追加し、remove() はコネクションが閉じられたときにマップ内の状態を更新します。
    • countClosed() は、開かれたコネクションと閉じられたコネクションの数を返します。
  • makeTestDial():
    • この関数は、http.TransportDial フィールドに設定できるカスタムのダイヤル関数を生成します。
    • このカスタムダイヤル関数は、実際の net.Dial を呼び出してTCPコネクションを確立した後、そのコネクションを testCloseConn でラップし、testConnSet に登録します。これにより、テスト中に開かれたすべてのコネクションを監視できるようになります。
  • TestTransportConnectionCloseOnResponseTestTransportConnectionCloseOnRequest:
    • これらのテストは、httptest.NewServer を使用してテスト用のHTTPサーバーを起動します。
    • http.TransportDial フィールドに makeTestDial() で生成したカスタムダイヤル関数を設定します。
    • ループ内で connectionClose フラグを falsetrue に切り替えながら、リクエストを送信します。
      • connectionClose = true の場合、Request.Close = true を設定するか、サーバーが Connection: close ヘッダーを返すようにします。
    • 各リクエストの後、tr.CloseIdleConnections() を呼び出して、アイドル状態のコネクションをクローズさせます。
    • ループの最後に、connSet.countClosed() を呼び出して、開かれたすべてのコネクションが適切に閉じられたかどうかを検証します。もし閉じられていないコネクションがあれば、テストは失敗します。

これらのテストは、net/http パッケージがHTTPプロトコルの仕様に従い、ユーザーの意図やサーバーの指示に基づいてコネクションを正しくクローズすることを、厳密に検証しています。

関連リンク

参考にした情報源リンク