なぜGo言語のHTTPサーバーでクライアント切断が検知できるのか調べた

1 min read読了の目安(約1600字

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()

となっています。このcancelCtxcontext.WithCancel(ctx)で得られるcancel関数が入っています。

まとめ

  • HTTPハンドラの呼び出しとは別のgoroutineがコネクションをReadしている
  • 切断するとそのReadでエラーとなるのでContextはキャンセルされる

なおContextはキャンセルされますが、HTTPハンドラはインタラプトされないので、普段は特に気にせずHTTPハンドラを実装してもおおむね大丈夫です。

使いどころとしては、例えばサーバー側で重い処理を行っている場合で、クライアント切断したら中断したいときなどは、Context.Doneを見ておく、といったときでしょうか。


関連: Songmuさんの記事