Cap'n Web:用于浏览器和 Web 服务器的新 RPC 系统
GraphQL 的替代方案?
如果您以前使用过 GraphQL,您可能会注意到一些相似之处。GraphQL 的一个优点是允许客户端在一次查询中请求多条数据,从而解决了传统 REST API 的“瀑布流”问题。例如,您不再需要依次发起三次 HTTP 请求:
图1
……您可以编写一个 GraphQL 查询来一次性获取所有内容。
相对于 REST 来说,这是一个很大的改进,但 GraphQL 也有其自身的缺点:
新语言和工具。您需要采用 GraphQL 的模式语言、服务器和客户端库。如果您的团队完全使用 JavaScript,那会增加很多额外的开销。
有限的可组合性。GraphQL 查询是声明式的,这使得它们非常适合用于获取数据,但对于链式操作或修改操作来说却很不方便。例如,您不能简单地说:“创建一个用户,然后立即使用这个新用户对象发起好友请求,且整个过程仅需一次网络往返。”
不同的抽象模型。 GraphQL 的外观和使用体验与您熟悉的 JavaScript API 截然不同。您是在学习一种新的思维模型,而不是扩展您日常使用的模型。
Cap’n Web 如何更进一步
Cap’n Web 解决了“瀑布流”问题,且无需引入新的语言或生态体系——它用的就是 JavaScript。得益于对 Promise 流水线和对象引用的支持,您可以写出如下所示的代码:
图2
这里面到底发生了什么?两个调用都被整合到一次网络往返中:
1.创建用户。
2.获取该调用的结果(一个新的 User 对象)。
3.立即对该对象调用 sendFriendRequest()。
所有这些都可以用 JavaScript 自然地表达,无需任何模式、查询语言或特殊工具。您只需调用方法并传递对象即可,就像在任何其他 JavaScript 代码中一样。
换句话说,GraphQL 为我们提供了一种化解 REST 架构中“瀑布流”问题的方案。而 Cap’n Web 则更进一步:它让您能够像编写普通程序那样,精确建模复杂的交互逻辑,且不存在任何阻抗失配问题。
但是我们如何解决数组问题呢?
结合我们目前为止所介绍的内容,若要将 Cap’n Web 作为 GraphQL 的可行替代方案来认真考虑,还有一个关键环节不容忽视:列表处理。GraphQL 常被用于实现这样的需求:“执行这个查询,然后针对每一个查询结果,再执行另一个查询。”例如:“列出用户的所有好友,然后为每位好友获取其个人资料照片。”
简而言之,我们需要一个无需增加往返即可执行的 array.map() 操作。
从历史发展来看,Cap’n Proto 从未支持过这样的功能。
但是,通过 Cap'n Web,我们已经解决了这个问题。您可以这样写:
图3
这是怎么实现的?
.map() 接受一个回调函数,该函数需要应用于数组中的每个元素。如前所述,通常,当您将函数传递给 RPC 时,该函数会“通过引用”传递,这意味着远程端会收到一个存根 (stub),调用该存根会将 RPC 返回到创建该函数的客户端。
但这里的情况并非如此。那样做就违背了目的:我们不希望服务器必须往返客户端来处理数组的每个成员。我们希望服务器只在服务器端应用转换。
为此,.map() 比较特殊。它不会向服务器发送 JavaScript 代码,而是发送类似“代码”的东西,仅限于特定领域的非图灵完备语言。“代码”是服务器应该针对数组的每个成员执行的一系列指令。在本例中,这些指令如下:
1.调用 api.getUserPhoto(friend.id)。
2.返回一个对象 {friend, photo},其中 friend 是原始数组元素,photo 是步骤 1 的结果。
但是,应用程序代码只指定了一个 JavaScript 方法。我们究竟该如何将其转换为狭义 DSL 呢?
答案是记录-回放机制:在客户端,我们执行一次回调函数,并传入一个特殊的占位值。该参数的行为类似于一个 RPC Promise,但要求回调函数必须是同步的,因此它实际上无法等待这个 Promise。它唯一能做的就是利用 Promise 流水线发起流水线调用。这些调用会被实现层拦截并记录为指令,随后这些指令可以被发送到服务端,并在需要时进行回放。
而且,由于记录基于 Promise 流水线,而这正是 RPC 协议本身的设计目的,因此,用于表示 map 函数“指令”的“DSL”只是 RPC 协议本身。🤯
实现详情
基于 JSON 的序列化
Cap'n Web 的底层协议基于 JSON,但包含一个预处理步骤来处理特殊类型。数组被视为“转义序列”,以便我们对其他值进行编码。例如,JSON 没有 Date 对象的编码,但 Cap'n Web 有。您可能会看到如下消息:
图4
要对文字数组进行编码,我们只需用两对 [] 将其双重包裹起来:
图5
换句话说,如果一个数组只有一个元素,并且该元素本身也是一个数组,则其值等于其内部数组的字面值。如果一个数组的第一个元素是类型名称,则其值等于该类型的一个实例,其余元素是该类型的参数。
需要注意的是,目前仅支持一组固定的类型:主要是“可结构化克隆”类型以及 RPC 存根类型。
在此基本编码之上,我们定义了一个受 Cap'n Proto 启发的 RPC 协议,但该协议已得到极大的简化。
RPC 协议
由于 Cap'n Web 是一种对称协议,因此在协议层面上没有明确定义的“客户端”或“服务器”。只有两方通过连接交换消息。任何交互都可以在任意方向上进行。
为了更容易描述这些互动,我将双方称为“Alice”和“Bob”。
Alice 和 Bob 通过建立某种双向消息流来启动连接。这可能是 WebSocket,但 Cap'n Web 也允许应用程序定义自己的传输方式。如前所述,流中的每条消息都采用 JSON 编码。
Alice 和 Bob 各自维护着一些与连接相关的状态信息。具体来说,双方都维护着一个「导出表」(描述他们暴露给对方的所有引用传递对象)和一个「导入表」(描述自己从对方接收到的引用)。Alice 的导出项对应 Bob 的导入项,反之亦然。导出表中的每个条目都有一个带符号的整数 ID,用于引用该条目。您可以把这些 ID 想象成 POSIX 系统中的文件描述符。不过与文件描述符不同的是,这些 ID 可以是负数,并且在连接的整个生命周期内,一个 ID 永远不会被重复使用。
在连接开始时,Alice 和 Bob 在各自的导出表中填充一个编号为 0 的条目,代表他们的“主”接口。通常,当一方充当“服务器”时,他们会将其主公共 RPC 接口导出为 ID 为 0 的条目,而“客户端”则会导出一个空接口。不过,这取决于应用程序:任何一方都可以导出任何他们想要的接口。
从那里开始,可以通过两种方式添加新的导出项:
当 Alice 向 Bob 发送包含对象或函数引用的消息时,Alice 会将目标对象添加到她的导出表中。在这种情况下分配的 ID 始终为负数,从 -1 开始向下计数。
Alice 可以向 Bob 发送一条“推送”(push) 消息,请求 Bob 向其导出表添加一个值。该“推送”消息包含一个表达式,Bob 会对其进行求值并导出结果。通常,该表达式描述了对 Bob 现有导出之一的方法调用——这就是 RPC 的实现方式。每次“推送”操作都会在导出表上分配一个正 ID,从 1 开始向上计数。由于仅在推送操作后才会分配正 ID,因此 Alice 可以预测她的每次推送操作产生的 ID,并可以在后续消息中立即使用该 ID。这就是 Promise 流水线的实现方式。
在发送“推送”(push) 消息后,Alice 可以接着发送一条“拉取”(pull)消息,该消息会告知 Bob:一旦他完成对“推送”内容的求值,就应该主动将结果序列化并回传给 Alice,形式为一条“解析”(resolve) 或“拒绝”(reject) 消息。不过,这一步是可选的:如果 Alice 只是想利用该 RPC 调用在 Promise 流水线中继续后续操作,而并不真正关心获取返回值,那么她可能根本不会去接收这个返回值。事实上,Cap’n Web 的实现只有在应用程序确实 await 了返回的 Promise 时,才会发送“拉取”消息。
把它们放在一起,代码序列如下:
图6
可能产生如下的消息交换:
图7
有关该协议的更多详细信息,请查看文档。
立即试用!
Cap’n Web 仍是一项较新的技术,目前正处于高度实验阶段,可能存在一些待解决的漏洞。但事实上,我们已经在实际使用它了。Cap’n Web 是 Wrangler 最近推出的“远程绑定”功能的基础,该功能让本地测试环境中的 workerd 实例能够通过 RPC 与生产环境中的服务进行通信。此外,我们也已开始在各类前端应用中对其进行实验性应用,未来敬请期待更多相关博客文章。
无论怎样,Cap’n Web 是开源的,您现在就可以在自己的项目中开始使用它。
您可以在 GitHub 上查看
Cloudflare 保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序,抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的部署与实施。
从任何设备访问 1.1.1.1,使用我们的免费应用加速和保护您的互联网。
立即联系我们,获取更多相关信息http://t.cn/A6Tx4yTT
#Cloudflare##Cap’n Web#
