基于有限状态机开发健壮的Nodejs/TCP客户端

有限状态机是一种数学计算模型,它描述了在任何给定时间只能处于一种状态的系统的行为。形式上,有限状态机有五个部分:

  • 初始状态值 (initial state)
  • 有限的一组状态 (states)
  • 有限的一组事件 (events)
  • 由事件驱动的一组状态转移关系 (transitions)
  • 有限的一组最终状态 (final states)

状态是指由状态机建模的系统中某种有限的定性的“模式”或“状态”,并不描述与该系统相关的所有(可能是无限的)数据。例如,水可以处于以下 4 种状态中的一种:冰、液体、气体或等离子体。然而,水的温度可以变化,所以其测量值是定量的和无限的。再比如管理TCP Socket连接时,其生命周期内存在明显的有限状态转换。

可能有相当多的同学在开发中没意识到有限状态机的作用,但是实际上,我们几乎无时不刻在有意无意间使用了有限状态机。当您在开发过程中能有意识地系统地进行有限状态分析并应用有限状态机,往往代表着您达到了较高的水平。

目前开源的有限状态机实现中比较知名的有:

  • xstate:堪称状态机航空母舰,功能太强大了,也太复杂了,学习成本非常高。
  • Javascript State Machine:功能较弱,在实际试用过程中发现在进行异步切换时存在问题。
  • jssm:特点是引入自己的DSL语法来描述状态机,使用起来比较别扭。

事实上,从功能完整度上看xstate是第一选择,但是其过于复杂了,在功能与易用平衡方面并不理想。

因此,我们开发了FlexState有限状态机,力求在功能性、易用性上达到平衡

FlexState是一款简单易用的有限状态机,具有以下特性:

  1. 支持基于Class构建有限状态机实例
  2. 支持状态enter/leave/resume/done钩子事件
  3. 状态切换完全支持异步操作
  4. 支持定义异步状态动作Action
  5. 支持状态切换生命周期事件订阅
  6. 支持错误处理和状态切换中止
  7. 基于TypeScript开发
  8. 支持子状态
  9. 核心代码90%+单元测试覆盖率

Github
官网

快速入门

下面我们以开发基于nodejs/net.socket的TCP客户端为例来说明FlexStateMachine的使用。

作为例子,我们为TCPClient设计以下几种状态:

  • Initial:初始状态,构建socket实例后处于该阶段。
  • Connecting:连接中,当调用Connect方法,触发connect事件前。
  • Connected:已连接,当触发connect事件后。
  • Disconnecting:正在断开,当调用destory或end方法后,end/close事件触发前。
  • Disconnected:被动断开,当触发end/close事件后。
  • AlwaysDisconnected: 主动断开状态
  • IDLE: 自动添加的空闲状态,状态机未启动时
  • ERROR: 自动添加的错误状态,特殊的FINAL状态

TCPClient的状态图如下:

第一步:构建状态机

推荐直接继承FlexStateMachine来创建一个TCPClient实例,该种方式更加简单易用。


