[インデックス 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つの箇所で変更が行われました。
-
(*chunkWriter).writeHeader
メソッド内: このメソッドは、HTTPレスポンスのヘッダを書き込む際に呼び出されます。以前は、w.closeAfterReply
が真(つまり、レスポンス後に接続を閉じる必要がある場合)かつConnection
ヘッダにclose
トークンが含まれていない場合、無条件にConnection: close
ヘッダを追加していました。 変更後、このConnection: close
ヘッダの追加は、リクエストのプロトコルバージョンがHTTP/1.1以上 (w.req.ProtoAtLeast(1, 1)
) である場合にのみ行われるようになりました。これにより、HTTP/1.0のリクエストに対しては、デフォルトの挙動(暗黙的な接続クローズ)に任せ、冗長なヘッダ送信を回避します。 -
(*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
の変更点
-
(*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のリクエストに対しては、この冗長なヘッダが送信されなくなります。 -
(*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.Dial
と fmt.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
が不要であることを確認するものです。
関連リンク
- Go Issue #5955: net/http: don't send Connection: close header in HTTP/1.0 responses
- Go CL 12435043: https://golang.org/cl/12435043