Skip to content

jacobbubu/dnodeprotocol-chs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

概述

dnode 是一个非常灵巧的异步 RPC 系统,由 substack 在两年前用 node.js 开发。本身 dnode 既可以运行在 node.js 中,也可以通过 Browserify 运行在浏览器中。

dnode 是传输协议无关的,尤其在 node.js 中,任何支持 Stream 的传输协议都可以应用 dnode。除了 Javascript,dnode 也有 Perl,PHP,Ruby,Objective-C 和 Java 等不同语言的实现。

dnode 吸收了 Javascript 动态语言的灵活性和 node.js 的异步特性,即简单轻巧,又适用于大部分的“现代” RPC 场景。

原本我只是想翻译 dnode-protocol,后来发现按照英文直译其实很难理解,因此扩展为一个小教程。

如果你之前没用过 dnode,那么在阅读本文之前,建议先阅读 dnode

协议文档

dnode 的消息采用 JSON 方式表示, 以新行符进行消息分割。和其他强类型的 RPC 协议不同,dnode 并不需要通信的两端预先交换 IDL (Interface Definition Language),例如:CORBA 或者 COM,也不对实际的方法签名进行检查。就像 Javascript 本身,一切都在执行时才真的揭晓。

dnode 的消息有两种类型,“方法交换”消息和“方法调用”消息。

方法交换

在通信连接建立之后,RPC 的两端首先要做的就是和对方交换自己的方法信息,这样在本地就可以生成一个远端的代理对象(在有些 RPC 系统里称为 Stub)。这是通过以下消息实现的:

{
  "method": "methods",
  "arguments": [{ "timesTen": "[Function]", "moo": "[Function]" }],
  "callbacks": { "0": ["0", "timesTen"], "1": ["0", "moo"] }
}

为了阅读方便,我并没有把 JSON 消息写到一行里。

对于“方法交换”消息,method 字段的值约定为 "methods"(所以你不应该把自己的 RPC 方法命名为 "methods")。该消息的 arguments 字段是仅包含“一个”元素的数组,该元素定义了本方所拥有的方法名称。其中的 "[Function]" 仅仅是一个占位符,表示这是一个方法(或函数)。

显然,这种描述方法也意味着 dnode 不支持相同的方法名称但是其 signature 却不同(参数不同)

callbacks 则是用来为 arguments 中的方法名称设定对应的整数 ID。例如,arguments 中的方法 "timesTen",其 ID 在 callbacks 中被定义为 "0",而 "moo" 的 ID 则为 "1"。 "0" : ["0","timesTen"] 的含义为:ID "0",指代的是 arguments 数组的第 0 个元素,且该元素(元素为一个 Object)的 key 为 "timesTen" 的值。["0","timesTen"] 在这里称为指向 arguments 内容的路径。类似地,["0","moo"] 是 ID "1" 的 arguments 中内容的路径。因此可以看到,某个整数 ID 可以实际代表 arguments 数组中任意深度的某个具体值。

使用 ID,一方面是为了今后调用时可以用 ID 替代方法名称以节约流量消耗,另一方面,也是为了应对异步回调时的匿名函数的情况。继续阅读“方法调用”章节即可有更深入的理解。

方法调用

我们看一个例子,其中既包含了“方法交换”也包含了“方法调用”的过程:

var proto = require('dnode-protocol');

var s = proto({
  x: function(f, g) {
    setTimeout(function() {
      f(5);
    }, 200);
    setTimeout(function() {
      g(6);
    }, 400);
  },
  y: 555
});
var c = proto();

s.on('request', c.handle.bind(c));
c.on('request', s.handle.bind(s));

c.on('remote', function(remote) {
  function f(x) {
    console.log('f(' + x + ')');
  }
  function g(x) {
    console.log('g(' + x + ')');
  }
  remote.x(f, g);
});

s.start();
c.start();

这是 module dnode-protocol 中的例子。这个 module 是 dnode 协议的 Javascript 实现,其本身和通信协议是无关的。具体 dnode-protocol 的 API 的文档请直接去它的 GitHub 站点。