import { state, FlexStateMachine } from "flexstate"class TcpClient extends FlexStateMachine{// 定义状态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,                // 状态上下文对象,当执行动作或状态转换事件时的this指向autoStart         : true,                // 自动启动状态机timeout           : 30 * 1000            // 当执行状态切换回调时的超时,如enter、leave、done回调injectActionMethod: true,                // 将动作方法注入到当前实例中  },options)) } @state{when:["Initial","Disconnected","Error"],   // 代表只能当处于此三种状态时才允许调用连接方法    pending:"Connecting",						// 执行后进入正在连接中的状态}connect(){this._socket.connect(this.options)    } @state({when:["Connected"],     	// 代表只有在已连接状态才允许执行断开方法pending:"Disconnecting"})  disconnect(){this._socket.destory()}// 当状态转换成功后会调用此方法ontTransition({error,from,to,done,timeConsuming}){console.log(`从<${previous}>转换到<current>,耗时:${timeConsuming}ms`)       // 例 ==> 从<Connecting>转换到<Connected,耗时12ms>console.log(this.current)                          // {name,value,....}}onData(data){....}}

说明:

  • 以上我们创建了一个继承自FlexStateMachine来创建一个TCPClient实例
  • 并且定义了InitialConnectingConnectedDisconnectingDisconnectedAlwaysDisconnected共六个状态以及状态之间的转换约束。同时,状态机还会自动添加一个ERRORIDLE状态。
  • 定义了connectdisconnect两个动作action,在这两个方法前添加@state代表了当执行这两个方法会导致状态变化。

第二步:初始化TCPSocket

当实例化TCPClient实例后,首先应该创建Socket实例。由于TCPClient实例继承自FlexStateMachine,并且我们指定了Initial为初始化状态。
状态机会在实例化并启动后自动转换到Initial状态。因此,我们可以在进入Initial状态前进行初始化操作。

class TcpClient extends FlexStateMachine{// 转换至Initial状态前会调用方法async onInitialEnter({retry,retryCount}){try{      this._socket = new net.Socket()// 当连接成功时,切换到Connected事件; 每一个状态均有一个大写的状态值实例成员// this.CONNECTED==this.states.Connected.valuethis._socket.on("connect",()=>this.transition(this.CONNECTED)) this._socket.on("close",()=>{//....     详见后续重连说明}) 		// 套接字因不活动而超时则触发,这只是通知套接字已空闲,用户必须手动关闭连接。// 通过事件触发方式来执行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后重试执行}else{				//throw e}      }
}

TCPClient实例化,状态机处于IDLE状态(<tcp实例>.current.name=='IDLE'),然后状态机自动启动(autoStart=true)将转换至Initial状态(initial状态)。

  • 状态机转换至Initial状态前会调用onInitialEnter。我们可以在此方法中创建TCP Socket实例以及其他相关的初始化。
  • onInitialEnter成功执行完毕后,状态机的状态将转换至Initial。(IDLE->Initial
  • 如果在onInitialEnter函数初始化失败或出错,则应该抛出错误。错误将导致状态机将无法转换至Initial状态,也就无法进行后续的所有操作了。一般在初始化失败时,会进行如下操作:
    • 进行重试操作,直至初始化成功(即成功创建好Socket并进行相应的事件绑定)。
    • 反复重试多次失败后,也可能会放弃重试,TCP Client将无法切换到Initial状态,而是保持在IDLE状态。
    • 当条件具备时,状态机需要重新运行(即调用tcp.start()来启动状态机),将重复上述过程。

第三步:连接服务器

TCPClient实例初始化完成后,就可以开始连接服务器。我们可以在类上创建状态机动作connect,启动连接操作。

import { state, FlexStateMachine } from "flexstate"class TcpClient extends FlexStateMachine{// 通过装饰器来声明这是一个状态动作  @state({// 代表只能当处于此三种状态时才允许执行动作,即调用连接方法when:["Initial","Disconnected","Error"],      // 执行后进入正在连接中的状态pending:"Connecting"	})                async connect(){      this._socket.connect(this.options)          }
}
// 创建连接实例
let tcp = new TcpClient({...})
// 连接
tcp.connect()   
// 状态机状态将变化: Initial -> Connecting -> Connected
// 如果连接出错状态将变化:Initial -> Connecting -> Error

上述的@state({....})定义了一个状态机动作,代表当调用connect方法时会导致一系列的状态转换:

  • 动作名称为connect,会创建一个同名的实例方法tcp.connect替换掉原始的connect方法。
  • when参数代表了只有当前状态为[InitialDisconnectedError]其中一个时才允许执行connect动作。
  • pending="Connecting"代表,执行connect动作前,状态机的状态将暂时会切换至Connecting,也就是会显示正在连接中。由于连接操作可能是耗时的,所有设计一个正在连接中是比较符合实际业务逻辑的。
  • 如果执行socket.connect({...}) 出错,可以通过@state({retry,retryCount})来启用重试逻辑。需要注意的是 调用connect成功仅仅代表该方法在调用时没有出错,并不代表已经连接成功。是否连接成功需要由socket/connect事件来触发确认。
  • 在上述中,并没有显式指定当连接成功时的状态,原因是因为connect方法是一个异步方法,是否连接成功或失败是通过事件回调的方式转换状态的。在初始化阶段,我们订阅了closeend等回调。
    • this._socket.on("close",()=>this.transition(this.DISCONNECTED))
    • this._socket.on("end",()=>this.transition(this.DISCONNECTED))
    • this._socket.on("error",()=>this.transition(this.ERROR))

当执行socket.connect方法后,如果接收到close/end/error则会转换到对应的DISCONNECTEDERROR状态。

  • 至此,实现了当tcp.connect方法,状态转换到Connecting状态,连接成功转换至Connected状态,连接被断开转换至Disconnected状态,出现错误时转换到ERROR状态。并且在出错时会进行一定重试操作,更多关于重试的内容详见后续介绍。

第四步:侦听连接状态

在TCP连接生命周期内,状态机会在最后Initial/Connecting/Connected/Disconnecting/Disconnected/AlwaysDisconnected状态之间进行转换,我们希望可能侦听状态机的状态转换事件,以便在连接发生状态转换时进行一些操作,此时就可以侦听各种连接事件。

侦听连接状态有两种方法:

  • FlexStateMachine本身就是一个EventEmitter,可以通过订阅事方式进行侦听。
// *****侦听某个状态事件*****tcp.on("Connected/enter",({from,to})=>{// 当准备进入连接前状态时触发此事件
})      tcp.on("Connected/leave",({from,to})=>{// 当准备要离开连接状态时触发此事件
})  tcp.on("Connected/done",({from,to})=>{// 当切换至连接状态后触发此事件
})   
  • 在类中也可以直接定义on<状态名>Enteron<状态名>on<状态名>Doneon<状态名>Leave类方法来侦听事件。
