[インデックス 16367] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージのテストスイートに、Hijacker
インターフェースの動作を検証するための新たなテストケースを追加するものです。具体的には、http.ResponseWriter
が提供する Hijack
メソッドを呼び出した後に、バッファリングされたライターと基盤となるコネクションの両方への書き込みが正しく機能するかを確認します。また、既存のテストコードにおけるHTTPリクエスト文字列の生成方法を共通のヘルパー関数に置き換えることで、コードの重複を排除し、可読性と保守性を向上させています。
コミット
commit 950fa9353126a784ce0f4cde8df1d1c16af0e6c4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue May 21 18:43:28 2013 -0700
net/http: add another Hijacker test
R=dsymonds
CC=gobot, golang-dev
https://golang.org/cl/9570043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/950fa9353126a784ce0f4cde8df1d1c16af0e6c4
元コミット内容
net/http: add another Hijacker test
R=dsymonds
CC=gobot, golang-dev
https://golang.org/cl/9570043
変更の背景
Goの net/http
パッケージは、HTTPサーバーとクライアントを構築するための強力な機能を提供します。その中でも Hijacker
インターフェースは、HTTPハンドラが基盤となるTCPコネクションを「乗っ取る(hijack)」ことを可能にする重要な機能です。これは、WebSocketのようなHTTP以外のプロトコルに切り替える場合や、カスタムのプロトコルハンドリングを実装する場合に必要となります。
このコミットの背景には、Hijack
メソッドが呼び出された後のコネクションの振る舞い、特に Hijack
が返す bufio.ReadWriter
と net.Conn
の両方への書き込みが期待通りに行われることを保証する必要がありました。既存のテストではこの特定のシナリオが十分にカバーされていなかったため、堅牢性を高めるために新たなテストケース TestWriteAfterHijack
が追加されました。
また、既存のテストコード内でHTTPリクエストのバイト列を生成する際に、strings.Replace
を用いて \n
を \r\n
に変換する処理が複数箇所で重複していました。これはコードの冗長性を生み、将来的な変更やメンテナンスを困難にする可能性がありました。この重複を解消し、コードの品質を向上させるために、共通のヘルパー関数 reqBytes
が導入されました。
前提知識の解説
Go言語の net/http
パッケージ
Go言語の net/http
パッケージは、HTTPプロトコルを扱うための標準ライブラリです。これには、HTTPサーバー、クライアント、リクエスト、レスポンス、ハンドラなどの機能が含まれます。
http.Handler
インターフェース: HTTPリクエストを処理するためのインターフェースで、ServeHTTP(ResponseWriter, *Request)
メソッドを持ちます。http.ResponseWriter
インターフェース: HTTPレスポンスをクライアントに書き込むためのインターフェースです。ヘッダーの設定やボディの書き込みを行います。http.Request
構造体: クライアントからのHTTPリクエストを表す構造体です。URL、ヘッダー、ボディなどの情報を含みます。
http.Hijacker
インターフェース
http.Hijacker
は http.ResponseWriter
が実装できるオプションのインターフェースです。このインターフェースを実装している ResponseWriter
は、Hijack()
メソッドを提供します。
Hijack() (net.Conn, *bufio.ReadWriter, error)
: このメソッドを呼び出すと、HTTPサーバーは基盤となるネットワークコネクションの制御をハンドラに引き渡します。これにより、ハンドラはHTTPプロトコルから離れて、生のTCPコネクションを直接操作できるようになります。net.Conn
: 基盤となる生のネットワークコネクションです。これを通じて直接データの読み書きが可能です。*bufio.ReadWriter
:Hijack()
が呼び出された時点で、http.ResponseWriter
が内部的にバッファリングしていたデータ(例えば、部分的に読み込まれたリクエストボディや、まだ送信されていないレスポンスヘッダーなど)を含むbufio.ReadWriter
オブジェクトです。このオブジェクトを通じて、バッファリングされていたデータを読み書きしたり、新しいデータをバッファリングしてフラッシュしたりできます。
HTTPプロトコルの行末
HTTP/1.1プロトコルでは、ヘッダーやメッセージボディの各行はCRLF(Carriage Return + Line Feed、\r\n
)で区切られることが規定されています。一般的なテキストファイルやプログラミング言語ではLF(Line Feed、\n
)が改行コードとして使われることが多いですが、HTTP通信においては \r\n
が必須です。
Go言語のテスト
Go言語には、標準でテストフレームワークが組み込まれています。
testing
パッケージ: テスト関数(TestXxx
)やベンチマーク関数(BenchmarkXxx
)を定義するために使用されます。t.Error()
/t.Errorf()
: テスト失敗を報告するためのメソッドです。bytes.Buffer
: バイト列を効率的に操作するためのバッファです。このコミットでは、モックコネクションの出力先として使用されています。bufio.Reader
/bufio.Writer
: バッファリングされたI/O操作を提供し、効率的な読み書きを可能にします。
技術的詳細
このコミットの技術的詳細は、主に以下の2点に集約されます。
-
reqBytes
ヘルパー関数の導入:- 目的: HTTPリクエスト文字列(
\n
区切り)を、HTTPプロトコルで要求される\r\n
区切りのバイト列に変換し、末尾に\r\n\r\n
を追加する。 - 実装:
strings.Replace(strings.TrimSpace(req), "\n", "\r\n", -1) + "\r\n\r\n"
strings.TrimSpace(req)
: 入力文字列の先頭と末尾の空白を削除します。これにより、リクエスト文字列の整形が容易になります。strings.Replace(..., "\n", "\r\n", -1)
: 文字列内のすべての\n
を\r\n
に置換します。-1
はすべての出現箇所を置換することを意味します。+ "\r\n\r\n"
: HTTPリクエストのヘッダーとボディの区切り、およびリクエスト全体の終端を示すために、末尾に空行(\r\n
)を2つ追加します。
- 効果: 既存の複数のテスト(
TestCloseNotifierChanLeak
,TestHeaderToWire
, 各種BenchmarkServerFakeConn
など)で重複していたリクエストバイト列生成ロジックを共通化し、コードの簡潔性と保守性を向上させました。
- 目的: HTTPリクエスト文字列(
-
TestWriteAfterHijack
テストの追加:- 目的:
http.Hijacker
インターフェースのHijack()
メソッドを呼び出した後、返されたbufio.ReadWriter
とnet.Conn
の両方への書き込みが正しく行われることを検証します。 - テストシナリオ:
- モックの
rwTestConn
を作成し、そのWriter
をbytes.Buffer
に設定して、書き込まれたデータをキャプチャできるようにします。 http.HandlerFunc
を定義し、その中でrw.(Hijacker).Hijack()
を呼び出してコネクションを乗っ取ります。- 新しいゴルーチンを起動し、乗っ取ったコネクションの
bufio.ReadWriter
(bufrw
) に[hijack-to-bufw]
を書き込み、Flush()
します。 - 次に、生の
net.Conn
(conn
) に[hijack-to-conn]
を書き込み、Close()
します。 - メインのテストゴルーチンは、コネクションが閉じられるのを待ち、その後、
bytes.Buffer
に書き込まれた内容が[hijack-to-bufw][hijack-to-conn]
となっていることをアサートします。
- モックの
- 検証ポイント:
Hijack()
後もbufrw
を通じた書き込みが機能し、その内容が基盤のコネクションにフラッシュされること。bufrw
のフラッシュ後、conn
を通じた直接の書き込みも機能すること。- これら2つの書き込みが期待通りの順序で結合されて出力されること。
- 重要性:
Hijack()
はHTTPサーバーの制御をアプリケーションに完全に委譲するため、この後のコネクション操作が正しく行われることは、WebSocketサーバーなどのカスタムプロトコル実装において極めて重要です。このテストは、その基本的な動作保証を提供します。
- 目的:
コアとなるコードの変更箇所
変更は src/pkg/net/http/serve_test.go
ファイルに集中しています。
-
reqBytes
関数の追加:// reqBytes treats req as a request (with \n delimiters) and returns it with \r\n delimiters, // ending in \r\n\r\n func reqBytes(req string) []byte { return []byte(strings.Replace(strings.TrimSpace(req), "\n", "\r\n", -1) + "\r\n\r\n") }
-
既存テストでの
reqBytes
の利用:TestCloseNotifierChanLeak
TestHeaderToWire
BenchmarkServerFakeConnNoKeepAlive
BenchmarkServerFakeConnWithKeepAlive
BenchmarkServerFakeConnWithKeepAliveLite
benchmarkHandler
これらのテストで、手動でstrings.Replace
を使っていた部分がreqBytes
の呼び出しに置き換えられました。
-
TestWriteAfterHijack
関数の追加:func TestWriteAfterHijack(t *testing.T) { req := reqBytes("GET / HTTP/1.1\nHost: golang.org") var buf bytes.Buffer wrotec := make(chan bool, 1) conn := &rwTestConn{ Reader: bytes.NewReader(req), Writer: &buf, closec: make(chan bool, 1), } handler := HandlerFunc(func(rw ResponseWriter, r *Request) { conn, bufrw, err := rw.(Hijacker).Hijack() if err != nil { t.Error(err) return } go func() { bufrw.Write([]byte("[hijack-to-bufw]")) bufrw.Flush() conn.Write([]byte("[hijack-to-conn]")) conn.Close() wrotec <- true }() }) ln := &oneConnListener{conn: conn} go Serve(ln, handler) <-conn.closec <-wrotec if g, w := buf.String(), "[hijack-to-bufw][hijack-to-conn]"; g != w { t.Errorf("wrote %q; want %q", g, w) } }
コアとなるコードの解説
reqBytes
関数
この関数は、HTTPリクエストの文字列をバイト列に変換する際の共通処理をカプセル化します。HTTPプロトコルでは行末に \r\n
を使用するため、入力文字列の \n
をすべて \r\n
に置換し、さらにリクエストの終端を示す \r\n\r\n
を追加します。これにより、テストコード内でリクエストバイト列を生成する際の冗長な記述を排除し、よりクリーンで読みやすいコードを実現しています。
TestWriteAfterHijack
関数
このテストは、net/http
パッケージの Hijacker
インターフェースの重要な側面を検証します。
- モックコネクションのセットアップ:
rwTestConn
は、テストのためにネットワークコネクションの振る舞いを模倣する構造体です。Writer
フィールドにbytes.Buffer
を設定することで、ハンドラがコネクションに書き込んだデータをキャプチャし、後で検証できるようにしています。 - ハンドラの定義と
Hijack
の呼び出し:http.HandlerFunc
が定義され、その中でrw.(Hijacker).Hijack()
が呼び出されます。これは、HTTPサーバーがこのコネクションの制御をハンドラに引き渡すことをシミュレートします。 - ゴルーチン内での書き込み:
Hijack()
が成功すると、生のnet.Conn
と、HTTPサーバーが内部的に使用していたbufio.ReadWriter
が返されます。テストでは、新しいゴルーチンを起動し、まずbufrw
にデータを書き込み、Flush()
します。これにより、bufrw
にバッファリングされていたデータが基盤のconn
に書き込まれます。次に、直接conn
にデータを書き込みます。 - 結果の検証: テストは、
conn
が閉じられるのを待ち、その後、bytes.Buffer
にキャプチャされた最終的な出力が、bufrw
からの書き込みとconn
からの書き込みが結合されたものであることをアサートします。この順序と内容の検証は、Hijack
後にアプリケーションがコネクションを完全に制御できることを保証するために不可欠です。
このテストは、Hijack
機能が正しく動作し、HTTPサーバーからコネクションの制御を奪った後も、バッファリングされたデータと直接のデータが期待通りに処理されることを確認するものです。これは、WebSocketやその他のカスタムプロトコルをGoの net/http
を使って実装する際の基盤となる信頼性を高めます。
関連リンク
- Go Documentation:
net/http
package: https://pkg.go.dev/net/http - Go Documentation:
http.Hijacker
interface: https://pkg.go.dev/net/http#Hijacker - Go Documentation:
bufio
package: https://pkg.go.dev/bufio - Go Documentation:
strings
package: https://pkg.go.dev/strings - Go Documentation:
bytes
package: https://pkg.go.dev/bytes
参考にした情報源リンク
- Go言語の公式ドキュメント
- HTTP/1.1 RFC (RFC 2616, 特にセクション 2.2 Basic Rules)
- Go言語のテストに関する一般的な知識
- Go言語の並行処理(ゴルーチンとチャネル)に関する知識