基于B/S架构的Tango-Control System的一点思考和尝试

2024/9/12

一、概述

传统的基于C/S架构的tango系统利用CORBA的omniORB以及新版本下的ZMQ来进行通信,效率很高,但是由于c++、java等语言的局限性,造成了客户端的设计并不总能满足我们的需求。伴随着Web的蓬勃发展,以js、html、css三件套为基础的web前端领域生态已经非常完善,各种组件和框架可以帮助我们很便利的开发出很好的客户端产品,同时electron框架以及http协议能够让我们的系统更快部署并且方便使用。

在gitlab上有两个开源的tangogql项目,tangogql本质上是构建一个web服务器,能够响应和处理来自web客户端的请求。对于web客户端的请求,tangogql需要解析该请求并且将相应的操作执行到实际的tangoDB或者说tango server上,并返回结果给web客户端。这些请求包括但不限于:

  • Query:对于device、attributes、commands、property、database等相关内容的查询
  • Mutate:对于device的attributes、property的更改,command的执行等
  • Subscribe:对应CHANGE、PERIODIC事件的订阅监听等

tangogql的gql是指Graphql,是一种用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由自己的数据定义),具体的可参见我写的另一个文档<TangoGraphQL参考手册>。两个tangogql项目的具体优劣将在第二章进行阐述。

在web客户端模块,采用vue框架进行前端界面的设计,采用vue-apollo来与后端的graphql接口进行对接通信,利用electron框架来打包生成可跨平台的app,各种框架技术的优劣以及选择理由将在第三章阐述。

总的来说,可以利用tangogql在tango的基础上暴露出可利用的web访问接口,接口语言定义使用graphql,前端客户端可使用electron + vue + vue-apollo来开发出web客户端,同时还可添加各种第三方库如可视化库Echarts等来满足我们的实际需求。


二、概要设计 以及 两种tangogql的异同

两个tangogql最大的一个区别在于,基于pytango的tangogql实现的webserver是独立于tango系统之外的,而cpp版本的则是将其集成为了一个设备,因此两者的系统架构有些许不同,但大体框架是一致的,接下来我将分别叙述并讲解其中遇到的一些问题。

2.1 CPP版本的tangogql

系统的整体架构图如下所示:

TangoGQL本身作为一个设备处于Tango系统中,其主要分为两个模块,一个是解析query语句的GQL Engine,另一个则是响应并处理http请求的Web Server端。其Web Server的实现较为简陋,只是用普通的socket网络编程搭建起了一个服务。

具体而言,当Web Server收到客户端的请求时,他将提取其http请求中携带的json串交给GQL Engine进行解析,GQL Engine解析完成后将根据其内容进行相应的操作,调用tango系统提供给我们的api对数据库进行相应的查询、更改等操作。

此处我们使用Nginx来进行负载均衡,有着轮询、最少连接数,ip—hash等策略,我们选择最少连接数即可。在TangoGQL原论文中,作者有提到其设想的负载均衡策略,将一个TangoGQL设备作为负载均衡的中间件,所有连接通过该设备并由其分发到其他TangoGQL设备,但是由于其本身Web Server设计的局限性,感觉实际运用可行性不大。Nginx采用epoll的网络连接策略,并且有着许多小细节的优化,在商业领域已经有了很成功的利用。

客户端的设计将放在第三章讲述

但是该TangoGQL与基于pytango的tangogql都有着一个共同的问题:更新较慢,所支持的协议不与新版本的协议兼容。Query和Mutate的实现没有什么问题,但是Subscribe订阅的实现却有问题。Subscription的实现需要借助Web Socket协议来保证客户端和服务端可以进行全双工通信进而才能实现订阅功能。GraphQL官网所提供的graphql-ws则是对该协议的一个包装,客户端和服务端都可以利用该库来进行通信。但是graphql-ws自4.7.0版本开始多了一个extensions字段,其作用是携带给插件的信息,我们其实也不太能用到,但是TangoGQL的解析就会不通过。因此CPP版本的我们需要做以下更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//GQLJson.h
class GQLJson {
...
// JSON fields of a GraphQL query
// add extensions filed here
+ JSON extensions
std::string operationName;
JSON variables;
GQLRequest *request;
...
};


