Golang 技巧:websocket client ping pong 斷線檢測

golang 控制 websocket 算是非常好用,但在所有文件內都沒有寫如何控制 ping / pong protocol … 除非你開 server,但在 client 處完全沒有寫這件事情,只有一個 SetWriteDeadline,包括翻遍官方的 source code 都是(這邊使用 gorilla/websocket

經實際測試 golang 的 websocket client 斷線後甚至需要三分鐘以上才會檢測到斷線,大概就是 websocket 底層的 ping / pong msg 沒實作或是根本不想實作在 client 上 … 最後玩了好久才發覺 … client 的 ping / pong 要自己實作才行

(其實 ping pong protocol 的 timer 應該是在 server 處規定的才行,好死不死通常都會設得很長 … 然後 ws client 就會卡死在那邊等 timer 生效|||)

anyway 以下為 demo code,包括我自己寫的斷線後重新連線之類的鬼

const(
	wsPingTimeout = 10 * time.Second
	wsHandshakeTimeout = 10 * time.Second
	wsURL = "wss://example"
)
func goWs() {
	// 將連線內所有的動作都收斂在這邊 ... 外部好做 rerun(裡面可以方便的用 retrun err)
	err := func() error {
		// 撥接
		wsDialer := websocket.DefaultDialer
		wsDialer.HandshakeTimeout = wsHandshakeTimeout // 增加 timeout 設定
		conn, _, err := wsDialer.Dial(
			wsURL,
			nil,
		)
		if err != nil {
			return fmt.Errorf("ws init conn fail : %s", err.Error())
		}
		defer conn.Close()

		// 這邊要 write init msg 到 server 去,如果該 server 需要類似訂閱或輸入些啥鬼才有後續封包時

		// 都連線完成後,才開始增加 keep alive 來做快速 ping pong 檢測斷線檢測
		lastPingResponse := time.Now()
		conn.SetPongHandler(func(msg string) error {
			lastPingResponse = time.Now()
			return nil
		})
		go func() { // 有實測這邊不會累積 ... 當下會乖乖的 break 掉
			for {
				err := conn.WriteMessage(websocket.PingMessage, []byte("keepalive"))
				if err != nil {
					return
				}
				time.Sleep(wsPingTimeout / 2)
				if time.Now().Sub(lastPingResponse) > wsPingTimeout {
					conn.Close()
					return
				}
			}
		}()
		// 這段是宣告自幹定時發出 ping pong msg 來做斷線檢測

		// 後面就只有持續接收訊息而已
		for {
			messageType, messageByte, err := conn.ReadMessage()
			if err != nil {
				return fmt.Errorf("fail to read msg %s", err.Error())
			}
			switch messageType {
			case websocket.TextMessage: // no need uncompressed
				// do nothing
			case websocket.BinaryMessage: // uncompressed(此部分要看對象 server),是否有做封包加密或壓縮
				reader := flate.NewReader(bytes.NewReader(messageByte))
				defer reader.Close()
				messageByte, err = ioutil.ReadAll(reader)
				if err != nil {
					return fmt.Errorf("read ws msg fail : %s", err.Error())
				}
			}
			err = updateData(messageByte) // 自己實作的收到 msg 後處理的 func
			if err != nil {
				return fmt.Errorf("fail to update Data %s", err.Error())
			}
		}
		// QwQ 被結束惹(斷線還是啥鬼的),不過這邊不需要,因為 for(loop) 應該一定會 return
		// return fmt.Errorf("ws return no err , maybe ws.done")
	}()
	// 出事了,印出錯誤,然後睡兩秒後重試後 reconnect
	fmt.Printf("%s : ws failed , sleep 2 sec and retry : %s", err.Error())
	time.Sleep(2 * time.Second)
	goRun() // rerun 自己(重新連線)
}

go goWs() // 記得開個 goroutine 來跑唄
select{} // 讓主程式卡住 ... 當然後面你應該會有其他實作?

當然 goWs() 還能輸入 obj 來做各階段的 status 變更就是,and 這邊要記得 ping pong msg 的時間不能太近,否則流量過大可能會被踢唄?而 websocket 的 ping / pong msg 基本上不會算在一般的封包內,所以 messageByte 那邊不會接到之類的,大概就這些而已,以上