Cap'n Web:为现代Web打造的轻量级、双向对象能力RPC协议

0 阅读8分钟

Cap'n Web: 轻量级、双向对象能力RPC

Cap'n Web 是一个专为现代 Web 环境设计的 JavaScript 原生 RPC(Remote Procedure Call)系统。它是 Cap'n Proto 的精神继承者,但专为 Web 技术栈优化,摒弃了复杂的 Schema,拥抱 JSON 和 JavaScript 的灵活性,同时在核心保留了强大的对象能力(Object-Capability)模型。

核心优势在于其极致的表达能力和轻量化设计:仅 10kB (压缩后),无依赖,支持所有主流 JavaScript 运行时,让跨进程、跨网络通信如同调用本地方法一样简单、强大且安全。

功能特性

Cap'n Web 的核心优势在于其对对象能力模型的实现,这使其显著区别于传统 RPC 框架。

  • ✨ 真正的双向调用 (Bidirectional RPC):协议本身是全然双向的。客户端可以调用服务器的方法,服务器同样可以调用客户端暴露的接口。这在实现回调、事件推送等场景时无需复杂的额外配置。

  • 🔗 按引用传递函数与对象 (Pass-by-Reference)

    • 函数引用:将客户端的一个函数作为参数传递给服务器,服务器获得的是一个“桩 (Stub)”。当服务器调用这个桩时,实际执行的代码仍在客户端。
    • 对象引用:任何继承自 RpcTarget 的类实例,在 RPC 中都会以引用的方式传递。对该对象的方法调用会通过网络路由回对象创建的原端。
  • ⏳ Promise 管道 (Promise Pipelining):在发起一个异步 RPC 调用后,可以立即在其返回的 Promise 上继续链式调用,而无需等待第一个 Promise 完成。这极大地减少了网络往返延迟,提升了分布式应用的性能。

  • 📦 零配置、无模式 (Schema-less):无需学习 IDL(接口定义语言)或生成胶水代码。直接使用 TypeScript/JavaScript 的类和函数定义接口,开箱即用。同时,TypeScript 类型定义确保了优秀的类型安全性。

  • 🌐 多传输层支持 (Multi-Transport):内置了对主流传输协议的支持,可轻松扩展。

    • WebSocket: 适用于需要全双工、低延迟通信的场景。
    • HTTP 批处理 (HTTP Batch): 将多个请求合并到一个 HTTP 请求中,适合无状态的 Request-Response 模式。
    • postMessage(): 在浏览器环境的不同 Worker (Web Worker, Service Worker) 或 iframe 间进行通信。
    • 自定义: 通过实现简单的 RpcTransport 接口,可以接入任何自定义的双工消息通道。
  • 🛡️ 对象能力安全模型:基于引用的安全性是其核心。一个对象只有持有对另一个对象的引用才能与之交互,这种“最小权限”原则天然地构建了安全边界,简化了复杂分布式系统的安全保障。

  • ⚡️ 跨运行时兼容:经过精心设计,可在所有主流浏览器、Node.js 和 Cloudflare Workers 中完美运行,拥有统一的 API 体验。

安装指南

Cap'n Web 是一个标准的 ES Module 包,可以通过 npm 或 yarn 进行安装。

系统要求

  • JavaScript 运行时: 支持 ES2023 (或通过 polyfill 支持 Symbol.dispose) 的任何环境,包括:
    • 现代浏览器 (Chrome, Firefox, Safari, Edge)
    • Node.js 18+
    • Cloudflare Workers
    • Deno (理论上支持)
  • 包管理器: npm, yarn 或 pnpm

安装步骤

  1. 在你的项目目录中,使用 npm 或 yarn 安装:

    npm install capnweb
    

    yarn add capnweb
    
  2. 在你的代码中导入所需的功能:

    // 导入核心类和函数
    import { RpcTarget, RpcStub, newWebSocketRpcSession, newHttpBatchRpcSession } from 'capnweb';
    

使用说明

Cap'n Web 的设计理念是让 RPC 调用尽可能接近本地函数调用。以下是几个核心场景的示例。

基础示例:定义和暴露服务

