Request.Context()を使うことでクライアントが切断したことをアプリケーションから検知することができる。一般的なhttpでの処理は以下のようになっている。そのうち5番まで処理が進んだ状態でそこから処理キャンセルをすることができるというのでどうやってるんだ?と思って調べた。
アプリケーションがHTTPリクエストの処理中にクライアントが切断したらどうなるのか
HTTPリクエストを受け取った後にアプリケーションの処理をしているだけではアプリケーションはクライアントの切断に何もしなければ気づくことはできない。なぜならTCPの処理はカーネルが行なっていて切断をアプリケーションに通知は行なっていないためである。アプリケーションからコネクションが切断されていることを知るためにはアプリケーションがsocketをread(2)してEOFが返ってくるのかを確認する必要がある。
Goだとどうなのか
とってもシンプルなwebアプリケーションですがr.Contex()でcontextを生成するとクライアントの切断を検知することが可能になります。
func helloWorld(w http.ResponseWriter, r *http.Request) { ctx := r.Context() for { select { case <-ctx.Done(): // クライアントの切断時はこちらが実行される fmt.Println("error: ", ctx.Err()) return default: fmt.Fprintf(w, "hello world3\n") } } }
ソースを追っていく
http.ListenAndServe(":8080", nil)
のように呼び出すとserver.ListenAndServe()
を内部的に呼び出す。net.Listen()を使ってlisten(2)を開始してsrv.Serve()
を呼び出す
func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) }
上記のメソッドでaccept(2)までを行いコネクションが貼られるたびにgoroutineが起動し以下のメソッドが呼び出される。
// Serve a new connection. func (c *conn) serve(ctx context.Context) { for { w, err := c.readRequest(ctx) if c.r.remain != c.server.initialReadLimitSize() { // If we read any bytes off the wire, we're active. c.setState(c.rwc, StateActive, runHooks) } if requestBodyRemains(req.Body) { registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead) } else { w.conn.r.startBackgroundRead() } }
バックグラウンドで実行されるのが以下のメソッド。
func (cr *connReader) startBackgroundRead() { cr.lock() defer cr.unlock() if cr.inRead { panic("invalid concurrent Body.Read call") } if cr.hasByte { return } cr.inRead = true cr.conn.rwc.SetReadDeadline(time.Time{}) go cr.backgroundRead() // バックグラウンドでソケットのReadを実行する }
func (cr *connReader) backgroundRead() { n, err := cr.conn.rwc.Read(cr.byteBuf[:]) cr.lock() if n == 1 { cr.hasByte = true } if ne, ok := err.(net.Error); ok && cr.aborted && ne.Timeout() { } else if err != nil { cr.handleReadError(err) // errが入るのでここに到達する } cr.aborted = false cr.inRead = false cr.unlock() cr.cond.Broadcast() }
handleReadErrorでコンテキストのキャンセルを実施する。これでクライアントの切断を検知して処理の中断を実装することができるようになっている。
func (cr *connReader) handleReadError(_ error) { cr.conn.cancelCtx() // コンテキストがキャンセルされる cr.closeNotify() }
まとめ
HTTPハンドラの呼び出しとは別のgoroutineがソケットを監視することで実現されていました。他言語だとどうなっているのかも気になりました。nginxだとproxy_ignore_client_abort
みたいなパラメータがあってクライアントの切断を同じような処理で監視しています。ただしupstreamのアプリケーションには通知を行うのみで通知を用いて何かをするような処理を実装しない限りは処理を継続して行なってしまうので注意が必要です。