class TcpClient extends FlexStateMachine{onInitialEnter({from,to}){...}			// 进入Initial状态前onInitial({from,to}){...}					// 已切换至Initial状态onInitialDone({from,to}){...}				// ===onInitialonInitialLeave({from,to}){...}		  	// 离开Initial状态时onConnectingEnter({from,to}){...}			// 进入Connecting状态前onConnecting({from,to}){...}				// 已切换至Connecting状态onConnectingDone({from,to}){...}      	// === onConnectingonConnectingLeave({from,to}){...}		  	// 离开Connecting状态时onConnectedEnter({from,to}){...}			// 进入Connected状态前onConnected({from,to}){...}				// 已切换至Connected状态onConnectedDone({from,to}){...}			// ===onConnectedonConnectedLeave({from,to}){...}		  	// 离开Connected状态时//...所有状态均可以定义on<状态名>Enter、on<状态名>、on<状态名>Leave事件 }

第五步:断开重新连接

连接管理中的断开重连是非常重要的功能,要处理此逻辑,首先分析一下什么情况下会断开连接。

断开连接一般包括主动被动两种情况:

  • 服务器或网络问题等导致的连接断开

此种情况属于客户端被动断开连接,一般会需要进行自动重新连接。服务器主动断开时,客户端会侦听到end事件,直接进入断开状态。即状态机不会切换到Disconnecting,而是直接至Disconnected

