跨语言互操作指南
如果提供商 SDK 的语言不是 JavaScript 或者通用的 HTTP 等应用层协议,情况会变得比较复杂。
如果您的场景并不如此复杂,请跳过本指南。
为方便说明,假设该 SDK 的语言为 X。
在开始讨论之前,先进行一些基本假设:
- 开发者(也就是您) 对 X 语言有基础的但不深入的了解,希望最小化 X 语言部分的开发复杂度。
- Yuan 已经提供了一系列 JS 库,可以帮助您快速对接 X SDK,并完成一系列杂活。
于是我们可以得到一些推论:
- 不考虑使用 X 语言直接与主机网络通讯的方案。
- 不考虑将 X SDK 打包成 WASM 进而在 JS Runtime 中运行的方案。
- 不考虑使用 JS 端通过内存绑定的方式直接跨语言调用 X SDK。
作这些假设是为了最小化对接的复杂度,提升对接效率和质量,同时也不会有过多的 overhead。
当然,您也可以自行修改对接方案,只需要遵循 Yuan 的技术协议,即可像其他供应商一样接入 Yuan。
架构
基于这些假设,我们可以得到由两个进程组成的简单架构:“JS 端”与“X 端”。
- JS 端较重,负责与主机通讯,异步流程调度,调用 X 端,以及各种杂活。
- X 端较轻,负责调用 SDK,执行业务逻辑。
- 两个进程运行在一个单一的容器中。
REPL 风格
建议将 X 端封装成 REPL (Read-Eval-Print Loop) 风格,这样可以大幅度简化 X 端的复杂度。 所有的编程语言,所有的计算语句均可归类于:调用 (CALL)、返回 (RETURN)、抛出异常 (THROW) 或者回调 (CALLBACK)。
- X 端不需要支持循环和分支跳转结构,也不需要保存历史记录。
使用 JSON 为输入输出编码:
interface IMessage {
trace_id: string;
type: 'call' | 'return' | 'throw' | 'callback';
method?: string; // for call
params?: any[]; // for call and callback
value?: any; // for return
message?: string; // for throw
}
- 结构体可能嵌套,建议使用 JSON 来序列化。
或者使用更简单的编码:
- 每个消息独占一行,消息内部如果有换行符 \n 必须进行转义为 \n
- 以消息 ID 作为开头,后用空格 隔开;然后接一个消息的类型,是 CALL,RETURN, THROW, CALLBACK 的其中之一;后续的部分都是自由协定的,可以是任意的结构。
> ID CALL METHOD PARAM1 ... PARAM_N
< ID RETURN VALUE
< ID THROW ERROR
< ID CALLBACK PARAM1 ... PARAM_N
- X 端可能没有合适的 JSON 库;如果不涉及结构体的传输,绑定 JSON 不一定是一个好选择。
- PARAM,VALUE,ERROR 都可以内部编码为 JSON,并不冲突。
- 任何语言都可以简单解释执行这种简单编码。
- X 端可能处理异步或者多线程的成本可能较高。
- X 端可能会更快遭遇来自 SDK 的性能瓶颈、限流等。
设计上需要 JS 端在 X 端未能及时响应时,主动重启 X 端的进程,原因是:
- X 端可能由于某种已知或未知的原因,无法响应 JS 端。
- 人类在使用 REPL 时,如果程序迟迟不响应,会主动 Ctrl-C 退出后再重试。当然 JS 端也是这么想的。
- X 端可能会提供实时订阅接口或者异步接口,因此需要主动推送给 JS 端。
基础设施
为了满足特定业务逻辑的一个系统中的逻辑复杂度无法被彻底消灭,只能通过逻辑重用的方式被吸收。 Yuan 维护的 Library 会吸收 JS 端中的部分逻辑,从而降低 JS 端的逻辑复杂度,最终使得整个对接流程变得轻松。
通讯中间件 ZMQ
JS 端和 X 端使用 ZeroMQ 作为消息中间件来进行通讯。
- X 端可能不提供 STDIO 接口,必须通过别的手段进行进程间通讯。
- ZMQ 支持 TCP 自动重连、高性能异步 IO、内存溢出限制、……建议阅读 ØMQ is just Sockets! - zeromq
- ZMQ 简单,只做了传输协议与消息模式,应用层协议直接留白,不涉及序列化反序列化问题,没有多余的东西,Overhead 较小。
- gRPC, Thrift, Protobuf 基于 DSL,需要预先准备对应的协议,工具链启动成本较高。适合逻辑更重的场景。
建议使用 ZMQ 的 PAIR-PAIR 模式
- JS 端作为 PAIR-SERVER,X 端作为 PAIR-CLIENT,由 X 端主动连接 JS 端。
- 调试时,JS 端额外开放对外的端口。
- PAIR-PAIR 仅需使用一个端口,是最简单的全双工方案。
- 负载均衡问题不会在单个容器中处理。
- 调试问题:X 端无法接受两个连接,因此进入容器中也无法调试 X 端的功能,但可以在 JS 端额外监听一个端口来做调试功能。
响应式编程范式 Reactive JS (RxJS)
为了处理复杂的异步逻辑,Yuan 大量使用了 Rx 来构建系统,接口也多半是 Rx Style 的。 Rx 本身是一个跨语言的编程方式,但受到各个语言自身的语法特性的限制,发展状态不一。
我们建议在 JS 端使用 Rx,在 X 端不使用 Rx。
- 对于匿名函数构造比较困难的语言,可能不会有好用的 Rx 实现。例如 Python,C。
- 会在 X 端增加不必要的复杂度。
- 从比较优势的角度来看,应当在 JS 端使用 Rx 负责异步调度。
一些拾遗
提示:
- X 端可能不会提供实时订阅接口,需要 JS 端主动 Polling。
- X 端可能缺失某些接口,但是可以通过别的接口变相合成得到,这个过程可能比较复杂。
- 开发过程中,经常需要手动调用 X 端的功能来进行调试。
建议:
- 每个消息中使用 UUID,在两边的日志中都要打印出来。
- 推荐使用 JSON 进行通讯,如果 X 生态下没有 JSON 库,可以降级成 CSV-like 的编码格式。
- 提供 Batch 机制,因为某些提供商 API 性能较低(仅占一个同步线程),必须要进行批处理。
- X 端的代码设计尽量与 X 自身的 API 命名语义一致,尽量少处理,逻辑尽量薄,有助于排查问题。有条件的话,可以生成 X 端使用的 SDK API 对应的模板代码。
- 对 JS 端进行代码复用并测试,提高提供商接入的效率和质量。
- 有条件的话,针对常用的 X 语言,建设对应的 lib。