[インデックス 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