上例中 cs 是通信的两端,你可以理解为 Client 和 Server。s 声明了 x 方法和 y 常量(s = proto({x: function{...}, y: 555})),而 c 没有声明任何方法(c = proto())。 一旦 s 或者 c 启动(s.start();c.start()),它们就会主动生成 “方法交换”的消息。该消息通过发布 request 事件发送出来。上例中的 s.on('request', c.handle.bind(c)); 表示,一旦 s.start(),那么就将发出包含“方法交换”消息 request 事件(具体消息对象是 on('request', function (req, argv) {} 中的 req),然后由 c.handle 函数来处理,反之亦然。

c.handle 收到这个消息之后,会发出 remote 事件(c.on('remote', function (remote) {}),回调函数中的参数 remote 实际上是 s 的一个方法代理,执行 remote.x(...) 或者 remote.y() 将最终调用到 sxy 方法。

上例中 c.on('remote', function (remote) {} 最终调用了 remote.x(f, g), 意味着为 s.x 方法传递了 fg 两个回调函数,因此最终对 s.x 的调用又会回到 c 中的函数 fg。所以该例最终的执行结果是在 console 输出了:

f(5)
g(6)

f(5) 先返回,因为在 s 上对 f 的回调是收到消息后 200 毫秒,而 g 则为 400 毫秒。你可以自己调整这些值来实验一下。

通信的一端无论是否有 RPC 方法,都要向对方发送“方法交换”消息,例如,c 发出的“方法交换”消息为

{
  "method": "methods",
  "arguments": [{}],
  "callbacks": {},
  "links": []
}

可见都是空的,即使如此,s 上仍然也会产生一个代表 c 的 RPC 方法的 remote 对象,只是其中一个方法都没有。

s 产生的“方法交换”消息的内容为:

{
    "method": "methods",
    "arguments": [ { "x": "[Function]", "y": 555 } ],
    "callbacks": { "0": [ "0", "x" ] },
    "links": []
}
}

我们可以验证一下之前对“方法交换”消息的说明:

  • method 固定取值为 methods
  • arguments是个数组,其第一个元素是个对象,其两个 keys xy 分别对应一个方法和一个常量(555)
  • callbacks 声明了一个方法 ID 0,指向 arguments 的第 0 个元素的 "x" 属性的值

“方法交换”完成后,c 会得到 s 的代理对象 remote,随后通过 remote.x(f, g) 调用 s.x,其对应的消息如下:

{
  "method": 0,
  "arguments": ["[Function]", "[Function]"],
  "callbacks": { "0": ["0"], "1": ["1"] },
  "links": []
}

其中 method 的值就是上一个消息中的 callbacks 中声明的 ID,对应到 s.x。在这里,"method": 0 或者 "method": "x" 都是合法的,只是用 ID 在大多数情况下更精简一些。而 arguments 中的两个元素 "[Function]" 则和之前的消息不同,并没有对应的名称。这是因为 remote.x(f, g) 中的 fg 本就是匿名函数,不会出现在 s 那里对应 cremote 代理对象中。而 callbacks 中则分别为这两个匿名函数准备好了 ID "0" 和 "1"。最终,s.x 执行时回调 fg 就是通过这两个 ID 完成的,消息如下:

对应 setTimeout(function () { f(5) }, 200) 的消息:

{
  "method": 0,
  "arguments": [5],
  "callbacks": {},
  "links": []
}

对应 setTimeout(function () { g(6) }, 400) 的消息:

{
  "method": 1,
  "arguments": [6],
  "callbacks": {},
  "links": []
}

所有的 dnode 消息超不出以下四个字段:

  • method :: String 或 Integer
  • arguments :: Array
  • callbacks :: Object
  • links :: Array

links 请见下节。

links

"links" 是一个可选的字段,用来表示 arguments 中循环引用的数据结构。

我们看一段代码(其中 fn 是一个远程方法):

var data = { a: 5, b: [{ c: 5 }] };
data.b.push(data);
fn(data);

data.b 的第二个元素又指回了 data,因此 data 是一个循环引用数据结构。

其产生的 message 为:

{
  "method": 12,
  "arguments": [{ "a": 5, "b": [{ "c": 5 }] }],
  "callbacks": {},
  "links": [{ "from": [0], "to": [0, "b", 1] }]
}

例子里假设 fn 的 ”method id“ 是 12

links 是个数组,每个元素为一个包含 fromto 的 link 定义。上例中的唯一一个 link 定义表示:arguments 中的第一个元素的 key 为 "b" 的对象的第二个元素("to" : [ 0, "b", 1 ] }),其值指向 arguments 的第一个元素("from" : [ 0 ])。

请注意,link 也不仅仅是用来表示循环引用的数据结构,引用本身也可以用来消除重复的数据。

About

A tutorial of dnone protocol in Chinese

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published