// GQLJson.cpp
void GQLJson::parse(GQLSchema *schema,string &jsonStr) {
...
while(!eof) {
...
if( key=="query" ) {
jsonParser.jumpSep(':');
jsonParser.readWord(jsonQuery);
+ } else if( key=="extensions" ) {
+ jsonParser.jumpSep(':');
+ jsonParser.parseJSON(extensions);
}
...
}

2.2 基于pytango的tangogql

与2.1的主要区别则在于tangogql是独立于Tango系统之外的,其利用aiohttp库实现了一个web服务,利用graphene库来解析graphql语句,其webserver使用的是epoll进行网络连接处理,相对而言更为合理完善,在实际部署中,不使用nginx而只使用该websever应该也能满足我们的业务需求。

但是由于python版本的graphql-ws已经很久不更新了,其版本还停留在了0.4.4。与现今的协议已经有了很大的不同,为了兼容现在的协议,需要做以下更改:(是否有隐藏的bug还未知)

1
2
3
4
5
6
// graphql_ws/constants.py
...
GQL_DATA = "data" # Server -> Client
GQL_NEXT = "next" # Server -> Client for graphql-ws@5.16.0
GQL_ERROR = "error" # Server -> Client
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// graphql_ws/base.py
def process_message(self, connection_context, parsed_message):
...
elif (op_type == GQL_CONNECTION_TERMINATE or op_type == "complete") :
return self.on_connection_terminate(connection_context, op_id)


elif (op_type == GQL_START or op_type == "subscribe") :
assert isintance(payload, dict),
params = self.get_graphql_params(connection_context, payload)
return self.on_start(connection_context, op_id, params)
...

def get_graphql_params(self, connection_context, payload):
...
return {
...
"extensions": payload.get("extensions"),
...
}


def send_execution_result(self, connection_context, op_id, execution_result):
result = self.execution_result_to_dict(execution_result)
if type(op_id) == str:
return self.send_message(connection_context, op_id, GQL_NEXT, result)
else:
return self.send_message(connection_context, op_id, GQL_DATA, result)

除此之外,我在测试时发现tangogql的mutate实现存在一定的问题,其有一个value_before的字段,即在更改设备属性时获取更改前的value值,但是在实际测试时却与预期相违背,查看源码是collaborative_read_attribute这个函数实现的有问题,目前还没想到怎么解决这个bug,只能先将value_before这个字段暂时弃用,并不影响我们的正常使用

在配置时需注意python版本使用3.9。 3.10版本会有一些问题。python的版本切换可使用下面这个命令

1
sudo update-alternatives

2.3 小结

graphql整体生态其实是很完善的,在前后端各个环节都有着比较完善的库可以使用,但是都主要集中在js领域,tango下的几个tangogql对于新版本的支持都比较差,有待进一步优化甚至重构

两个tangogql的异同如下:

  • cpp版本作为tango设备集成在tango系统中,更方便部署和管理
  • cpp版本长时间没有人更新维护,pytango版本有维护但是其所使用的依赖库也都比较老旧(写完文档发现pytango版本有一个重构版正在开发中,我将放在第四章阐述下)
  • pytango版本的具有比较好的日志系统,方便维护和查找bug
  • 事件订阅均只支持CHANGE、PERIODIC两种,对于tango系统中archive等event并不支持
  • 定义的schema并不相同,但是对于开发所需接口都比较完善
  • 都有用户认证功能,但是cpp版本的用户信息是写死在TangoGQL设备的property里的,pytango采用jwt来实现,但具体的用户登录逻辑似乎还要自己实现

三、web客户端技术初探

出于测试tangogql是否能与现今流行的web框架一起使用的考量,我的demo程序提供的功能主要是:用户输入Query、mutate、subscribe的语句,然后再将执行结果返回给用户。但在实际开发中,是不应当这么做的,我们不应该赋予用户这样的权利,应当将我们所需要用到的Query、mutate、subscribe操作语句写死,或者暴露少量的可更改variable的接口,将其一个个功能绑定到相应按钮上,否则可能会存在安全隐患。

首先是关于electron是否必须的问题,个人感觉不是的,现今所有的操作系统都提供有浏览器,我们只需部署网页版即可。但是如果有开发桌面应用的需求,electron则是必不可少的。electron实际上可理解为Chromium(浏览器引擎) 和 Node.js(js运行时)两部分的结合,他提供给我们的功能主要就是构建桌面应用界面、渲染web界面、与操作系统的交互,web界面的实际设计并不是他的考虑范围,其更多的作用比如说建立托盘图标、添加全局的快捷方式、不同窗口进程间的通信,提供上下文隔离保证安全等。对于electron的深度使用和开发的难度还是比较高的。

构建web界面,现今流行的框架则有两种,vue和react。其实在使用graphql的情况下,使用react应该更为合适,因为其都是出于facebook公司,适配度也做得很好。但是出于各方面考虑,demo程序我使用了vue。

在web客户端,不使用其他的库也是可以的,直接使用原生的js的api进行网络请求也可以达到我们的目的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function fenchPost() {
let headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Accept-Encoding': 'gzip,deflate,br,zstd',
'Content-Length': input.value.length,
'Sec-Fetch-Site': 'same-site',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
};

fetch("http://localhost:80/graphql", {
method: 'POST',
headers: headers,
mode: "cors",
keepalive: true,
body: JSON.stringify({ query: input.value })
})
.then(response => {
if (!response.ok) {
throw new Error("fetchGQL(): " + response.status + " " + response.statusText);
}
return response.json();
})
.then(data => {
output.value = data;
})
.catch(error => {
console.error(error);
});
}