首先,定义一个服务类,继承自 RpcTarget。这个类的实例可以被远程访问。

// server.js (或 server.ts)
import { RpcTarget } from 'capnweb';

// 一个简单的计数器,将作为远程对象暴露
export class Counter extends RpcTarget {
  constructor(private value: number = 0) {
    super();
  }

  // 公共方法将可供远程调用
  increment(amount: number = 1): number {
    this.value += amount;
    return this.value;
  }

  // Getter 同样可以远程调用
  get currentValue(): number {
    return this.value;
  }
}

WebSocket 全双工通信

此示例展示了如何通过 WebSocket 建立一个双向 RPC 连接,服务端可以调用客户端提供的回调函数。

服务端 (Node.js 或 Workers)

import { WebSocketServer } from 'ws'; // Node.js 环境
import { newWebSocketRpcSession, RpcTarget } from 'capnweb';
import { Counter } from './server.js'; // 引入上面的 Counter 类

// 假设已创建 WebSocket 服务器 wss
wss.on('connection', (ws) => {
  // 创建一个 Counter 实例作为服务端主入口点
  const mainCounter = new Counter(10);

  // 启动 RPC 会话,将 mainCounter 暴露给客户端
  // 返回的 remoteStub 代表客户端的 'main' 接口
  const remoteClientMain = newWebSocketRpcSession(ws, mainCounter);

  // 3秒后,通过 remoteClientMain 调用客户端的方法
  setTimeout(async () => {
    // 假设客户端有一个 'report' 函数
    if (remoteClientMain.report) {
      try {
        const response = await remoteClientMain.report('Server count is now: ' + await mainCounter.currentValue);
        console.log('Client reported back:', response);
      } catch (err) {
        console.error('Failed to call client report:', err);
      }
    }
  }, 3000);
});

客户端 (浏览器)

import { newWebSocketRpcSession, RpcTarget } from 'capnweb';

// 客户端也可以暴露自己的服务给服务器调用
class ClientReporter extends RpcTarget {
  report(message: string): string {
    console.log('Report from server:', message);
    return 'Message received by client!';
  }
}

const ws = new WebSocket('ws://localhost:8080');
// 启动 RPC 会话,将 ClientReporter 实例暴露给服务器
// remoteServerStub 代表服务端的 'main' 接口 (即 Counter 实例)
const remoteServerStub = newWebSocketRpcSession(ws, new ClientReporter());

// 可以直接调用服务器上的 Counter 方法
async function run() {
  console.log(await remoteServerStub.currentValue); // 输出: 10
  console.log(await remoteServerStub.increment(5)); // 输出: 15
}

run();

HTTP 批处理通信

适用于请求-响应模式,将多个调用合并到一个 HTTP 请求中。

服务端 (Node.js)

import http from 'node:http';
import { nodeHttpBatchRpcResponse } from 'capnweb';
import { Counter } from './server.js';

const server = http.createServer((req, res) => {
  // 假设我们将 '/api' 路径用于 RPC
  if (req.url?.startsWith('/api')) {
    // 处理批处理 RPC 请求,暴露一个新的 Counter 实例
    nodeHttpBatchRpcResponse(req, res, new Counter(100));
  } else {
    res.writeHead(404).end();
  }
});
server.listen(3000);

客户端 (浏览器或 Node.js)

import { newHttpBatchRpcSession } from 'capnweb';

// 创建一个指向服务端端点的会话
const remoteCounter = newHttpBatchRpcSession('http://localhost:3000/api');

async function run() {
  // 尽管是 HTTP 批处理,但使用方式与 WebSocket 一致
  console.log(await remoteCounter.currentValue); // 输出: 100
  console.log(await remoteCounter.increment(10)); // 输出: 110
  // 注意:这里的两个调用可能会被合并到同一个 HTTP 请求中(取决于内部批处理策略)
}

run();

核心代码

1. 核心类型定义与序列化 (src/serialize.ts 节选)

该代码片段展示了 Cap'n Web 如何对非 JSON 原生类型(如 Date, BigInt, Error)进行编码。它使用一个带类型标记的数组来表示这些特殊类型,从而在保持 JSON 可读性的同时扩展了其能力。

