做網(wǎng)站每頁面費用/bing搜索引擎國際版
有限狀態(tài)機(jī)
是一種數(shù)學(xué)計算模型,它描述了在任何給定時間只能處于一種狀態(tài)的系統(tǒng)的行為。形式上,有限狀態(tài)機(jī)有五個部分:
- 初始狀態(tài)值 (
initial state
) - 有限的一組狀態(tài) (
states
) - 有限的一組事件 (
events
) - 由事件驅(qū)動的一組狀態(tài)轉(zhuǎn)移關(guān)系 (
transitions
) - 有限的一組最終狀態(tài) (
final states
)
狀態(tài)是指由狀態(tài)機(jī)建模的系統(tǒng)中某種有限的
、定性的
“模式”或“狀態(tài)”,并不描述與該系統(tǒng)相關(guān)的所有(可能是無限的)數(shù)據(jù)。例如,水可以處于以下 4 種狀態(tài)中的一種:冰、液體、氣體或等離子體。然而,水的溫度可以變化,所以其測量值是定量的和無限的。再比如管理TCP Socket
連接時,其生命周期內(nèi)存在明顯的有限狀態(tài)轉(zhuǎn)換。
可能有相當(dāng)多的同學(xué)在開發(fā)中沒意識到有限狀態(tài)機(jī)
的作用,但是實際上,我們幾乎無時不刻在有意無意間使用了有限狀態(tài)機(jī)。當(dāng)您在開發(fā)過程中能有意識地系統(tǒng)地進(jìn)行有限狀態(tài)分析并應(yīng)用有限狀態(tài)機(jī),往往代表著您達(dá)到了較高的水平。
目前開源的有限狀態(tài)機(jī)實現(xiàn)中比較知名的有:
xstate
:堪稱狀態(tài)機(jī)航空母艦,功能太強大了,也太復(fù)雜了,學(xué)習(xí)成本非常高。Javascript State Machine
:功能較弱,在實際試用過程中發(fā)現(xiàn)在進(jìn)行異步切換時存在問題。jssm
:特點是引入自己的DSL語法來描述狀態(tài)機(jī),使用起來比較別扭。
事實上,從功能完整度上看xstate
是第一選擇,但是其過于復(fù)雜了,在功能與易用平衡方面并不理想。
因此,我們開發(fā)了FlexState
有限狀態(tài)機(jī),力求在功能性、易用性上達(dá)到平衡。
FlexState
是一款簡單易用的有限狀態(tài)機(jī),具有以下特性:
- 支持基于
Class
構(gòu)建有限狀態(tài)機(jī)實例 - 支持狀態(tài)
enter/leave/resume/done
鉤子事件 - 狀態(tài)切換完全支持異步操作
- 支持定義異步狀態(tài)動作
Action
- 支持狀態(tài)切換生命周期事件訂閱
- 支持錯誤處理和狀態(tài)切換中止
- 基于
TypeScript
開發(fā) - 支持子狀態(tài)
- 核心代碼
90%+
單元測試覆蓋率
Github
官網(wǎng)
快速入門
下面我們以開發(fā)基于nodejs/net.socket
的TCP客戶端為例來說明FlexStateMachine
的使用。
作為例子,我們?yōu)?code>TCPClient設(shè)計以下幾種狀態(tài):
Initial
:初始狀態(tài),構(gòu)建socket實例后處于該階段。Connecting
:連接中,當(dāng)調(diào)用Connect方法,觸發(fā)connect事件前。Connected
:已連接,當(dāng)觸發(fā)connect事件后。Disconnecting
:正在斷開,當(dāng)調(diào)用destory或end方法后,end/close事件觸發(fā)前。Disconnected
:被動斷開,當(dāng)觸發(fā)end/close事件后。AlwaysDisconnected
: 主動斷開狀態(tài)IDLE
: 自動添加的空閑狀態(tài),狀態(tài)機(jī)未啟動時ERROR
: 自動添加的錯誤狀態(tài),特殊的FINAL
狀態(tài)
TCPClient
的狀態(tài)圖如下:
第一步:構(gòu)建狀態(tài)機(jī)
推薦直接繼承FlexStateMachine
來創(chuàng)建一個TCPClient
實例,該種方式更加簡單易用。
import { state, FlexStateMachine } from "flexstate"class TcpClient extends FlexStateMachine{// 定義狀態(tài)static states = { Initial : { value:0, title:"已初始化", next:["Connecting","Connected","Disconnected"],initial:true},Connecting : { value:1, title:"正在連接...", next:["Connected","Disconnected"] },Connected : { value:2, title:"已連接", next:["Disconnecting","Disconnected"] },Disconnecting : { value:3, title:"正在斷開連接...", next:["Disconnected"] },Disconnected : {value:4, title:"已斷開連接", next:["Connecting"]},AlwaysDisconnected : {value:5, title:"已主動斷開連接", next:["Connecting"]}} constructor(options:FlexStateOptions){super(Object.assign({host:"",port:9000,autoStart:true,context : null, // 狀態(tài)上下文對象,當(dāng)執(zhí)行動作或狀態(tài)轉(zhuǎn)換事件時的this指向autoStart : true, // 自動啟動狀態(tài)機(jī)timeout : 30 * 1000 // 當(dāng)執(zhí)行狀態(tài)切換回調(diào)時的超時,如enter、leave、done回調(diào)injectActionMethod: true, // 將動作方法注入到當(dāng)前實例中 },options)) } @state{when:["Initial","Disconnected","Error"], // 代表只能當(dāng)處于此三種狀態(tài)時才允許調(diào)用連接方法 pending:"Connecting", // 執(zhí)行后進(jìn)入正在連接中的狀態(tài)}connect(){this._socket.connect(this.options) } @state({when:["Connected"], // 代表只有在已連接狀態(tài)才允許執(zhí)行斷開方法pending:"Disconnecting"}) disconnect(){this._socket.destory()}// 當(dāng)狀態(tài)轉(zhuǎn)換成功后會調(diào)用此方法ontTransition({error,from,to,done,timeConsuming}){console.log(`從<${previous}>轉(zhuǎn)換到<current>,耗時:${timeConsuming}ms`) // 例 ==> 從<Connecting>轉(zhuǎn)換到<Connected,耗時12ms>console.log(this.current) // {name,value,....}}onData(data){....}}
說明:
- 以上我們創(chuàng)建了一個繼承自
FlexStateMachine
來創(chuàng)建一個TCPClient
實例 - 并且定義了
Initial
、Connecting
、Connected
、Disconnecting
、Disconnected
、AlwaysDisconnected
共六個狀態(tài)以及狀態(tài)之間的轉(zhuǎn)換約束。同時,狀態(tài)機(jī)還會自動添加一個ERROR
和IDLE
狀態(tài)。 - 定義了
connect
和disconnect
兩個動作action
,在這兩個方法前添加@state
代表了當(dāng)執(zhí)行這兩個方法會導(dǎo)致狀態(tài)變化。
第二步:初始化TCPSocket
當(dāng)實例化TCPClient
實例后,首先應(yīng)該創(chuàng)建Socket實例。由于TCPClient
實例繼承自FlexStateMachine
,并且我們指定了Initial
為初始化狀態(tài)。
狀態(tài)機(jī)會在實例化并啟動后自動轉(zhuǎn)換到Initial
狀態(tài)。因此,我們可以在進(jìn)入Initial
狀態(tài)前進(jìn)行初始化操作。
class TcpClient extends FlexStateMachine{// 轉(zhuǎn)換至Initial狀態(tài)前會調(diào)用方法async onInitialEnter({retry,retryCount}){try{ this._socket = new net.Socket()// 當(dāng)連接成功時,切換到Connected事件; 每一個狀態(tài)均有一個大寫的狀態(tài)值實例成員// this.CONNECTED==this.states.Connected.valuethis._socket.on("connect",()=>this.transition(this.CONNECTED)) this._socket.on("close",()=>{//.... 詳見后續(xù)重連說明}) // 套接字因不活動而超時則觸發(fā),這只是通知套接字已空閑,用戶必須手動關(guān)閉連接。// 通過事件觸發(fā)方式來執(zhí)行disconnect動作this._socket.on("timeout",()=>this.emit("disconnect"))this._socket.on("error",()=>this.transition(this.ERROR))this._socket.on("data",this.onData.bind(this))}catch(e){if(retryCount<3){retry(1000) // 1000ms后重試執(zhí)行}else{ //throw e} }
}
當(dāng)TCPClient
實例化,狀態(tài)機(jī)處于IDLE
狀態(tài)(<tcp實例>.current.name=='IDLE'
),然后狀態(tài)機(jī)自動啟動(autoStart=true
)將轉(zhuǎn)換至Initial
狀態(tài)(initial
狀態(tài))。
- 狀態(tài)機(jī)轉(zhuǎn)換至
Initial
狀態(tài)前會調(diào)用onInitialEnter
。我們可以在此方法中創(chuàng)建TCP Socket
實例以及其他相關(guān)的初始化。 onInitialEnter
成功執(zhí)行完畢后,狀態(tài)機(jī)的狀態(tài)將轉(zhuǎn)換至Initial
。(IDLE
->Initial
)- 如果在
onInitialEnter
函數(shù)初始化失敗或出錯,則應(yīng)該拋出錯誤。錯誤將導(dǎo)致狀態(tài)機(jī)將無法轉(zhuǎn)換至Initial
狀態(tài),也就無法進(jìn)行后續(xù)的所有操作了。一般在初始化失敗時,會進(jìn)行如下操作:- 進(jìn)行重試操作,直至初始化成功(即成功創(chuàng)建好Socket并進(jìn)行相應(yīng)的事件綁定)。
- 反復(fù)重試多次失敗后,也可能會放棄重試,
TCP Client
將無法切換到Initial
狀態(tài),而是保持在IDLE
狀態(tài)。 - 當(dāng)條件具備時,狀態(tài)機(jī)需要重新運行(即調(diào)用
tcp.start()
來啟動狀態(tài)機(jī)),將重復(fù)上述過程。
第三步:連接服務(wù)器
當(dāng)TCPClient
實例初始化完成后,就可以開始連接服務(wù)器。我們可以在類上創(chuàng)建狀態(tài)機(jī)動作connect
,啟動連接操作。
import { state, FlexStateMachine } from "flexstate"class TcpClient extends FlexStateMachine{// 通過裝飾器來聲明這是一個狀態(tài)動作 @state({// 代表只能當(dāng)處于此三種狀態(tài)時才允許執(zhí)行動作,即調(diào)用連接方法when:["Initial","Disconnected","Error"], // 執(zhí)行后進(jìn)入正在連接中的狀態(tài)pending:"Connecting" }) async connect(){ this._socket.connect(this.options) }
}
// 創(chuàng)建連接實例
let tcp = new TcpClient({...})
// 連接
tcp.connect()
// 狀態(tài)機(jī)狀態(tài)將變化: Initial -> Connecting -> Connected
// 如果連接出錯狀態(tài)將變化:Initial -> Connecting -> Error
上述的@state({....})
定義了一個狀態(tài)機(jī)動作,代表當(dāng)調(diào)用connect
方法時會導(dǎo)致一系列的狀態(tài)轉(zhuǎn)換:
- 動作名稱為
connect
,會創(chuàng)建一個同名的實例方法tcp.connect
替換掉原始的connect
方法。 when
參數(shù)代表了只有當(dāng)前狀態(tài)為[Initial
、Disconnected
、Error
]其中一個時才允許執(zhí)行connect
動作。pending="Connecting"
代表,執(zhí)行connect
動作前,狀態(tài)機(jī)的狀態(tài)將暫時會切換至Connecting
,也就是會顯示正在連接中
。由于連接操作可能是耗時的,所有設(shè)計一個正在連接中是比較符合實際業(yè)務(wù)邏輯的。- 如果執(zhí)行
socket.connect({...})
出錯,可以通過@state({retry,retryCount})
來啟用重試邏輯。需要注意的是 調(diào)用connect
成功僅僅代表該方法在調(diào)用時沒有出錯,并不代表已經(jīng)連接成功。是否連接成功需要由socket/connect
事件來觸發(fā)確認(rèn)。 - 在上述中,并沒有顯式指定當(dāng)連接成功時的狀態(tài),原因是因為
connect
方法是一個異步方法,是否連接成功或失敗是通過事件回調(diào)的方式轉(zhuǎn)換狀態(tài)的。在初始化階段,我們訂閱了close
、end
等回調(diào)。this._socket.on("close",()=>this.transition(this.DISCONNECTED))
this._socket.on("end",()=>this.transition(this.DISCONNECTED))
this._socket.on("error",()=>this.transition(this.ERROR))
當(dāng)執(zhí)行socket.connect
方法后,如果接收到close
/end
/error
則會轉(zhuǎn)換到對應(yīng)的DISCONNECTED
、ERROR
狀態(tài)。
- 至此,實現(xiàn)了當(dāng)
tcp.connect
方法,狀態(tài)轉(zhuǎn)換到Connecting
狀態(tài),連接成功轉(zhuǎn)換至Connected
狀態(tài),連接被斷開轉(zhuǎn)換至Disconnected
狀態(tài),出現(xiàn)錯誤時轉(zhuǎn)換到ERROR
狀態(tài)。并且在出錯時會進(jìn)行一定重試操作,更多關(guān)于重試的內(nèi)容詳見后續(xù)介紹。
第四步:偵聽連接狀態(tài)
在TCP連接生命周期內(nèi),狀態(tài)機(jī)會在最后Initial/Connecting/Connected/Disconnecting/Disconnected/AlwaysDisconnected
狀態(tài)之間進(jìn)行轉(zhuǎn)換,我們希望可能偵聽狀態(tài)機(jī)的狀態(tài)轉(zhuǎn)換事件,以便在連接發(fā)生狀態(tài)轉(zhuǎn)換時進(jìn)行一些操作,此時就可以偵聽各種連接事件。
偵聽連接狀態(tài)有兩種方法:
FlexStateMachine
本身就是一個EventEmitter,可以通過訂閱事方式進(jìn)行偵聽。
// *****偵聽某個狀態(tài)事件*****tcp.on("Connected/enter",({from,to})=>{// 當(dāng)準(zhǔn)備進(jìn)入連接前狀態(tài)時觸發(fā)此事件
}) tcp.on("Connected/leave",({from,to})=>{// 當(dāng)準(zhǔn)備要離開連接狀態(tài)時觸發(fā)此事件
}) tcp.on("Connected/done",({from,to})=>{// 當(dāng)切換至連接狀態(tài)后觸發(fā)此事件
})
- 在類中也可以直接定義
on<狀態(tài)名>Enter
、on<狀態(tài)名>
、on<狀態(tài)名>Done
、on<狀態(tài)名>Leave
類方法來偵聽事件。
class TcpClient extends FlexStateMachine{onInitialEnter({from,to}){...} // 進(jìn)入Initial狀態(tài)前onInitial({from,to}){...} // 已切換至Initial狀態(tài)onInitialDone({from,to}){...} // ===onInitialonInitialLeave({from,to}){...} // 離開Initial狀態(tài)時onConnectingEnter({from,to}){...} // 進(jìn)入Connecting狀態(tài)前onConnecting({from,to}){...} // 已切換至Connecting狀態(tài)onConnectingDone({from,to}){...} // === onConnectingonConnectingLeave({from,to}){...} // 離開Connecting狀態(tài)時onConnectedEnter({from,to}){...} // 進(jìn)入Connected狀態(tài)前onConnected({from,to}){...} // 已切換至Connected狀態(tài)onConnectedDone({from,to}){...} // ===onConnectedonConnectedLeave({from,to}){...} // 離開Connected狀態(tài)時//...所有狀態(tài)均可以定義on<狀態(tài)名>Enter、on<狀態(tài)名>、on<狀態(tài)名>Leave事件 }
第五步:斷開重新連接
連接管理中的斷開重連是非常重要的功能,要處理此邏輯,首先分析一下什么情況下會斷開連接。
斷開連接一般包括主動
和被動
兩種情況:
- 服務(wù)器或網(wǎng)絡(luò)問題等導(dǎo)致的連接斷開
此種情況屬于客戶端被動斷開連接,一般會需要進(jìn)行自動重新連接。服務(wù)器主動斷開時,客戶端會偵聽到end
事件,直接進(jìn)入斷開狀態(tài)。即狀態(tài)機(jī)不會切換到Disconnecting
,而是直接至Disconnected
。
- 客戶端主動斷開連接
此種情況屬性客戶主動斷開連接發(fā),就是客戶端主動調(diào)用disconnect
方法,一般是不需要進(jìn)行自動重連的。
主動斷開時,需要調(diào)用socket.end
方法,然后等待end
事件的觸發(fā)。狀態(tài)機(jī)會經(jīng)歷從Disconnecting
到Disconnected
的過程。
無論是主動斷開連接還是被動斷開連接,均會觸發(fā)close
事件,因此需要在close
事件觸發(fā)時區(qū)別是主動斷開還是被動斷開。
為了更好地區(qū)別主動斷開
和被動斷開
,我們可以增加一個狀態(tài)AlwaysDisconnected
來代表是客戶端主動斷開,AlwaysDisconnected
被設(shè)計為FINAL
狀態(tài)。
當(dāng)狀態(tài)機(jī)切換到Disconnected
狀態(tài)時調(diào)用connect
動作方法來重新連接。當(dāng)狀態(tài)機(jī)切換到AlwaysDisconnected
時,則不進(jìn)行重新連接。
兩者差別在于,如果是主動斷開會經(jīng)歷Disconnecting
狀態(tài),而被動斷開則不會經(jīng)過此狀態(tài),因此我們就可以在on("close")
事件中處理將狀態(tài)轉(zhuǎn)換至AlwaysDisconnected
或DISCONNECTED
。
class TcpClient extends FlexStateMachine{class TcpClient extends FlexStateMachine{...// 轉(zhuǎn)換至Initial狀態(tài)前會調(diào)用方法async onInitialEnter({retry,retryCount}){// 在此需要確認(rèn)該切換到Disconnected還是AlwaysDisconnected狀態(tài)this._socket.on("close",()=>{// 主動調(diào)用disconnect方法時,狀態(tài)機(jī)才會切換到Disconnectingif(this.current.name==="Disconnecting"){ this.transition(this.ALWAYSDISCONNECTED)}else{this.transition(this.DISCONNECTED)}})}// 當(dāng)切換至Disconnected狀態(tài)的回調(diào)async onDisconnected({from,to}){await delay(3000)this.connect() // 重新執(zhí)行Connect動作}//async onConnectClosed({from,to}){}@state({when:"Connected",pending:"Disconnecting"// 由于調(diào)用end方法是異步操作,需要等待close事件觸發(fā)后,才是真正的斷開連接 // 因此,不能在調(diào)用disconnected返回后就將狀態(tài)設(shè)置為AlwaysDisconnected// 也就是說不要在此配置rejected參數(shù);// 假設(shè)執(zhí)行this._socket.end沒有出錯,則狀態(tài)將保持在Disconnecting狀態(tài),直至this._socket.on("close",callback)時才進(jìn)行狀態(tài)轉(zhuǎn)換// rejected:"" })async disconnect(){// 注意:此操作是異步狀態(tài)this._socket.end() }
}
第六步:連接認(rèn)證子狀態(tài)
當(dāng)tcp連接成功后,一般服務(wù)器會要求對客戶連接進(jìn)行認(rèn)證才允許進(jìn)行使用,而認(rèn)證操作(login/logout
)是一個耗時的異步操作,同樣需要進(jìn)行狀態(tài)管理。當(dāng)進(jìn)入Connected
狀態(tài)后,狀態(tài)將在未認(rèn)證
、正在認(rèn)證
、已認(rèn)證
三個狀態(tài)間進(jìn)行轉(zhuǎn)換,并且在連接斷開或者出錯時馬上退出這三個狀態(tài)。因此,就有必要引入子狀態(tài)的概念。
引入子狀態(tài)后,對應(yīng)的狀態(tài)圖更新如下:
class TcpClient extends FlexStateMachine{static states = { Connected : { value:2, title:"已連接", next:["Disconnecting","Disconnected","Error"] // 定義一個獨立的狀態(tài)機(jī)域scope:{states:{Unauthenticated : {value:0,title:"未認(rèn)證",initial:true,next:["Authenticating"]},Authenticating : {value:1,title:"正在認(rèn)證",next:["Authenticated"]}Authenticated : {value:2,title:"已認(rèn)證",next:["Unauthenticated"]},}}}, } ......// 當(dāng)狀態(tài)機(jī)進(jìn)入Connected后會啟動其子狀態(tài)機(jī)// 子狀態(tài)機(jī)會轉(zhuǎn)換到其初始狀態(tài)Unauthenticated,然后就可以在此執(zhí)行登錄動作async onUnauthenticatedEnter({from,to}){this.login() // }onAuthenticated({from,to}){}@state({when:["Authenticating"],pending:["Authenticating"]})async login(){await this.send({// 認(rèn)證信息})}@state({when:["Authenticated"] })async logout(){await this.send({// 注銷信息}) }
}
推薦
以下是我的一大波開源項目推薦:
- 全流程一健化React/Vue/Nodejs國際化方案 - VoerkaI18n
- 無以倫比的React表單開發(fā)庫 - speedform
- 終端界面開發(fā)增強庫 - Logsets
- 簡單的日志輸出庫 - VoerkaLogger
- 裝飾器開發(fā) - FlexDecorators
- 有限狀態(tài)機(jī)庫 - FlexState
- 通用函數(shù)工具庫 - FlexTools
- 小巧優(yōu)雅的CSS-IN-JS庫 - Styledfc
- 為JSON文件添加注釋的VSCODE插件 - json_comments_extension
- 開發(fā)交互式命令行程序庫 - mixed-cli
- 強大的字符串插值變量處理工具庫 - flexvars
- 前端link調(diào)試輔助工具 - yald
- 異步信號 - asyncsignal