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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージに CloseNotifier インターフェースを実装するものです。これにより、HTTPハンドラがクライアントとの接続が切断されたことを検知できるようになり、特に長時間実行される処理において、クライアントが途中で切断した場合にサーバー側のリソースを適切に解放したり、処理を中断したりすることが可能になります。

コミット

commit 4fb78c3a16580329c1c465fbc67c12456b8297dd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Dec 5 19:25:43 2012 -0800

    net/http: implement CloseNotifier
    
    Fixes #2510
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6867050

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

https://github.com/golang/go/commit/4fb78c3a16580329c1c465fbc67c12456b8297dd

元コミット内容

このコミットは、net/http パッケージに CloseNotifier インターフェースを追加し、ResponseWriter がこのインターフェースを実装するように変更しています。これにより、HTTPハンドラ内で ResponseWriterCloseNotifier 型に型アサーションし、CloseNotify() メソッドから返されるチャネルを監視することで、クライアント接続の切断を検知できるようになります。また、Hijack メソッドとの互換性に関する考慮も含まれています。

変更の背景

この変更は、Goの内部イシュートラッカーのイシュー #2510 を解決するために行われました。当時の net/http パッケージでは、HTTPハンドラがクライアントからのリクエストを処理している最中に、クライアントが接続を閉じたことを検知する標準的なメカニズムが存在しませんでした。

例えば、サーバーが非常に時間のかかる処理(データベースクエリ、外部API呼び出し、大きなファイルの生成など)を実行している間に、クライアントがブラウザを閉じたり、ネットワーク接続が切れたりした場合、サーバーはクライアントがすでに存在しないにもかかわらず、無駄に処理を続行してしまう可能性がありました。これはサーバーのリソースを浪費し、不要な処理を続けることになります。

CloseNotifier の導入は、このようなシナリオにおいて、サーバーがクライアントの切断を早期に検知し、不要な処理を中断したり、関連するリソースを解放したりするための手段を提供することを目的としています。これにより、サーバーの効率性と応答性が向上します。

前提知識の解説

Go言語の net/http パッケージ

net/http パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。

  • http.Handler インターフェース: HTTPリクエストを処理するためのインターフェースで、ServeHTTP(ResponseWriter, *Request) メソッドを定義します。
  • http.ResponseWriter インターフェース: HTTPレスポンスをクライアントに書き込むためのインターフェースです。ヘッダーの設定、ステータスコードの送信、ボディの書き込みなどを行います。
  • http.Request 構造体: クライアントからのHTTPリクエストを表す構造体です。リクエストメソッド、URL、ヘッダー、ボディなどの情報を含みます。

インターフェースと型アサーション

Go言語のインターフェースは、メソッドのシグネチャの集合を定義します。ある型がインターフェースのすべてのメソッドを実装していれば、その型はそのインターフェースを満たしているとみなされます。

型アサーションは、インターフェース型の変数が、特定の具象型または別のインターフェース型であるかどうかをチェックし、その型に変換するメカニズムです。例えば、rw.(CloseNotifier)rwCloseNotifier インターフェースを実装しているかどうかをチェックし、実装していれば CloseNotifier 型として返します。

チャネル (Channels)

Go言語のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、並行処理における同期と通信を安全に行うために使用されます。このコミットでは、クライアントの切断イベントを通知するためにチャネルが使用されています。チャネルに値が送信されると、そのチャネルを待機しているゴルーチンはブロック解除されます。

io.Pipe

io.Pipe は、メモリ内で接続された io.Readerio.Writer のペアを作成します。PipeWriter に書き込まれたデータは、対応する PipeReader から読み取ることができます。これは、ストリーム処理や、異なるI/O操作を連結する際に便利です。このコミットでは、元の接続からの読み取りを Pipe を介してリダイレクトし、読み取りエラー(接続切断など)を検知するために使用されています。

Hijacker インターフェース