// src/serialize.ts (节选)

// 判断一个值在 RPC 中应被视为何种类型
export function typeForRpc(value: unknown): TypeForRpc {
  // ... 基础类型判断 ...

  // 对于对象和函数,通过其原型链来判断
  let prototype = Object.getPrototypeOf(value);
  switch (prototype) {
    case Object.prototype:
      return "object";
    case Function.prototype:
      return "function";
    case Array.prototype:
      return "array";
    case Date.prototype:
      return "date";
    case Error.prototype: // 检查是否是 Error 或其子类
      // 特别检查常见的错误类型
      if (value instanceof TypeError) return "error";
      if (value instanceof RangeError) return "error";
      // ... 其他错误类型
      return "error";
    // ... 处理 RpcTarget, Promise 等
  }
  // ...
}

// 在序列化时,对于非基本类型,会调用类似的方法将其转换为可传输的格式
// 例如,一个 Date 对象会被转换为:["date", 1749342170815]

2. 可远程调用的目标基类 (src/core.ts 节选)

任何希望被远程调用的类都应该继承 RpcTarget。这个基类为 RPC 系统提供了一个身份标记,确保该对象在网络上是以引用而非值的方式传递。

// src/core.ts (节选)

// 一个独特的 Symbol 品牌,用于在类型系统中标识 RpcTarget
export const __RPC_TARGET_BRAND: unique symbol = Symbol.for('capnweb.rpcTarget');

// RpcTarget 类的类型定义,它使用品牌来防止结构类型误用
export interface RpcTarget {
  [__RPC_TARGET_BRAND]: never;
};

// RpcTarget 的具体实现。在支持 cloudflare:workers 的环境中,
// 可能会使用其原生实现以获得更好的集成。
export let RpcTarget = workersModule ? workersModule.RpcTarget : class {
  // 一个空的基类,其主要作用是提供品牌标记。
  // 实际逻辑由 RPC 系统通过 Proxy 和内部表处理。
};

3. WebSocket 传输层实现 (src/websocket.ts 节选)

该代码展示了 WebSocketTransport 如何实现 RpcTransport 接口,将 WebSocket 的消息事件适配成 RPC 核心引擎所需的简单 send()receive() 方法。

// src/websocket.ts (节选)

class WebSocketTransport implements RpcTransport {
  #webSocket: WebSocket;
  #sendQueue?: string[];  // 连接建立前暂存消息
  #receiveResolver?: (message: string) => void;
  #receiveQueue: string[] = [];
  #error?: any;

  async send(message: string): Promise<void> {
    // 如果连接已就绪,直接发送;否则加入队列等待 open 事件
    if (this.#sendQueue === undefined) {
      this.#webSocket.send(message);
    } else {
      this.#sendQueue.push(message);
    }
  }

  async receive(): Promise<string> {
    // 如果有缓存的消息,立即返回
    if (this.#receiveQueue.length > 0) {
      return this.#receiveQueue.shift()!;
    } else if (this.#error) {
      throw this.#error;
    } else {
      // 否则,等待下一个 message 事件
      return new Promise<string>((resolve, reject) => {
        this.#receiveResolver = resolve;
        this.#receiveRejecter = reject;
      });
    }
  }

  // 当 RPC 会话需要中止时调用,尝试通知对方并清理资源
  abort?(reason: any): void {
    let message = reason instanceof Error ? reason.message : `${reason}`;
    this.#webSocket.close(3000, message);
    // ... 清理内部状态
  }

  // 构造函数中设置事件监听器,将 WebSocket 事件转换为 Promise 的 resolve/reject
  constructor(webSocket: WebSocket) {
    this.#webSocket = webSocket;
    // ... 处理 open, message, close, error 事件
    webSocket.addEventListener("message", (event) => {
      if (typeof event.data === "string") {
        if (this.#receiveResolver) {
          this.#receiveResolver(event.data);
          this.#receiveResolver = undefined;
        } else {
          this.#receiveQueue.push(event.data);
        }
      }
    });
    // ...
  }
}
```FINISHED
P14CvGTT35tVv2ry9KnGB1EihlQqe/flwBmfOGjBF1c=