先说下这个项目想要解决的实际问题。
之前一直在做呼叫流程控制的程序,涉及到的业务流程非常多,经常需要根据业务流程的变化变更代码。
现在想要把业务流程部分通过前端编辑的方式进行重排,最后选型jsplumb作为前端控制组件。
主要实现以下功能:
- 通过拖拽的方式实现状态机模型的绘制和连接
- 能够保存所绘制的模型关系并以文件存储
- 能够通过绘制的关系正确执行对应的业务流程
本例子中的状态机业务为volte中的呼叫业务,volte中使用SIP协议,在理解业务的时候最好向阅读SIP相关文档,对 SIP的事务状态机有一定的了解。下图为SIP协议的事务状态机:
在实际例中按照使用者对电话的理解将状态机抽象成CALLINIT(准备呼叫)、CALLRING(振铃状态)、CALLCONNECT(摘机状态)、CALLDISCONNECT(挂机状态)、 CALLEND(呼叫结束状态)。这几个状态对普通电话也适用,针对手机增加了ATTACH(附着状态)、针对VOLTE增加了REGISTER(SIP注册状态),对应也增加了DEATTCH(去附着状态)和 UNREGISTER(注销状态)。
关于jsplumb的基本知识这里不做介绍,可以自行参考官网的文档和网友贡献的中文文档。
官方地址 https://jsplumbtoolkit.com//
jsplumb支持多种图形绘制,此处只是用了jsplumb的 statemathine功能。
方式一:在线预览
jsplumb效果demo网址
方式二:本地预览
git clone https://github.com/chenwenti/jsplumb-statemachine.git
用浏览器直接打开static下的callflowDemo.html页面(注意:低版本IE浏览器不支持)
方式三:在flask下预览
下载源码后安装flask库,运行CallflowFlaskDemo.py预览.
本demo参考了github上的jsworkflow项目,为了满足需求进行了改动。 增加了lable参数,增加了保存、导入、清除、拖拽功能,对存储的结构按需要进行优化。
为使绘制的图形能传递完整信息,需要在绘制的线上增加参数。jsplumb关于lable部分是通过overlays来实现,
默认参数下会创建ConnectionOverlays图层,id设置为defaultlabel。
// Import all the given defaults into this instance.
instance.importDefaults({
Endpoint: ["Dot", {
radius: 0.1
}],
HoverPaintStyle: {
strokeStyle: "#6699FF",
lineWidth: 2
},
ConnectionOverlays: [
["Arrow", {
location: 1,
id: "arrow",
length: 14,
foldback: 0.4
}],
["Label", {
//label: "transition",
id: "defaultlabel",
cssClass: "aLabel",
events:{
click (label) {
// var bc=prompt("shuru...");
// label.setLabel(bc);
}
}
}]
],
在手动拖动创建连线时创建的是overlay图层 ,id设置为connectlable。
instance.createStateTrasitions = function (workflowData) {
var transitions=workflowData.transitions,
source,target,label;
for (var tr in transitions) {
var trx=transitions[tr];
labtext = trx["label"];
var conn=instance.connect({
source: trx["source"],
target: trx["target"],
labelStyle: {cssClass: "aLabel",},
overlays: [
//ConnectionOverlays: [
["Label", {
cssClass: "aLabel",
label: labtext,
location: 0.5,
id: "connectlabel",
}],
]
});
//conn.setLabel(trx["label"]);
导入的和手动绘制的两个图层不一样,在修改和保持时进行了判断。这里对overlay的理解还不是很透彻,或许有更好的 处理办法。
内容用JSON格式存储,transitions下记录了原状态到目的状态,lable记录的中间的触发参数。 names记录了所有的状态机变量,positions记录的是所在位置,从文件导入到web页面上使用,在做状态机解释时候不需要。
{
"transitions": {
"tr0": {
"source": "idle",
"target": "attach",
"label": "start"
},
"tr1": {
"source": "attach",
"target": "register",
"label": "attach_success"
},
"tr2": {
"source": "attach",
"target": "callend",
"label": "attach_faile"
},
"tr3": {
"source": "attach",
"target": "callend",
"label": "attach_timeout"
},
"tr4": {
"source": "register",
"target": "callend",
"label": "register_faile"
},
"tr5": {
"source": "register",
"target": "callend",
"label": "register_timeout"
},
"tr6": {
"source": "callend",
"target": "unregister",
"label": "is_need_unregister"
},
"tr7": {
"source": "callend",
"target": "unattach",
"label": "is_need_unattach"
},
"tr8": {
"source": "unregister",
"target": "unattach",
"label": "is_need_unattach"
},
"tr9": {
"source": "register",
"target": "messagemo",
"label": "register_success"
},
"tr10": {
"source": "messagemo",
"target": "callend",
"label": "message_success"
},
"tr11": {
"source": "messagemo",
"target": "callend",
"label": "message_faile"
},
"tr12": {
"source": "messagemo",
"target": "callend",
"label": "message_timeout"
}
},
"names": {
"idle": "IDLE",
"attach": "ATTACH",
"register": "REGISTER",
"callend": "CALLEND",
"unregister": "UNREGISTER",
"unattach": "UNATTACH",
"messagemo": "MESSAGEMO"
},
"positions": {
"idle": {
"top": 147,
"left": 368.90625
},
"attach": {
"top": 263,
"left": 305.90625
},
"register": {
"top": 384,
"left": 291.90625
},
"callend": {
"top": 303,
"left": 954.859375
},
"unregister": {
"top": 473,
"left": 896.859375
},
"unattach": {
"top": 581,
"left": 1091.859375
},
"messagemo": {
"top": 508,
"left": 515.6875
}
},
"container": "workflow-2"
}
在设计状态机解释器时首先需要掌握状态机的数学模型公式:
有限状态机是一个五元组M=(Q,Σ,δ,q0,F),其中:
Q={q0,q1,...,qn}是有限状态集合。在任一确定的时刻,有限状态机只能处于一个确定的状态qi;
Σ={σ1,σ2,...,σn}是有限输入字符集合。在任一确定的时刻,有限状态机只能接收一个确定的输入σj;
δ:Q×Σ→Q是状态转移函数,在某一状态下,给定输入后有限状态机将转入状态迁移函数决定的一个新状态;
q0∈Q是初始状态,有限状态机由此状态开始接收输入;
F∈Q是最终状态集合,有限状态机在达到终态后不再接收输入。
因此我们在设计状态机软件模型的时候主要是要抽象归纳业务模型,套用公式。
在本例中状态机的的状态Q设计为
Q={IDLE,ATTACH,REGISTER,CALLINIT,CALLRING,CALLCONNECT,CALLDISCONNECT,CALLEND,UNREGISTER,DEATTACH};
Σ={start,attach_success,attach_faile,attach_timeout,register_success,register_faile,register_timeout, callring,callinit_timeout,callring_timeout,callconnect,calldisconnect..........}
δ是每一个事件对应执行的动作
δ={before_start2attach().before_attach2reg(),before_attach2callend(),..........}
初始状态q0= IDLE
最终状态F = DEATTACH
为了实现以上的数学模型,选择使用了python下的transitions库来解析,实现demo。 具体介绍可以参考官方文档 https://github.com/pytransitions/transitions
所有的状态机都需要有定时器处理,因此需要使用transitions的timeout扩展属性
先定义state变量,假设都超时10秒,超时后执行超时函数并触发超时事件。
states = [{'name': 'IDLE'},
{'name': 'ATTACH','timeout': 10, 'on_timeout':['on_timeout_attach','attach_timeout']},
{'name': 'REGISTER', 'timeout': 10, 'on_timeout': ['on_timeout_register','register_timeout']},
{'name': 'CALLINIT','timeout': 10, 'on_timeout': ['on_timeout_callinit','callinit_timeout']},
{'name': 'CALLRING','timeout': 10, 'on_timeout': ['on_timeout_callring','callring_timeout']},
{'name': 'CALLCONNECT','timeout': 10, 'on_timeout': ['on_timeout_callconnect','callconnect_timeout']},
{'name': 'CALLDISCONNECT','timeout': 10, 'on_timeout': ['on_timeout_calldisconnect','calldisconnect_timeout']},
{'name': 'CALLEND','timeout': 10, 'on_timeout': ['on_timeout_callend']},
{'name': 'UNREGISTER','timeout': 10, 'on_timeout': ['on_timeout_unregister','unregister_timeout']},
{'name': 'DEATTACH','timeout': 10, 'on_timeout': ['on_timeout_deattach','deattach_timeout']}
]
然后定义触发器,这里可以根据jsplumb编写的json文件自动转换成这两个定义。
transitions = [
{'trigger': 'start', 'source': 'IDLE', 'dest': 'ATTACH', 'before': 'before_start2attach'},
{'trigger': 'attach_success', 'source': 'ATTACH', 'dest': 'REGISTER', 'before': 'before_attach2reg' },
{'trigger': 'attach_faile', 'source': 'ATTACH', 'dest': 'CALLEND', 'before': 'before_attach2callend' },
{'trigger': 'attach_timeout', 'source': 'ATTACH', 'dest': 'CALLEND', 'before': 'before_attach2callend' },
{'trigger': 'register_success', 'source': 'REGISTER', 'dest': 'CALLINIT', 'before': 'before_reg2callinit' },
{'trigger': 'register_faile', 'source': 'REGISTER', 'dest': 'CALLEND', 'before': 'before_reg2callend' },
{'trigger': 'register_timeout', 'source': 'REGISTER', 'dest': 'CALLEND', 'before': 'before_reg2callend' },
{'trigger': 'callring', 'source': 'CALLINIT', 'dest': 'CALLRING', 'before': 'before_reg2callinit'},
{'trigger': 'callinit_timeout', 'source': 'CALLINIT', 'dest': 'CALLEND', 'before': 'before_reg2callend'},
{'trigger': 'callring_timeout', 'source': 'CALLRING', 'dest': 'CALLEND', 'before': 'before_callring2callend'},
{'trigger': 'callconnect', 'source': 'CALLRING', 'dest': 'CALLCONNECT', 'before': 'before_reg2callend'},
{'trigger': 'calldisconnect', 'source': 'CALLRING', 'dest': 'CALLDISCONNECT', 'before': 'before_callring2calldisconnect'},
{'trigger': 'calldisconnect', 'source': 'CALLCONNECT', 'dest': 'CALLDISCONNECT', 'before': 'before_callconnect2calldisconnect'},
{'trigger': 'callconnect_timeout', 'source': 'CALLCONNECT', 'dest': 'CALLCONNECT', 'before': 'timeout2handup'},
{'trigger': 'callend_timeout', 'source': 'CALLEND', 'dest': 'CALLEND', 'before': 'before_reg2callend'},
]
实现类函数
from time import sleep
from transitions import Machine
from transitions.extensions.states import add_state_features, Tags, Timeout
import threading
from queue import Queue
from threading import *
@add_state_features(Tags, Timeout)
class JsExtenStateMachine(Machine):
pass
class VolteCallStateMachine(object):
def __init__(self):
self.entourage = 0
#超时处理函数
def on_enter_waiting(self):
self.entourage += 1
def on_timeout_attach(self):
print ("on_timeout_attach dosomthing")
def on_timeout_register(self):
print ("on_timeoute_register do something")
....
实现每个函数功能
例子函数中做了两个队列,一个模拟输入事件,一个取出事件,因为事件都是字符串,因此需要需要转换成 函数处理,使用了getattr函数。
# 从队列中取出事件并执行到最终的状态
def geteventsfromqueue():
sleep(2)
print("start geteventsfromqueue")
while(1):
print(q.qsize())
event = q.get()
print ('recv event = '+event)
func = getattr(volte401,event, None)
func()
print("now state=" + volte401.state)
sleep(3)
python3.6
安装transitions后直接运行
1.lable参数可设计数据格式,增加比如timeout时长,条件等。
例子可以设计成 项2= 参数1,项目2=参数2
如fun=xxxx,timer=10表示执行什么函数,超时时长多少。
2.lable中的参数是输入的,可以设计成下拉方式被选择,减少输入时的错误。
3.本例中用的transitions在实际项目中的性能存在疑问,使用transitions只是鉴于好理解状态机的设计模式,在设计 使用中比如用于sip状态机模型对定时器的要求很高,很多定时器要求需要达到200ms或以下,目前的秒级定时器无法满足要求。