net/http パッケージの Hijacker インターフェースは、HTTPサーバーが基盤となるTCP接続をHTTPハンドラに引き渡すことを可能にします。これにより、ハンドラはHTTPプロトコルから離れて、WebSocketのような異なるプロトコルを実装したり、カスタムの通信プロトコルを扱ったりすることができます。Hijack が行われると、net/http サーバーはそれ以上その接続を管理しなくなります。

context.Context (補足:現代のGoにおける接続切断検知)

このコミットが作成された2012年時点では context.Context はGoの標準ライブラリには存在しませんでした。しかし、Go 1.7で context パッケージが導入され、Go 1.8で http.RequestContext() メソッドが追加されて以降、CloseNotifier は非推奨となりました。現代のGoアプリケーションでは、クライアント接続の切断を検知するには、http.Request.Context() から取得できる context.ContextDone() チャネルを使用するのが推奨されています。Context は、リクエストのキャンセル、タイムアウト、および値の伝播のためのより汎用的なメカニズムを提供します。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. CloseNotifier インターフェースの定義: CloseNotifier インターフェースが net/http パッケージに追加されました。

    type CloseNotifier interface {
        CloseNotify() <-chan bool
    }
    

    このインターフェースは、クライアント接続が切断されたときに単一の値を送信するチャネルを返す CloseNotify() メソッドを定義します。

  2. conn 構造体の変更: http サーバーの内部接続を表す conn 構造体に、クライアント切断の状態を管理するためのフィールドが追加されました。

    • mu sync.Mutex: clientGonecloseNotifyc を保護するためのミューテックス。
    • clientGone bool: クライアントが切断されたかどうかを示すフラグ。
    • closeNotifyc chan bool: CloseNotify() メソッドが返すチャネル。必要に応じて遅延初期化されます。
    • hijackedv bool: 接続がハイジャックされたかどうかを示すフラグ。
  3. conn.closeNotify() メソッドの実装: このメソッドは CloseNotifier インターフェースの実装の中核です。

    • 初めて呼び出されたときに closeNotifyc チャネルを初期化します。
    • Hijack された接続の場合、closeNotifyc は作成されますが、値は送信されません(Hijack された接続は net/http が管理しないため)。
    • 重要なのは、io.Pipe を使用して元の接続の読み取りストリームを置き換える点です。c.sr.r (元の rwc またはそのラッパー) から io.PipeWriter へデータをコピーするゴルーチンが起動されます。io.Copy がエラー(通常は io.EOF またはネットワークエラー)で終了すると、PipeWriter が閉じられ、c.noteClientGone() が呼び出されます。
  4. conn.noteClientGone() メソッド: このメソッドは、クライアントが切断されたことを内部的に記録し、closeNotifyc チャネルが存在し、まだ通知されていない場合に true を送信します。

  5. switchReader 構造体: conn 構造体の sr フィールドとして導入された switchReader は、io.Reader を動的に切り替えることができるラッパーです。これにより、CloseNotifier が有効になったときに、元の net.Conn からの読み取りを io.PipeReader にリダイレクトすることが可能になります。

  6. Hijack 処理の変更: conn.hijack() メソッドが導入され、Hijacker インターフェースの実装が response 構造体から conn 構造体に移されました。

    • Hijack が呼び出された際に closeNotifyc が既に初期化されている場合、エラーを返すようになりました。これは、CloseNotifierHijack が同じ接続の異なる管理モデルを表すため、両方を同時に使用することができないという制約を課しています。
  7. response 構造体からの HijackCloseNotify の委譲: http.ResponseWriter の具象実装である response 構造体は、HijackerCloseNotifier インターフェースを実装し、その呼び出しを内部の conn 構造体に委譲するように変更されました。

  8. Expect ヘッダー処理の改善: Expect ヘッダーの処理ロジックが sendExpectationFailed() という新しいヘルパー関数に抽出され、コードの重複が解消されました。

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

