0%

2022-03-11-带着问题看源码5-NodeRed系统数据存储

NodeRed 系统相关的配置文件会以 JONS 格式存储在本地,同时也提供了插件机制实现系统数据的自定义存储,本文从 Storage 模块出发,从源码分析下如何以插件形式实现自定义存储,也对这种方式的可能的用途进行分析。

1. NodeRed 系统数据有哪些#

文件名称 说明
package.json nodered npm 信息
.sessions.json 维护会话信息,每一个建立的会话都会记录
flows_[hostname].json NodeRed 流程文件
flows_[hostname]_cred.json 用以保存节点中以.credentials 保存的信息,加密与否可使用配置文件中 credentialSecret 参数配置
.config.runtime.json 运行时配置
.config.users.json 用户配置
.config.nodes.json 节点配置
.config.projects.json Git 配置

2. NodeRed 中系统数据存储机制#

NodeRed 提供 storage 模块支持上述文件的读写。

2.1. 源码分析#

以接口形式封装模块,模块的使用者只依赖接口,不依赖接口的实现,符合面向对象的依赖倒置原则(依赖倒置原则可以减少类间的耦合性,提高系统的稳定,降低并行开发引起的风险,提高代码的可读性和可维护性。)

  1. 以接口形式封装,具体的实现由动态加载的插件实现
1
2
3
4
5
6
7
8
9
10
11
12
getFlows: async function() { //====>此处为接口
return storageModule.getFlows().then(function(flows) { //====>此处getFlows为具体实现
return storageModule.getCredentials().then(function(creds) {
var result = {
flows: flows,
credentials: creds
};
result.rev = crypto.createHash('md5').update(JSON.stringify(result.flows)).digest("hex");
return result;
})
});
},
  1. 在初始化时按配置文件选择要使用的存储模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
init: async function(_runtime) {
runtime = _runtime;
// Any errors thrown by the module will get passed up to the called
// as a rejected promise
storageModule = bmoduleSelector(runtime.settings); // ====>从配置项中加载模块
settingsAvailable = storageModule.hasOwnProperty("getSettings") && storageModule.hasOwnProperty("saveSettings");
sessionsAvailable = storageModule.hasOwnProperty("getSessions") && storageModule.hasOwnProperty("saveSessions");
if (!!storageModule.projects) {
var projectsEnabled = false;
if (runtime.settings.hasOwnProperty("editorTheme") && runtime.settings.editorTheme.hasOwnProperty("projects")) {
projectsEnabled = runtime.settings.editorTheme.projects.enabled === true;
}
if (projectsEnabled) {
storageModuleInterface.projects = storageModule.projects;
}
}
if (storageModule.sshkeys) {
storageModuleInterface.sshkeys = storageModule.sshkeys;
}
return storageModule.init(runtime.settings,runtime);
},
  1. 在可能有越权行为的接口处增加检验,保证文件访问不会越权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
function is_malicious(path) {
return path.indexOf('../') != -1 || path.indexOf('..\\') != -1;
}

getLibraryEntry: async function(type, path) {
if (is_malicious(path)) {
var err = new Error();
err.code = "forbidden";
throw err;
}
return storageModule.getLibraryEntry(type, path);
},
...
  1. 在有可能同时写文件(共同资源)时,做互斥操作
1
2
3
4
5
6
7
saveSettings: async function(settings) {
if (settingsAvailable) {
return settingsSaveMutex.runExclusive(() => storageModule.saveSettings(settings))
}
},


2.2. API#

storage API 使用插件式配置 Node-RED 运行时存储数据

Function Description
Storage.init(settings) initialise the storage system
Storage.getFlows() get the flow configuration
Storage.saveFlows(flows) save the flow configuration
Storage.getCredentials() get the flow credentials
Storage.saveCredentials(credentials) save the flow credentials
Storage.getSettings() get the user settings
Storage.saveSettings(settings) save the user settings
Storage.getSessions() get the user sessions
Storage.saveSessions(sessions) save the user sessions
Storage.getLibraryEntry(type,name) get a type-specific library entry
Storage.saveLibraryEntry(type,name,meta,body) save a type-specific library entry

3. NodeRed 中系统数据存储机制有哪些参与者#

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4. NodeRed 为什么这么设计,这种设计的优劣有哪些#

以接口形式封装模块,模块的使用者只依赖接口,不依赖接口的实现,符合面向对象的依赖倒置原则(依赖倒置原则可以减少类间的耦合性,提高系统的稳定,降低并行开发引起的风险,提高代码的可读性和可维护性。)

6. 相关知识#

  1. async-mutex:异步互斥
    支持使用互斥量和信号量两种方式
  • mutex(互斥体)

    术语“互斥体”通常是指一种用于同步不同线程上运行的并发进程的数据结构。例如,在访问非线程安全资源之前,线程会锁定互斥体。这保证阻塞线程,直到没有其他线程持有互斥锁,从而强制对资源的独占访问。一旦操作完成,线程释放锁,允许其他线程获取锁并访问资源。

    虽然 Javascript 是严格的单线程,但其执行模型的异步特性允许需要类似同步原语的竞争条件。例如,考虑一个与 web worker 通信的库,为了完成一项任务,它需要与 worker 交换几个后续消息。因为这些消息是以异步方式交换的,所以在这个过程中很有可能再次调用这个库。根据异步过程中处理状态的方式,这将导致难以修复甚至更难跟踪的竞争情况。

    这个库通过将互斥的概念应用于 Javascript 解决了这个问题。锁定互斥体将返回一个承诺,一旦互斥体变得可用,该承诺就会解决。一旦异步进程完成(通常需要事件循环的多次旋转),调用提供给调用者的回调以释放互斥体,允许下一个调度的工作线程执行。

  • semaphore(信号量)
    想象一下,您需要控制对共享资源的几个实例的访问。例如,您可能希望在执行转换的几个工作进程之间分发图像,或者您可能希望创建一个 web crawler 来并行执行定义数量的请求。

    信号量是一种数据结构,它被初始化为一个正整数值,并且可以被多次锁定。只要信号量值为正,锁定它将返回当前值,锁定进程将立即继续执行;信号量将在锁定时递减。释放锁将再次增加信号量。

    一旦信号量达到零,下一个试图获取锁的进程将被挂起,直到另一个进程释放它的锁,这将再次增加信号量。

    这个库为 Javascript 提供了一个信号量实现,类似于上面描述的互斥体实现。

  1. 面向对象 6 大原则:依赖倒置
    高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
    依赖倒置原则是指模块间的依赖是通过抽象来发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口是来实现的,这就是俗称的面向接口编程。

7. 应用场景分析#

  1. 换一种存储格式(YAML)
    有些库已经实现此功能 node-red-contrib-yaml-storage
    Node-RED 将流文件存储为 JSON 文档。虽然 JSON 是轻量级和通用的,但它不是最容易阅读的格式。Node-RED 在函数、注释和模板节点中存储各种形式的代码,如 JavaScript、HTML、CSS、Markdown 等。这些代码块被表示为 JSON 结构中的一行,这使得在读取流文件时很难进行调试,并且会产生差异
  2. 按指定的接口将文件存储在服务端
    有些库已经实现(@flowforge/nr-storage)
  3. 做数据加密
    可以将数据加密后存储到本地或远端,防止拷贝数据
  4. 远端存储并做授权认证
    通过将数据存放在远端,可对用户访问进行授权,限定用户可操作的时间。