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

[インデックス 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.ReadWriternet.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.Hijackerhttp.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点に集約されます。

  1. 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 など)で重複していたリクエストバイト列生成ロジックを共通化し、コードの簡潔性と保守性を向上させました。
  2. TestWriteAfterHijack テストの追加:

    • 目的: http.Hijacker インターフェースの Hijack() メソッドを呼び出した後、返された bufio.ReadWriternet.Conn の両方への書き込みが正しく行われることを検証します。
    • テストシナリオ:
      1. モックの rwTestConn を作成し、その Writerbytes.Buffer に設定して、書き込まれたデータをキャプチャできるようにします。
      2. http.HandlerFunc を定義し、その中で rw.(Hijacker).Hijack() を呼び出してコネクションを乗っ取ります。
      3. 新しいゴルーチンを起動し、乗っ取ったコネクションの bufio.ReadWriter (bufrw) に [hijack-to-bufw] を書き込み、Flush() します。
      4. 次に、生の net.Conn (conn) に [hijack-to-conn] を書き込み、Close() します。
      5. メインのテストゴルーチンは、コネクションが閉じられるのを待ち、その後、bytes.Buffer に書き込まれた内容が [hijack-to-bufw][hijack-to-conn] となっていることをアサートします。
    • 検証ポイント:
      • Hijack() 後も bufrw を通じた書き込みが機能し、その内容が基盤のコネクションにフラッシュされること。
      • bufrw のフラッシュ後、conn を通じた直接の書き込みも機能すること。
      • これら2つの書き込みが期待通りの順序で結合されて出力されること。
    • 重要性: Hijack() はHTTPサーバーの制御をアプリケーションに完全に委譲するため、この後のコネクション操作が正しく行われることは、WebSocketサーバーなどのカスタムプロトコル実装において極めて重要です。このテストは、その基本的な動作保証を提供します。

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

変更は src/pkg/net/http/serve_test.go ファイルに集中しています。

  1. 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")
    }
    
  2. 既存テストでの reqBytes の利用:

    • TestCloseNotifierChanLeak
    • TestHeaderToWire
    • BenchmarkServerFakeConnNoKeepAlive
    • BenchmarkServerFakeConnWithKeepAlive
    • BenchmarkServerFakeConnWithKeepAliveLite
    • benchmarkHandler これらのテストで、手動で strings.Replace を使っていた部分が reqBytes の呼び出しに置き換えられました。
  3. 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 インターフェースの重要な側面を検証します。

  1. モックコネクションのセットアップ: rwTestConn は、テストのためにネットワークコネクションの振る舞いを模倣する構造体です。Writer フィールドに bytes.Buffer を設定することで、ハンドラがコネクションに書き込んだデータをキャプチャし、後で検証できるようにしています。
  2. ハンドラの定義と Hijack の呼び出し: http.HandlerFunc が定義され、その中で rw.(Hijacker).Hijack() が呼び出されます。これは、HTTPサーバーがこのコネクションの制御をハンドラに引き渡すことをシミュレートします。
  3. ゴルーチン内での書き込み: Hijack() が成功すると、生の net.Conn と、HTTPサーバーが内部的に使用していた bufio.ReadWriter が返されます。テストでは、新しいゴルーチンを起動し、まず bufrw にデータを書き込み、Flush() します。これにより、bufrw にバッファリングされていたデータが基盤の conn に書き込まれます。次に、直接 conn にデータを書き込みます。
  4. 結果の検証: テストは、conn が閉じられるのを待ち、その後、bytes.Buffer にキャプチャされた最終的な出力が、bufrw からの書き込みと conn からの書き込みが結合されたものであることをアサートします。この順序と内容の検証は、Hijack 後にアプリケーションがコネクションを完全に制御できることを保証するために不可欠です。

このテストは、Hijack 機能が正しく動作し、HTTPサーバーからコネクションの制御を奪った後も、バッファリングされたデータと直接のデータが期待通りに処理されることを確認するものです。これは、WebSocketやその他のカスタムプロトコルをGoの net/http を使って実装する際の基盤となる信頼性を高めます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • HTTP/1.1 RFC (RFC 2616, 特にセクション 2.2 Basic Rules)
  • Go言語のテストに関する一般的な知識
  • Go言語の並行処理(ゴルーチンとチャネル)に関する知識