  • 客户端主动断开连接

此种情况属性客户主动断开连接发,就是客户端主动调用disconnect方法,一般是不需要进行自动重连的。
主动断开时,需要调用socket.end方法,然后等待end事件的触发。状态机会经历从DisconnectingDisconnected的过程。

无论是主动断开连接还是被动断开连接,均会触发close事件,因此需要在close事件触发时区别是主动断开还是被动断开。
为了更好地区别主动断开被动断开,我们可以增加一个状态AlwaysDisconnected来代表是客户端主动断开,AlwaysDisconnected被设计为FINAL状态。
当状态机切换到Disconnected状态时调用connect动作方法来重新连接。当状态机切换到AlwaysDisconnected时,则不进行重新连接。
两者差别在于,如果是主动断开会经历Disconnecting状态,而被动断开则不会经过此状态,因此我们就可以在on("close")事件中处理将状态转换至AlwaysDisconnectedDISCONNECTED

class TcpClient extends FlexStateMachine{class TcpClient extends FlexStateMachine{...// 转换至Initial状态前会调用方法async onInitialEnter({retry,retryCount}){// 在此需要确认该切换到Disconnected还是AlwaysDisconnected状态this._socket.on("close",()=>{// 主动调用disconnect方法时,状态机才会切换到Disconnectingif(this.current.name==="Disconnecting"){ this.transition(this.ALWAYSDISCONNECTED)}else{this.transition(this.DISCONNECTED)}})}// 当切换至Disconnected状态的回调async onDisconnected({from,to}){await delay(3000)this.connect()								// 重新执行Connect动作}//async onConnectClosed({from,to}){}@state({when:"Connected",pending:"Disconnecting"// 由于调用end方法是异步操作,需要等待close事件触发后,才是真正的断开连接 // 因此,不能在调用disconnected返回后就将状态设置为AlwaysDisconnected// 也就是说不要在此配置rejected参数;// 假设执行this._socket.end没有出错,则状态将保持在Disconnecting状态,直至this._socket.on("close",callback)时才进行状态转换// rejected:""  })async disconnect(){// 注意:此操作是异步状态this._socket.end()   }
}

第六步:连接认证子状态

当tcp连接成功后,一般服务器会要求对客户连接进行认证才允许进行使用,而认证操作(login/logout)是一个耗时的异步操作,同样需要进行状态管理。当进入Connected状态后,状态将在未认证正在认证已认证三个状态间进行转换,并且在连接断开或者出错时马上退出这三个状态。因此,就有必要引入子状态的概念。

引入子状态后,对应的状态图更新如下:

在这里插入图片描述

class TcpClient extends FlexStateMachine{static states = { Connected		: { value:2, title:"已连接", next:["Disconnecting","Disconnected","Error"] // 定义一个独立的状态机域scope:{states:{Unauthenticated : {value:0,title:"未认证",initial:true,next:["Authenticating"]},Authenticating  : {value:1,title:"正在认证",next:["Authenticated"]}Authenticated   : {value:2,title:"已认证",next:["Unauthenticated"]},}}},  }  ......// 当状态机进入Connected后会启动其子状态机// 子状态机会转换到其初始状态Unauthenticated,然后就可以在此执行登录动作async onUnauthenticatedEnter({from,to}){this.login()								// }onAuthenticated({from,to}){}@state({when:["Authenticating"],pending:["Authenticating"]})async login(){await this.send({// 认证信息})}@state({when:["Authenticated"] })async logout(){await this.send({// 注销信息})    }
}

推荐

以下是我的一大波开源项目推荐:

  • 全流程一健化React/Vue/Nodejs国际化方案 - VoerkaI18n
  • 无以伦比的React表单开发库 - speedform
  • 终端界面开发增强库 - Logsets
  • 简单的日志输出库 - VoerkaLogger
  • 装饰器开发 - FlexDecorators
  • 有限状态机库 - FlexState
  • 通用函数工具库 - FlexTools
  • 小巧优雅的CSS-IN-JS库 - Styledfc
  • 为JSON文件添加注释的VSCODE插件 - json_comments_extension
  • 开发交互式命令行程序库 - mixed-cli
  • 强大的字符串插值变量处理工具库 - flexvars
  • 前端link调试辅助工具 - yald
  • 异步信号 - asyncsignal

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/276412.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分析型数据库的主要使用场景有哪些?

如今数据已经成为了企业和组织的核心资产。如何有效地管理和利用这些数据&#xff0c;成为了决定竞争力的关键。分析型数据库作为数据处理领域的重要工具&#xff0c;为各行各业提供了强大的数据分析和洞察能力。基于分析型数据库&#xff08;Apache Doris &#xff09;构建的现…

Linux中GPU相关命令

Linux查看显卡信息&#xff1a; lspci | grep -i vga 使用nvidia GPU可以&#xff1a; lspci | grep -i nvidia1 前边的序号 "00:0f.0"是显卡的代号(这里是用的虚拟机); 查看指定显卡的详细信息用以下指令&#xff1a; lspci -v -s 00:0f.01 Linux查看Nvidia显…

MongoDB实战面试指南:常见问题一网打尽

码到三十五 &#xff1a; 个人主页 心中有诗画&#xff0c;指尖舞代码&#xff0c;目光览世界&#xff0c;步履越千山&#xff0c;人间尽值得 ! MongoDB是一款流行的非关系型数据库&#xff0c;以其高效、可扩展的特性受到开发者的青睐。了解MongoDB的架构、存储引擎和数据结…

Jmeter进行http接口测试

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 本文主要针对http接口进行测试&#xff0c;使用 jmeter工具实现…

vue3+ts props定义识别为unknown

"vue": "^3.3.4", "typescript": "5.0.4", 确保agriculturalPollution引入成功确保PropType引入成功details获得类型推断defineProps传参正确props的detail为unknown 这就很奇怪&#xff0c;一步步都是按照规范写的&#xff0c;但是…

目前研一,是选 FPGA 还是 Linux 嵌入式?

目前研一&#xff0c;是选 FPGA 还是 Linux 嵌入式? 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「Linux 的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&a…

【论文阅读】MSGNet:学习多变量时间序列预测中的多尺度间序列相关性

MSGNet&#xff1a;学习多变量时间序列预测中的多尺度间序列相关性 文献介绍摘要总体介绍背景及当前面临的问题现有解决方案及其局限性本文的解决方案及其贡献 背景知识的相关工作背景知识问题表述&#xff1a; Method论文主要工作1.输入嵌入和剩余连接 (Input Embedding and R…

论文阅读——RingMo

RingMo: A Remote Sensing Foundation Model With Masked Image Modeling 与自然场景相比&#xff0c;RS图像存在以下困难。 1&#xff09;分辨率和方位范围大&#xff1a;受遥感传感器的影响&#xff0c;图像具有多种空间分辨率。此外&#xff0c;与自然图像的实例通常由于重…

3、设计模式之工厂模式1(Factory)

工厂模式是什么&#xff1f;     工厂模式是一种创建者模式&#xff0c;用于封装和管理对象的创建&#xff0c;屏蔽了大量的创建细节&#xff0c;根据抽象程度不同&#xff0c;主要分为简单工厂模式、工厂方法模式以及抽象工厂模式。 简单工厂模式 看一个具体的需求 看一个…

华为配置ISP选路实现报文按运营商转发

CLI举例&#xff1a;配置ISP选路实现报文按运营商转发 介绍通过配置ISP选路实现报文按运营商转发的配置举例。 组网需求 如图1所示&#xff0c;FW作为安全网关部署在网络出口&#xff0c;企业分别从ISP1和ISP2租用一条链路。 企业希望访问Server 1的报文从ISP1链路转发&#…

C语言例:表达式(a=2,3),a+1的值

题目&#xff1a;设int a; 则表达式(a2,3),a1的值 #include<stdio.h> int main(void) {int a0;int b;int c;b (a2,4);c (a2,3),a1;printf("a1%d\n",a1); //a1 3;printf("a2,4的值为&#xff1a;%d\n",b); //a2,4的值为&…

今日AI:苹果大模型MM1入场;Sora训练数据来源竟来自这;全球最快AI芯片WSE-3发布;阿里邀请AI参加数学竞赛

欢迎来到【今日AI】栏目!这里是你每天探索人工智能世界的指南&#xff0c;每天我们为你呈现AI领域的热点内容&#xff0c;聚焦开发者&#xff0c;助你洞悉技术趋势、了解创新AI产品应用。 新鲜AI产品点击了解&#xff1a;AIbase - 智能匹配最适合您的AI产品和网站 &#x1f4f…

C语言指针与数组(不适合初学者版):一篇文章带你深入了解指针与数组!

&#x1f388;个人主页&#xff1a;JAMES别扣了 &#x1f495;在校大学生一枚。对IT有着极其浓厚的兴趣 ✨系列专栏目前为C语言初阶、后续会更新c语言的学习方法以及c题目分享. &#x1f60d;希望我的文章对大家有着不一样的帮助&#xff0c;欢迎大家关注我&#xff0c;我也会回…

大语言模型:Large Language Models Are Human-Level Prompt Engineers概述

研究内容 如何通过prompt&#xff0c;在不进行微调大语言模型的前提下&#xff0c;增加大语言模型的表现 研究动机 prompt非常有用&#xff0c;但是人工设置的非常不自然&#xff1b;因此提出了要自动使用大语言模型自己选择prompt&#xff1b;取得了很好的效果。 作者主要…

【全志H616】1 --用orangepi控制硬件

【全志H616】1 --用orangepi控制硬件 本文介绍了如歌用orangepi 控制蜂鸣器&超声波模块&#xff0c;通过键盘输入1、2、3、4来控制转动角度舵机模块&#xff1b;同时还介绍了利用全志如何配置定时器&#xff1b;以及查看H616引脚状态的命令等… 超声波模块和舵机模块的讲解…

WordPress供求插件API文档:获取市场类型

请注意&#xff0c;该文档为&#xff1a; WordPress供求插件&#xff1a;一款专注于同城生活信息发布的插件-CSDN博客文章浏览阅读396次&#xff0c;点赞6次&#xff0c;收藏5次。WordPress供求插件&#xff1a;sliver-urban-life 是一款专注于提供同城生活信息发布与查看的插件…

JVM 面试——G1和ZGC的区别

ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器ZGC的目标主要有4个 支持TB量级的堆。我们生产环境的硬盘还没有上TB呢&#xff0c;这应该可以满足未来十年内&#xff0c;所有JAVA应用的需求了吧。最大GC停顿时间不超10ms。目前一般线上环境运行良好的JAVA应用Minor …

接口自动化测试框架postman tests常用方法

postman常用方法集合&#xff1a; 1.设置环境变量 postman.setEnvironmentVariable("key", "value"); pm.environment.set("key", "value");//postman 5.0以上版本设置环境变量的方法 2.设置全局变量 postman.setGlobalVariable(&…

杂七杂八111

MQ 用处 一、异步。可提高性能和吞吐量 二、解耦 三、削峰 四、可靠。常用消息队列可以保证消息不丢失、不重复消费、消息顺序、消息幂等 选型 一Kafak:吞吐量最大&#xff0c;性能最好&#xff0c;集群高可用。缺点&#xff1a;会丢数据&#xff0c;功能较单一。 二Ra…

文献速递:深度学习乳腺癌诊断---使用深度学习改善乳腺癌组织学分级

Title 题目 Improved breast cancer histological grading using deep learning 使用深度学习改善乳腺癌组织学分级 01 文献速递介绍 乳腺癌组织学分级是乳腺癌中一个确立的临床变量&#xff0c;它包括来自三个方面的信息&#xff0c;即小管形成程度、核多态性和有丝分裂计…