なぜGo言語のHTTPサーバーでクライアント切断が検知できるのか調べた
Go言語のHTTPサーバーで、クライアント側が切断するとContext
がキャンセルされるので、HTTP層においてもそれを検知できます。
以前、某社内のSlackで似たような話題が出たとき、自分は最初、TCP層での切断は、より上位のHTTP層では検知されない(レスポンスの送信を試みるまでは)と思っていました。
なぜこの検知ができるのか。それについては、Go言語のnet/httpの実装を見ていきましょう。
注意: 本記事はコードを読んで理解した内容を記述していますが、実際に動作させて経路を追ったわけではありません。あくまで読んだだけなので、理解の誤り等があればコメントで教えてください。
まずnet/httpのServerの入り口あたりから。
func (srv *Server) Serve(l net.Listener) error
は内部でforループで接続を待ち続けています。
for {
rw, err := l.Accept()
...
Acceptするとconnオブジェクトを生成してgoroutineで処理を開始します。
c := srv.newConn(rw)
c.setState(c.rwc, StateNew)
go c.serve(connCtx)
serveの定義は以下の通り。
func (c *conn) serve(ctx context.Context)
この関数内でServeHTTPが呼ばれ、ここからいわゆるアプリ側の処理へと移っていくわけです。
serverHandler{c.server}.ServeHTTP(w, w.req)
さて、この呼び出しの前に、以下のような処理があります。
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
このstartBackgroundRead
はさらにgo cr.backgroundRead()
をしており、そのなかではコネクションからRead
しようとしています。
n, err := cr.conn.rwc.Read(cr.byteBuf[:])
HTTPリクエストは受信しきったあとなので、このRead
はブロックします。
クライアントが切断すると、このRead
からerr
が返ってくるのでcr.handleReadError(err)
が呼ばれ、その内部では
cr.conn.cancelCtx()
となっています。このcancelCtx
はcontext.WithCancel(ctx)
で得られるcancel関数が入っています。
まとめ
- HTTPハンドラの呼び出しとは別のgoroutineがコネクションを
Read
している - 切断するとその
Read
でエラーとなるのでContext
はキャンセルされる
なおContext
はキャンセルされますが、HTTPハンドラはインタラプトされないので、普段は特に気にせずHTTPハンドラを実装してもおおむね大丈夫です。
使いどころとしては、例えばサーバー側で重い処理を行っている場合で、クライアント切断したら中断したいときなどは、Context.Done
を見ておく、といったときでしょうか。
関連: Songmuさんの記事
Discussion