在 Day18 提到了 Client 端在進入頁面時,會開一個 websocket,然後在接下來都用它來和後端溝通。
這並不是件簡單的事情,因為網路連線有可能會有各種問題, Client 要好好處理,例如網路環境的不穩定性可能會導致連線中斷,這時就需要一個健壯的狀態機來管理連線狀態。
WebSocket 狀態機會根據不同的網路狀況,將連線狀態切換到不同的階段,我們可以先做簡單的四個狀態:
所以狀態機大致上可以畫成這樣:
準備 Enum 對應狀態和路徑:
enum WebSocketState {
Initial,
Ping,
TryConnect,
Connected,
}
enum WebSocketAction {
OK,
Error,
Closed,
}
我們可以把 StatefulWebsocket 打包起來,讓使用者決定在各個 Action 時需要做甚麼事情,例如可以在斷開連線時在頁面顯示連線中。
export class StatefulWebSocket {
conn: WebSocket | null
state: WebSocketState
pageName: string
stateID: string
/** Receive pack from connected websocket. */
recv: (pack: any) => void
/** Call when stateID is assigned a new value from server. */
onStateIDChange: () => void = () => { }
/** Call when state change from TryConnect to Connected. */
onConnect: () => void = () => { }
constructor(pageName: string, recv: (pack: any) => void) {
this.state = WebSocketState.Initial
this.pageName = pageName
this.conn = null
this.stateID = ''
this.recv = recv
}
// ...
}
一開始時進入 Init 狀態
init() {
if (this.state !== WebSocketState.Initial) {
throw new Error('init should call on state Initial')
}
this.walk(WebSocketAction.OK)
}
Ping 的時候會不斷的去確認 server 是否可以戳到:
async ping() {
var waitMS = 200
while (1) {
var resp: Response
try {
resp = await fetch(healthCheckURL)
if (resp.status === 200) {
break
}
console.error('health check', resp)
} catch (e) {
console.error(e)
}
await sleep(waitMS)
waitMS *= 1.5
waitMS = Math.min(waitMS, 60000)
}
console.log('ping ok')
this.walk(WebSocketAction.OK)
}
連線
tryConnect() {
this.conn = new WebSocket(getUpdateURI(this.pageName))
var that = this
this.conn.onopen = function () {
that.conn.send(JSON.stringify({ state_id: that.stateID }))
console.log('socket open ok')
that.walk(WebSocketAction.OK)
}
this.conn.onmessage = function (e) {
const data = JSON.parse(e.data)
if (data.state_id) {
that.stateID = data.state_id
that.onStateIDChange()
return
}
that.recv(data)
}
this.conn.onclose = function () {
that.conn = null
that.walk(WebSocketAction.Closed)
}
}
狀態轉移:
walkTo(state: WebSocketState) {
switch (state) {
case WebSocketState.Ping:
this.state = state
this.ping()
break
case WebSocketState.TryConnect:
this.state = state
this.tryConnect()
break
case WebSocketState.Connected:
this.state = state
this.onConnect()
break
default:
console.error('undefined state', state)
throw new Error('undefine state')
}
}
walk(action: WebSocketAction) {
switch (this.state) {
case WebSocketState.Initial:
if (action === WebSocketAction.OK) {
this.walkTo(WebSocketState.Ping)
return
}
break
case WebSocketState.Ping:
if (action == WebSocketAction.OK) {
this.walkTo(WebSocketState.TryConnect)
return
}
break
case WebSocketState.TryConnect:
switch (action) {
case WebSocketAction.OK:
this.walkTo(WebSocketState.Connected)
return
case WebSocketAction.Error:
case WebSocketAction.Closed:
this.walkTo(WebSocketState.Ping)
return
default:
break
}
break
case WebSocketState.Connected:
switch (action) {
case WebSocketAction.Error:
case WebSocketAction.Closed:
this.walkTo(WebSocketState.Ping)
return
default:
break
}
break
}
console.error('undefine action on state', this.state, action)
throw new Error('undefine action on state')
}
最後提供一個從 websocket 送資料的介面
send(pack: any) {
if (this.state !== WebSocketState.Connected || this.conn === null) {
throw new Error('websocket is not prepared')
}
this.conn.send(JSON.stringify(pack))
}
通過建立一個狀態機,我們可以有效地管理 WebSocket 連線,讓 App 前端更穩定。