在本章中,你将学习如何运行和调试自己的留言服务以及掌握背后的原理。
CYFS 协议是对 HTTP 协议的整体升级,会尽量保持 HTTP 的近似语义。核心流程是:
CYFS DEC App <--cyfs@http--> cyfs-runtime <--cyfs@bdt--> gateway <--cyfs@http--> CYFS DEC Service
实际在网络中运行的 cyfs@bdt 协议并不会被 DEC App 的客户端和服务器直接使用,在开发者看来不管是前端还是后端,都只是处理有特殊 Header 的 HTTP 协议。这个设计让 cyfs@bdt 协议的实现细节对应用开发者透明,BDT 能有机会进行持续迭代(BDT 还是一个年轻的协议),同时也能降低开发者的学习和使用门槛。
cyfs@http 协议会被 DEC App 开发者直接使用,因此其设计是简洁易懂且相对稳定的。
GET 协议在数据流转的流程上主要分为 3 步:
- Step1: CYFS DEC App(浏览器) <-> cyfs-runtime@local (cyfs@http)
这是最常见的请求,所以其接口逻辑为向 local cyfs-runtime 平凡的发起一个 HTTP GET 请求。
按这个设计,当 cyfs-runtime 绑定本地的 80 端口时,如用户在 HOST 中把 o 配置为 127.0.0.1(或 cyfs-runtime 绑定的本地 virtual IP),那么可以在传统浏览器中直接用
http://o/$ownerid/$objectid
打开。
- Step2:cyfs-runtime <-> gateway (cyfs@BDT or BDT)
BDT 协议目前对应用透明,所以我们保留了根据应用实践改进性能的机会。比如可以为 NamedObject GET 定制专门的 BDT 协议报文。使用 HTTP@BDT Stream 是目前最稳定的实现。
按上述设计,这一层建立好正确的 BDT Stream 后,只需原样转发 HTTP 请求即可。因为 BDT 自带身份,所以 Req 中的 cyfs-from 和 Resp 中的 cyfs-remote 字段以删除以减少流量占用。
- Step3:gateway <-> DEC Service (cyfs@http)
正常情况下,DEC Service 不应该 HANDLE NamedObject 的 GET 请求。gateway 的默认行为会自动的进行 NamedObject 查找,并返回结果. 默认行为下,对 GET NamedObject 的权限控制思路为
- Zone 内请求全放行,如果 Zone 内没有 OOD 会尝试去从其它地方获取。(
请求中的ownerid不一定要等于 OOD's Owner
)。 - Zone 外请求,如果请求的 Object 在 OOD 上没有,则直接返回 404。如果有,则判断该 Object 的 Owner,如果 Owner 不是 OOD's Owner,则返回。如果是,满足下面条件的请求放行:来源于“好友 Zone”;NamedObject 为 Public;有效的 ContextId(详见 Context 管理)
DEC Service 可以按 SDK 里 gateway 部分的接口,设置 GET NamedObject Handler。设置后的基本流程和 nginx upstream 类似,流程如下:
gateway-req->data_firewall->DEC Service->data_firewall-resp->gateway
gateway 在把请求转发给 DEC Service 之前,以及 DEC Service 完成处理产生 Response 之后,都会经过数据防火墙的处理。
- 我们现在还未开放 GET NamedObject Handler.
PUT 协议在数据流转的流程上主要分为 3 步:
- Step1: CYFS DEC App(浏览器) <-> cyfs-runtime@local (cyfs@http) Reqeust
PUT http://o/$ownerid/$objectid HTTP/1.1
[cyfs-from:$deviceid] // 如果填写,说明App希望用指定身份发起请求
[cyfs-target:$deviceid] // 如果填写,说明要到达的具体设备
[cyfs-decid:$decid] //发起请求的decid。
[cyfs-cache-time:$time] //希望缓存的时间
(Body) 为NamedObject的二进制编码
Response
HTTP/1.1 200 OK //该NamedObject已经被缓存
[cyfs-remote:$remote-device-id]
[cyfs-cache-time:$time] //决定缓存的时间
- Step2 cyfs-runtime <-> gateway or cyfs-runtime (cyfs@BDT) 建立好正确的 BDT Stream 后,原样转发 HTTP 请求到目标设备。因为 BDT 自带身份,所以 Req 中的 cyfs-from 和 Resp 中的 cyfs-remote 字段以删除以减少流量占用。
- Step3:gateway or cyfs-runtime <-> DEC Service / Named Object Cache (cyfs@http) cyfs-runtime 里通常不允许 DEC App Set Handler.这里我们讨论 Gateway 的情况。 gateway 的一般 HANDLE 逻辑如下:
- 来自 Zone 内的 PUT 默认接受
- 来自 Zone 外的 PUT 默认拒绝
POST(CALL) 协议在数据流转的流程上主要分为 3 步:
- Step1: CYFS DEC App(浏览器) <-> cyfs-runtime@local (cyfs@http) Request
POST http://r/$ownerid/$decid/$dec_name?d1=objid1&d2=objid2 HTTP/1.1
[cyfs-from:$deviceid] // 如果填写,说明App希望用指定身份发起请求
[cyfs-decid:$decid] //发起请求的decid。
[cyfs-dec-action:exeucte | verify]
POST Call 的 Body 中可以带一组 package 的 named object。但大部分情况下,由 DEC Service 自行 Prepare。
Response
HTTP/1.1 200 OK
cyfs-dec-state: complete | prepare | running | wait_verify| failed //本次dec是完成,准备中,正在工作,等待验证,失败
cyfs-dec-finish : $time //dec完成的时间(dec不会重复执行,如果之前已经完成过,会用之前的时间)
cyfs-prepare : objid1,objid2,objid3 ... // 如果处在准备状态的
Response Body:如果 action 是执行,则返回 Result ObjectIds.如果是验证,验证通过返回对 DEC 三元组的签名。
-
Step2 cyfs-runtime <-> gateway (cyfs@BDT)
-
Step3:gateway <-> DEC Service (cyfs@http)
整个流程基本是 HTTP POST Request 和 Response 的原样转发,也是 DEC Service 会主要 Handle 的请求
AppManager 是 CYFS 协议中的基础服务之一。主要用来安装和运行 DEC App。 AppManager 通过 docker 容器的方式运行 DEC App 的进程,能够更安全的运行 DEC App,让协议栈能够鉴权 DEC App 的请求,以及检查其携带的 dec id 是否是伪造的。
在运行 Service 之前,我们先来深入了解一下 Service 启动程序。
- 完整代码见 src/service/entry/app_startup.ts
Service 启动程序的入口为 main 函数,主要完成 3 个步骤:
- 开启 Service 日志。
- 打开并等待 Stack 上线。
- 在 Stack 上注册路由模块。
日志可以让我们快速的发现并定位问题,对解决线上问题十分有帮助。不同的操作系统,应用日志的存储路径略有不同;
- mac: ~/Library/cyfs/log/app/<dec_id>
- windows: C:\cyfs\log\app<dec_id>
- linux: /cyfs/log/app/<dec_id>
基于 CYFS SDK 开启 Service 日志非常简单,代码如下:
import * as cyfs from "cyfs-sdk";
cyfs.clog.enable_file_log({
name: APP_NAME,
dir: cyfs.get_app_log_dir(APP_NAME),
});
通过引入 cyfs_helper 中的 waitStackOOD 方法,我们可以很方便实现打开并等待 Stack 上线,代码如下:
import { waitStackOOD } from "src/common/cyfs_helper/stack_wraper";
const waitR = await waitStackOOD(DEC_ID);
if (waitR.err) {
console.error(`service start failed when wait stack online, err: ${waitR}.`);
return;
}
使用 addRouters 方法来批量注册路由,提高开发效率。addRouters 方法中通过遍历封装了全部路由模块的 routers
对象,完成批量注册路由的功能。在每一轮循环中,主要完成 2 个任务:
- 为请求路径 req_path 动态设置 access 权限,这里我们为每一个请求路径都设置
只对 OwnerDec 开放全部的权限(Read/Write/Call)
。 - 使用 add_post_object_handler 方法将路由模块挂载到 Stack 上的指定请求路径下。
addRouters 代码如下:
import * as cyfs from "cyfs-sdk";
export type RouterArray = Array<{
reqPath: string;
router: postRouterHandle;
}>;
async function addRouters(
stack: cyfs.SharedCyfsStack,
routers: RouterArray
): Promise<void> {
for (const routerObj of routers) {
// 为 req_path 设置 access 权限
const access = new cyfs.AccessString(0);
access.set_group_permissions(
cyfs.AccessGroup.OwnerDec,
cyfs.AccessPermissions.Full
);
const ra = await stack
.root_state_meta_stub()
.add_access(
cyfs.GlobalStatePathAccessItem.new(routerObj.reqPath, access)
);
if (ra.err) {
console.error(`path (${routerObj.reqPath}) add access error: ${ra}`);
continue;
}
console.log("add access successed: ", ra.unwrap());
// 挂载路由模块到指定的 req_path
const handleId = `post-${routerObj.reqPath}`;
const r = await stack
.router_handlers()
.add_post_object_handler(
cyfs.RouterHandlerChain.Handler,
handleId,
1,
undefined,
routerObj.reqPath,
cyfs.RouterHandlerAction.Pass,
new PostRouterReqPathRouterHandler(routerObj)
);
if (r.err) {
console.error(`add post handler (${handleId}) failed, err: ${r}`);
} else {
console.info(`add post handler (${handleId}) success.`);
}
}
}
我们推荐采用前后端分离的方式去开发 DEC App。 到目前为止,留言板的 DEC Service 已经开发完毕。 接下来,我们把 DEC Service 发布到 OOD 并对 DEC Service 的各项功能进行独立调试。
在项目根目录下,打开终端,运行如下指令:
npx tsc
指令执行完,可以在项目根目录下看到新增了 deploy 文件夹。
- deploy: 发布到 OOD 的文件夹,包含了项目中所有 ts 文件 编译后的 js 文件
接着,把 src/common/objs/obj_proto_pb.js 文件拷贝到 deploy/src/common/objs/obj_proto_pb.js
打包 App 是个本地过程,将 DEC App 的 Service 部分,Web 部分,和各种配置文件,拷贝到 dist 文件夹,并以特定格式组织,dist 文件夹的位置由工程配置文件的 dist 字段指定
可以单独执行命令 cyfs pack,手工进行一次打包流程,检查打包过程,和打包后的文件夹是否有错误 打包后的 dist 文件夹类似下面的组织方式:
├─acl
│ └───acl.cfg
│
├─dependent
│ └───dependent.cfg
│
├─service
│ ├───x86_64-pc-windows-msvc.zip
│ └───x86_64-unknown-linux-gnu.zip
│
└─web
acl: 存放 service 的 acl 配置文件,打包过程会将你在 service.app_acl_config.default 字段指定的文件,拷贝到该文件夹下,并重命名为 acl.cfg dependent: 设计用来存放 service 的 CYFS 协议栈依赖配置。当前该功能无效 service:存放 service 的二进制文件。按照 service.dist_targets 的配置,分别给每个平台打包{target}.zip 文件,当用 ts 开发 service 时,zip 文件的内容是 service.pack 中指定的文件夹,加上对应平台的 app_config 文件 web: 存放 app 的 web 端内容,打包过程中,会将 web.folder 文件夹下的内容拷贝至此 如果 service.pack 为空,则不会产生 acl, dependent, service 文件夹;如果 web.folder 为空,则不会产生 web 文件夹
如果一个 app 的 service.pack 和 web.folder 都为空,则 deploy 命令无效。不会发布一个空 app
目前这里使用 CYFS 浏览器里的 cyfs-client 工具,将 dist 文件夹上传到 owner 的 OOD。由于一些历史及稳定性的原因,暂且没有使用 CYFS 协议栈的标准上传方法。这里的上传,和使用 cyfs upload 命令的上传是不同的。
DEC App 上传完成后,将这个版本的信息添加到本地的 DEC App 对象,然后将对象上链。
信息发布成功后,按照以下规则生成链接:cyfs://{owner_id}/{dec_id}。由于 DEC App 对象中已经包含了所有的版本信息,因此,你会注意到,每次发布 DEC App 时,这个链接都是不变的
我们先打开 CYFS 浏览器,然后,在项目根目录下,打开终端,运行如下指令:
- mac
npm run mac-deploy-pre
npm run deploy
如果过程中出现以下错误:
[error],[2022-09-14 19:39:09.175],<>,owner mismatch, exit deploy., cyfs.js:389
这个报错代表当前的 owner 与应用的 owner 不匹配,我们需要手动修改应用的 owner,在项目根目录打开终端,输入以下指令:
cyfs modify -o ~/.cyfs_profile/people
yes
执行命令,打印出 save app object success 的字样,代表修改成功。
接下来,我们打开项目根目录下的 cyfs.config.json
文件,会发现 app_id
已经改变。因此,我们需要把 src/common/constant.ts
中的 DEC_ID_BASE58
修改为最新的 app_id
。
修改好了之后,我们重新走一遍 编译和打包项目
的流程即可。
- windows
npm run deploy
最终,终端会显示上传的信息,上传完成后,终端显示如下信息:
Upload DEC App Finished.
CYFS App Install Link: cyfs://5r4MYfFbqqyqoA4RipKdGEKQ6ZSX3JzNRaEpMPKiKWAQ/9tGpLNnbNtojWgQ3GmU2Y7byFm7uHDr1AH2FJBoGt5YF
恭喜你,这代表我们的 DEC Service 已经成功发布到了 OOD。
请把CYFS App Install Link
对应的链接复制下来,下一节我们将会使用这个链接进行 DEC Service 的安装。
在上一节发布服务到 OOD
中,我们已经成功把 DEC Service 发布到了 OOD 上。现在,我们来安装 DEC Service。
安装 DEC Service 之前,我们有必要了解一下 AppManager 安装 DEC App 的原理。
用户将 DEC App 的指定版本安装到 ood 上时,ood 会做以下 4 件事:
-
根据 ood 的 target,查找对应 dir 是否有 service/<target>.zip 文件,这里使用 zip 是为了减少发布和下载大小。如果有,将文件解压到{cyfs_root}/app/<app_id>文件夹;
-
查找对应 dir 是否有 web 文件夹,如有,将 web 文件夹下载到{cyfs_root}/app/web/<app_id>文件夹,然后将该文件夹添加到 cyfs 栈,得到新的 web dir id;
-
如果该 DEC App 有 service,就会执行 service install 脚本(对应项目根目录下的 service_package.cfg 文件中的 install 配置);
-
如果该 DEC App 有 service,就会执行 service start 脚本(对应项目根目录下的 service_package.cfg 文件中的 start 配置)。
- 复制
CYFS App Install Link
后面的这串 CYFS 链接,去 CYFS 浏览器中打开DEC App Store 页面
(cyfs://static/DEC AppStore/app_store_list.html),点击通过 URL 安装
按钮,把安装链接粘贴进去后点击获取应用信息
绿色按钮。 - 在页面中的
版本列表
区域,可以看到 DEC App 的历史版本,我们选择最新的版本,点击安装
即可。 - 返回
DEC App Store 页面
(cyfs://static/DEC AppStore/app_store_list.html),点击页面顶部的已安装
绿色按钮,可以看到已经安装好的 DEC Service。如果显示安装中
,请耐心的等待一会儿。
通过学习前面 AppManager 的安装原理和流程,我们可以发现 AppManager 不是一个中心化的节点,而是一个分布有众多节点的分布式系统。 AppManager 通过 target 找到目标 OOD 节点,OOD 节点之间彼此可以自由连接。当我们安装 DEC App 的时候,任何一个节点都可能成为阶段性的中心,但不具备强制性的中心控制功能。OOD 节点与 OOD 节点之间的影响,会通过网络而形成非线性因果关系。 AppManager 体现出了开放式、扁平化、平等性的系统结构,我们称 AppManager 是去中心化的 AppManager。
到这里,我们的 DEC Service 已经在 OOD 上运行起来了。现在,我们来对留言的增删改查功能进行调试。
测试程序的主要功能是启动一个 Client 与服务进行交互,以便测试各个接口的功能。 从原理上看,就是启动一个 runtime-stack,利用 runtime-stack 向 DEC Service 发起请求。
在项目根目录下,打开终端,执行如下指令:
npx tsc
执行完之后,所有的测试脚本文件都在 deploy/src/service/test 文件夹下。
- 完整源码见 src/service/test/publish_message_test.ts
调试发布留言模块的主入口是 main 函数,需要完成以下 3 个步骤:
- 初始化 runtime-stack
- 设置新的留言 key 值和 content 文本内容
- 发起请求
在项目根目录下,打开终端,执行如下指令:
node ./deploy/src/service/test/publish_message_test.js
如果接口正常,Client 控制台会打印publish message msgKey is ${msgKey}, result: ${r}
,其中的 msgKey 就是新建留言对象的 key 值。
否则,打印publish message failed.
建议复制 Client 控制台打印的新留言对象的 key 值,用来供接下来的查询、修改和删除的功能调试使用。
cyfs shell 是快速查看和验证 RootState 数据状态的工具,非常易用。
前面,我们发布了一条新留言,对应在应用的 messages_list
路径下就新增了一个留言对象。我们使用 cyfs shell 来验证一下。
打开终端,输入以下指令来启动 cyfs shell:
cyfs shell
执行完之后,会出现 cyfs shell 命令行终端。我们按一下步骤来查看新建的留言对象:
- 使用键盘的
上下键
来选择查看的OOD
还是Device
的 RootState,因为我们的 Service 在 OOD 上,所以选择OOD
(第 1 个)并按回车; - 输入
ls
并回车,查看 RootState 根路径下的所有子节点,在里面可以看到message-board
对应的dec id
,复制dec id
到终端并回车; - 输入
cd <dec id>
并回车,进入留言板message-board
的应用根路径; - 输入
ls
并回车,查看留言板的 RootState 根路径下的所有子节点,会看到.cyfs
和messages_list
,这个messages_list
就是存放所有留言对象的地方; - 输入
cd messages_list
并回车,进入messages_list
路径; - 输入
ls
并回车,可以看到messages_list
路径下的全部留言对象,左边
是留言对象的 id,右边
是留言对象的 key 值。
经过前面 cyfs shell 的实操,相信你现在对 RootState 已经有了一个更加具象的认知。
使用 cyfs shell,让 RootState 上的数据是可以被 看见
!这真的很酷!
现在,我们已经成功的与 OOD 上的 Service 进行了发布留言功能的联调。 如果 OOD 上的 Service 内部出现错误,我们该怎么去定位问题呢? 其实,在 OOD 上,我们可以很方便的查看 Service 端的日志,以下是 OOD 不同操作系统环境中 Service 日志的存放路径。
- mac: ~/Library/cyfs/log/app/<dec_id>
- windows: C:\cyfs\log\app<dec_id>
- linux: /cyfs/log/app/<dec_id>
- 完整源码见 src/service/test/retrieve_message_test.ts
调试查询留言模块的主入口是 main 函数,需要完成以下 3 个步骤:
- 初始化 runtime-stack
- 设置要查询的留言对象 key 值
- 发起请求
打开 deploy/src/service/test/retrieve_message_test.js 文件,把上一步 Client控制台
打印出来的 msgKey
字符串赋值给 main
函数的 msgKey
常量。
修改好之后,在项目根目录下,打开终端,执行如下指令:
node ./deploy/src/service/test/retrieve_message_test.js
如果接口正常,Client 控制台会打印retrieve message result: current Message key is ${msgRawObj.key}, content is ${msgRawObj.content}
,其中包含留言对象的 key 值和 content 内容。
否则,打印retrieve message failed.
- 完整源码见 src/service/test/update_message_test.ts
调试修改留言模块的主入口是 main 函数,需要完成以下 3 个步骤:
- 初始化 runtime-stack
- 设置将要修改的留言对象 key 值和新的 content 值
- 发起请求
打开 deploy/src/service/test/update_message_test.js 文件,把在 调试发布留言功能
中 Client控制台
打印出来的 msgKey
字符串赋值给 main
函数的 msgKey
常量。此外,你也可以手动设置content
常量的为你喜欢的任何字符串,目的是更改原有的留言内容。
修改好之后,在项目根目录下,打开终端,执行如下指令:
node ./deploy/src/service/test/update_message_test.js
如果接口正常,Client 控制台会打印 update message result: ${r}
。
否则,打印 update message failed.
- 完整源码见 src/service/test/deletee_message_test.ts
调试删除留言模块的主入口是 main 函数,需要完成以下 3 个步骤:
- 初始化 runtime-stack
- 设置将要删除的留言对象 key 值
- 发起请求
打开 deploy/src/service/test/delete_message_test.js 文件,把在 调试发布留言功能
中 Client控制台
打印出来的 msgKey
字符串赋值给 main
函数的 msgKey
常量。
修改好之后,在项目根目录下,打开终端,执行如下指令:
node ./deploy/src/service/test/delete_message_test.js
如果接口正常,Client 控制台会打印delete message result: ${r}
。
否则,打印delete message failed.
使用 CYFS-SHELL 可以快速的感知到 root_state 上的数据状态改变。
- 输入 cyfs shell [ -e runtime/ood],进入交互式命令行, 选择 device_id 和 dec_id 后进入对应的 Root-State 根。
- 使用以下指令
-
ls: 列出该目录下所有子节点
-
cd: 进入该子节点,如果子节点不是 ObjectMap,提示错误,并留在当前目录
-
cat: 以 json 格式展示该子节点的对象内容
-
dump: 以二进制格式保存该子节点的对象内容,保存路径默认为当前路径,保存文件名为.obj
-
get: 保存该节点和后续节点的文件到本地,保存路径默认为当前路径+节点名
-
rm:删除节点,如果节点是 object map, 且还有子节点,删除失败
-
target: 重新选择 target,选择后路径重置为根目录
-
clear: 清除屏幕
-
help: 帮助信息
-
exit: 退出 shell
这是一个很酷的功能,我们正在加紧研发中,敬请期待吧!
到这里,我们的留言服务调试完毕,这个服务最大的特点就是只能为自己服务,也就是说,你只能看到自己的留言板内容。如果你想看到好友的留言板,那暂时是爱莫能助了。不过,不用气馁,当你完整的学习完这个系列教程后,你可以很容易的做到这点!