GOLANG探測HTTP連線斷開

winlin發表於2017-11-22

考慮基於HTTP的RPC,或者HTTP伺服器主動通知客戶端的機制,就是HTTP Long-Polling,意思就是客戶端發起一個長連線,伺服器阻塞忍住不響應直到:

  1. 超時,比如5秒後,我們給客戶端響應一個keepalive,意思是現在還沒有啥事,請繼續polling。
  2. 拿到結果,這個可能是任何時候,比如300毫秒、1100毫秒、2300毫秒拿到一個事件,響應給客戶端,實現了有事件非同步通知。

這樣客戶端和伺服器之間RPC的效率就非常高,只有在有事件時才會通知。但是,實際上還有一種情況需要處理:

  1. 當客戶端斷開連線,比如客戶端設定了3秒鐘TCP請求超時,或者因為客戶端Crash時OS回收了FD等等,這個時候伺服器應該要終止polling事務,停止獲取事件。因為如果這個時候獲取了事件,那麼如何處理這個事件?只能丟棄,如果客戶端再次發起請求,就拿不到這個事件了。

問題就來了,如何在HTTP Handler中探測客戶端斷開?例如:

var incoming chan []byte
http.HandleFunc("/polling", func(w http.ResponseWriter, r *http.Request) {
    select {
    case b := <- incoming:
        w.Write(b)
    case <-time.After(5 * time.Second):
        w.Write("keepalive")
    // how to detect TCP disconnect event?
    }
})

可能有以下方式:

  1. 讀取r.Body,如果發現斷開應該會有錯誤。
  2. 有朋友用reflect或hijack取到底層的TCPConn,然後Peek。
  3. w轉換成http.CloseNotifier,在TCP連線關閉時拿到事件。

r.Body Read

這種方式是不靠譜的,假設沒有Body內容,直接讀取檢測是否有error:

nn,err := io.Copy(ioutil.Discard, r.Body)

實際上返回的是nn=0err=nil,也就是沒有Body,沒有錯誤。因為這個讀取的含義是指Request結束。

如果讀取完Body後再讀呢?收到的是io.EOF,在沒有傳送Response之前,Request已經結束了,所以就是io.EOF,並不能檢測到底層TCP斷開。

Peek TcpConn

使用reflect獲取底層的TCPConn物件,是知道w http.ResponseWriter實際上是http.response

// A response represents the server side of an HTTP response.
type response struct {
    conn             *conn

它有個Field就是conn,再轉成TCPConn就可以Peek。

這樣做的風險就是,不同的GOLANG版本,可能會對底層實現進行變更,在升級時會有風險。

Reflect方式始終不是最好的。

另外,還有一種方式,就是用http hijack方式,這種方式雖然是http庫提供的介面,但是很多地方註釋都說hijack需要特殊處理,因此也不是最好的方式。參考When to use hijack

Close Notifier

在GO1.1提供了http.CloseNotifier介面,參考Close Notifier,但是也注意會有一些問題,參考net/http: CloseNotifier fails to fire when underlying connection is gone。用法如下:

var incoming chan []byte
http.HandleFunc("/polling", func(w http.ResponseWriter, r *http.Request) {
    select {
    case <- w.(http.CloseNotifier).CloseNotify():
        fmt.Println("connection closed")
    }
})

實際上,超時機制始終是需要的,加上之前的邏輯,考慮context.Context取消事件,http-long polling的完整實現應該是:

func polling(ctx context.Context, incoming chan []byte) {
    http.HandleFunc("/polling", func(w http.ResponseWriter, r *http.Request) {
        select {
        case <- ctx.Done():
            fmt.Println("system quit")
        case b := <- incoming:
            w.Write(b)
        case <-time.After(5 * time.Second):
            w.Write("keepalive")
        case <- w.(http.CloseNotifier).CloseNotify():
            fmt.Println("connection closed")
        }
    })
}

相關文章