[インデックス 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ハンドラ内で ResponseWriter を CloseNotifier 型に型アサーションし、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) は rw が CloseNotifier インターフェースを実装しているかどうかをチェックし、実装していれば CloseNotifier 型として返します。
チャネル (Channels)
Go言語のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、並行処理における同期と通信を安全に行うために使用されます。このコミットでは、クライアントの切断イベントを通知するためにチャネルが使用されています。チャネルに値が送信されると、そのチャネルを待機しているゴルーチンはブロック解除されます。
io.Pipe
io.Pipe は、メモリ内で接続された io.Reader と io.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.Request に Context() メソッドが追加されて以降、CloseNotifier は非推奨となりました。現代のGoアプリケーションでは、クライアント接続の切断を検知するには、http.Request.Context() から取得できる context.Context の Done() チャネルを使用するのが推奨されています。Context は、リクエストのキャンセル、タイムアウト、および値の伝播のためのより汎用的なメカニズムを提供します。
技術的詳細
このコミットの主要な技術的変更点は以下の通りです。
-
CloseNotifierインターフェースの定義:CloseNotifierインターフェースがnet/httpパッケージに追加されました。type CloseNotifier interface { CloseNotify() <-chan bool }このインターフェースは、クライアント接続が切断されたときに単一の値を送信するチャネルを返す
CloseNotify()メソッドを定義します。 -
conn構造体の変更:httpサーバーの内部接続を表すconn構造体に、クライアント切断の状態を管理するためのフィールドが追加されました。mu sync.Mutex:clientGoneとcloseNotifycを保護するためのミューテックス。clientGone bool: クライアントが切断されたかどうかを示すフラグ。closeNotifyc chan bool:CloseNotify()メソッドが返すチャネル。必要に応じて遅延初期化されます。hijackedv bool: 接続がハイジャックされたかどうかを示すフラグ。
-
conn.closeNotify()メソッドの実装: このメソッドはCloseNotifierインターフェースの実装の中核です。- 初めて呼び出されたときに
closeNotifycチャネルを初期化します。 Hijackされた接続の場合、closeNotifycは作成されますが、値は送信されません(Hijackされた接続はnet/httpが管理しないため)。- 重要なのは、
io.Pipeを使用して元の接続の読み取りストリームを置き換える点です。c.sr.r(元のrwcまたはそのラッパー) からio.PipeWriterへデータをコピーするゴルーチンが起動されます。io.Copyがエラー(通常はio.EOFまたはネットワークエラー)で終了すると、PipeWriterが閉じられ、c.noteClientGone()が呼び出されます。
- 初めて呼び出されたときに
-
conn.noteClientGone()メソッド: このメソッドは、クライアントが切断されたことを内部的に記録し、closeNotifycチャネルが存在し、まだ通知されていない場合にtrueを送信します。 -
switchReader構造体:conn構造体のsrフィールドとして導入されたswitchReaderは、io.Readerを動的に切り替えることができるラッパーです。これにより、CloseNotifierが有効になったときに、元のnet.Connからの読み取りをio.PipeReaderにリダイレクトすることが可能になります。 -
Hijack処理の変更:conn.hijack()メソッドが導入され、Hijackerインターフェースの実装がresponse構造体からconn構造体に移されました。Hijackが呼び出された際にcloseNotifycが既に初期化されている場合、エラーを返すようになりました。これは、CloseNotifierとHijackが同じ接続の異なる管理モデルを表すため、両方を同時に使用することができないという制約を課しています。
-
response構造体からのHijackとCloseNotifyの委譲:http.ResponseWriterの具象実装であるresponse構造体は、HijackerとCloseNotifierインターフェースを実装し、その呼び出しを内部のconn構造体に委譲するように変更されました。 -
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.srとconn.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 の機能を提供する中心的なロジックを含んでいます。
closeNotifycチャネルがまだ作成されていなければ、新しく作成します。io.Pipe()を使用して、メモリ上のパイプを作成します。prはPipeReader、pwはPipeWriterです。c.sr.r = prの行で、connのswitchReaderの内部リーダーを、元のネットワーク接続 (c.rwc) から新しく作成したPipeReader(pr) に切り替えます。これにより、以降のHTTPリクエストの読み取りは、このパイプを介して行われるようになります。- 新しいゴルーチンが起動され、
io.Copy(pw, readSource)を実行します。これは、元のネットワーク接続 (readSource) から読み取ったデータを、パイプの書き込み側 (pw) にコピーし続けます。 - この
io.Copyがエラー(例えば、クライアントが接続を閉じたことによるio.EOFやネットワークエラー)で終了すると、pw.CloseWithError(err)が呼び出され、パイプの書き込み側が閉じられます。 - 最後に
c.noteClientGone()が呼び出され、closeNotifycチャネルにtrueが送信され、ハンドラにクライアント切断が通知されます。
この巧妙なメカニズムにより、net/http サーバーは、通常のHTTPリクエストの読み取りフローを妨げることなく、基盤となるTCP接続の切断イベントを検知し、それを CloseNotifier インターフェースを通じてハンドラに伝達することができます。
関連リンク
- Go言語の
net/httpパッケージドキュメント: https://pkg.go.dev/net/http - Go言語の
contextパッケージドキュメント (現代のGoでの推奨されるキャンセルメカニズム): https://pkg.go.dev/context
参考にした情報源リンク
- Go issue 2510 (このコミットが修正した内部イシュー): https://golang.org/issue/2510 (注: このリンクはGoの内部イシュートラッカーへのものであり、一般にはアクセスできない可能性があります。コミットメッセージに記載されている情報に基づいています。)
- Go
net/http.CloseNotifierの deprecation に関する情報 (Stack Overflow): https://stackoverflow.com/questions/37000048/how-to-detect-client-disconnect-in-go-http-server - Go
net/http.CloseNotifierの deprecation に関する情報 (Go.dev): https://go.dev/doc/go1.8#net/http (Go 1.8 リリースノートでCloseNotifierが非推奨になったことが記載されています) - Go
contextパッケージの導入に関する情報: https://blog.golang.org/context