但是可以看到,这样极其繁琐,我们的开发效率以及代码可维护性都会很差。因此我们使用三个库:vue-apollo、graphql-js、graphql-ws

graphql-js、graphql-ws是graphql官网提供给我们的库,graphql-js是GraphQL 规范的参考实现,是我们必不可少的库,graphql-ws则是对websocket协议的封装,以实现subcribe功能。vue-apollo是vue官方提供给我们的工具,其利用上述两个库设计并暴露了一系列方便利用并且很容易与vue进行交互的api接口,同时提供了比如错误管理、分页支持、预读数据、数据缓存等特性功能,方便我们开发使用。如果是在react进行开发,vue-apollo对应的工具则是apollo-client

上述的三个库本身也是比较轻量级的,并不会太影响我们程序的体量

3 小结

web前端领域对于graphql的支持是很好的,web客户端开发方面的技术也很多,但是出于稳定性和可持续性的考虑,所有库和框架均选自官方库,第三方库存在很多跑路停止维护的情况。但个人感觉这方面应该不是重点,更多应该放在tangogql的选择、开发和维护上。


四、tangogql-ariadne

在gitlab的incubator目录下找到了该项目,是tangogql的原作者重写的一个项目,其webserver采用现今比较流行的uvicorn,gql采用ariadne进行解析,测试了一下发现与新版本拟合的很好,之前在前两个项目中不支持的extensions字段在该项目中也得到了支持。

但是目前该项目还处于开发状态,很多功能都没有实现,原作者也只设置了本地测试版本,我在加上跨源策略后用我的demo程序测试了一下基本功能,没有什么问题,但是一些功能没实现完,比如subscribe的polling周期强制写死为了3秒等待,源码中也还有很多的TODO待完成,是否还有隐藏的bug未知。