src/pkg/net/http/server.go

  • CloseNotifier インターフェースの定義を追加。
  • conn 構造体に mu, clientGone, closeNotifyc, hijackedv, sr フィールドを追加。
  • conn 構造体に hijacked(), hijack(), closeNotify(), noteClientGone() メソッドを追加。
  • switchReader 構造体とその Read メソッドを追加。
  • srv.newConn 内で conn.srconn.lr の初期化を変更し、switchReader を使用するように修正。
  • response 構造体の Hijack() メソッドを conn.hijack() に委譲するように変更。
  • response 構造体に CloseNotify() メソッドを追加し、conn.closeNotify() に委譲するように変更。
  • conn.serve() 内の Expect ヘッダー処理を w.sendExpectationFailed() を呼び出すように変更。
  • sendExpectationFailed() ヘルパー関数を追加。
  • c.hijacked の直接参照を c.hijacked() メソッド呼び出しに置き換え。

src/pkg/net/http/serve_test.go

  • TestCloseNotifier という新しいテストケースを追加。このテストは、httptest.NewServer を使用してサーバーを起動し、クライアントが接続を閉じたときに CloseNotify が正しく機能するかどうかを検証します。具体的には、ハンドラ内で CloseNotify() から返されるチャネルを待ち、クライアントが接続を閉じたときにチャネルが通知を受け取ることを確認しています。

コアとなるコードの解説

CloseNotifier インターフェース

type CloseNotifier interface {
	// CloseNotify returns a channel that receives a single value
	// when the client connection has gone away.
	CloseNotify() <-chan bool
}

このインターフェースは、http.ResponseWriter が実装することで、クライアント接続の切断をハンドラに通知する機能を提供します。CloseNotify() メソッドは読み取り専用の bool 型チャネルを返します。このチャネルに値が送信された場合(通常は true)、それはクライアントが接続を閉じたことを意味します。

conn.closeNotify() の実装の肝

func (c *conn) closeNotify() <-chan bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closeNotifyc == nil {
		c.closeNotifyc = make(chan bool)
		// ... (hijackedv のチェック) ...
		pr, pw := io.Pipe() // パイプを作成
		readSource := c.sr.r // 元の読み取り元 (rwc)
		c.sr.Lock()
		c.sr.r = pr // switchReader の読み取り元をパイプの読み取り側に切り替える
		c.sr.Unlock()
		go func() {
			_, err := io.Copy(pw, readSource) // 元の読み取り元からパイプの書き込み側へコピー
			if err == nil {
				err = io.EOF
			}
			pw.CloseWithError(err) // コピーが終了したらパイプの書き込み側を閉じる
			c.noteClientGone() // クライアント切断を通知
		}()
	}
	return c.closeNotifyc
}

このメソッドは、CloseNotifier の機能を提供する中心的なロジックを含んでいます。

  1. closeNotifyc チャネルがまだ作成されていなければ、新しく作成します。
  2. io.Pipe() を使用して、メモリ上のパイプを作成します。prPipeReaderpwPipeWriter です。
  3. c.sr.r = pr の行で、connswitchReader の内部リーダーを、元のネットワーク接続 (c.rwc) から新しく作成した PipeReader (pr) に切り替えます。これにより、以降のHTTPリクエストの読み取りは、このパイプを介して行われるようになります。
  4. 新しいゴルーチンが起動され、io.Copy(pw, readSource) を実行します。これは、元のネットワーク接続 (readSource) から読み取ったデータを、パイプの書き込み側 (pw) にコピーし続けます。
  5. この io.Copy がエラー(例えば、クライアントが接続を閉じたことによる io.EOF やネットワークエラー)で終了すると、pw.CloseWithError(err) が呼び出され、パイプの書き込み側が閉じられます。
  6. 最後に c.noteClientGone() が呼び出され、closeNotifyc チャネルに true が送信され、ハンドラにクライアント切断が通知されます。

この巧妙なメカニズムにより、net/http サーバーは、通常のHTTPリクエストの読み取りフローを妨げることなく、基盤となるTCP接続の切断イベントを検知し、それを CloseNotifier インターフェースを通じてハンドラに伝達することができます。

関連リンク

参考にした情報源リンク