目录

Rust-RPC

基于 Rust 标准库 TcpListener/TcpStream 和 JSON 的自研 RPC 框架。

没有使用任何现成 RPC 框架(无 tonic/tarpc/jsonrpsee,也不依赖任何异步运行时): Socket 编程、自定义帧协议、JSON 消息、服务注册与分发、线程池、混合加密握手、 IDL 编译器全部自行实现;第三方依赖仅限通用基础库(serde/serde_json 序列化、 rsa/sha2/rand 密码学原语、aes-gcm 对称加密)。 当前版本:**v0.8 最终版(跨语言客户端、性能测试与完整文档)**。

实现功能对应关系

实现功能 实现方式 位置
基于 Socket 接口编程 直接使用 TcpListener/TcpStream,自定义帧协议 service/src/frame.rs
不使用现成 RPC 框架 见上文说明,零 RPC/异步框架依赖 Cargo.toml
具备 RPC 基本调用能力 请求 → 服务端执行 → 响应返回 tcpclient/tcpserver
支持多个方法 方法名进入请求,Calculator 四方法动态分发 service/src/stub.rs
具备平台化特征 服务注册表 + 通用 RpcClient/RpcServer,Runtime 不感知业务 service/src/registry.rs、tests/platform.rs
稳定的 Socket 消息传输 长度前缀帧 + read_exact/write_all,半包/粘包/超长/非法帧处理 service/src/frame.rs、tests/framing.rs
并发模型 每连接一线程 / 固定大小线程池(worker_threads(N)) tcpserver/src/thread_pool.rs
安全通信(加分) RSA-OAEP 握手 + AES-256-GCM 会话加密,双模式自动分流 service/src/security.rs、crypto/
IDL 与编译器(加分) .rpc 文件四阶段编译,生成类型安全 Rust 客户端 rpc-idl/
跨语言调用(加分) Python 客户端(纯标准库)按同一协议调用 Rust 服务 clients/python/
设计说明与演示 本 README + DESIGN.md/PROTOCOL.md/TESTING.md/benchmarks/ 仓库根目录

版本演进记录

版本 主题 核心成果
v0.1 最小 RPC 一条 TCP 链路调通 Calculator.add(1,2) → 3
v0.2 帧协议 RPC1 长度前缀帧解决半包/粘包;request_id;结构化错误码
v0.3 服务注册表 RpcService trait + 注册表动态分发,四方法,平台化验收
v0.4 运行时 API 可复用 RpcServer/RpcClient,全部接口 Result<T, RpcError>
v0.5 并发模型 固定线程池,10 客户端×100 调用 0 失败,吞吐对比数据
v0.6 安全传输 RSA 握手 + AES-GCM,双模式自动分流,篡改/错误密钥防护
v0.7 IDL 代码生成 .rpc 四阶段编译器,生成类型安全客户端,确定性输出
v0.8 最终版 Python 跨语言客户端、性能基准、完整文档与仓库整理

每个版本有独立 Git 标签(git tag -l 查看),提交历史保持每步可编译可测试。

当前版本:v0.8-final-cross-language

最终提交版本,在 v0.7 基础上完成跨语言加分项、性能数据与完整文档:

  • 交互式演示客户端:cargo run -p tcpclient 进入 rpc> 提示符, 自定义服务/方法/参数发起调用(参数按 JSON 解析,支持引号字符串), 也可命令行单次调用(cargo run -p tcpclient -- calculator add 1 2); help 查看用法,quit 退出,--secure 同样适用;
  • Python 跨语言客户端(clients/python/,纯标准库):实现与 Rust 完全相同的 帧协议,调用 calculator.add(1, 2)、calculator.multiply(3, 4) 成功, 证明协议没有绑定 Rust 内部类型;业务错误同样以结构化错误码返回;
  • 性能测试(benchmarks/ + tcpclient/examples/bench_client.rs): 串行(明文/安全 × 小/4KB 消息 × 连接复用/每次重连)与并发 (1/4/8/16 客户端)实测数据,全部由真实运行产生并附复现命令; 要点:明文小消息 58µs/次(17k calls/s),8 客户端并发吞吐 39k calls/s, 安全模式每次重连需 ~263ms(临时 RSA 密钥对生成)——连接复用价值一目了然;
  • 服务端新增 RPC_QUIET=1 静默日志开关(基准测试使用,不影响正常演示);
  • 完整文档:PROTOCOL.md(线上协议规范)、DESIGN.md(设计说明)、 TESTING.md(测试与验收)、各示例/客户端 README,见上方文档导航。

IDL 与代码生成

轻量 IDL:业务接口用 .rpc 文件描述,自研编译器(词法 → 语法 → 语义 → 生成, 纯标准库)生成类型安全的 Rust 客户端包装;语义检查覆盖未知类型、重复命名、 Rust 关键字冲突等,生成确定性输出可用 diff 验证。生成代码包装 RpcClient, 明文与安全连接均可直接使用。

支持类型:i32 / string / bool;// 行注释。

语法定义

idl       := service_def+
service   := 'service' IDENT '{' rpc_def* '}'
rpc_def   := 'rpc' IDENT '(' type_list? ')' 'returns' type ';'
type_list := type (',' type)*

示例 examples/calculator.rpc:

service Calculator {
    rpc add(i32, i32) returns i32;
    rpc subtract(i32, i32) returns i32;
    rpc multiply(i32, i32) returns i32;
    rpc divide(i32, i32) returns i32;
}

编译命令

cargo run -p rpc-idl -- examples/calculator.rpc --out-dir generated

输出的 generated/calculator_client.rs(节选):

pub struct CalculatorClient {
    inner: RpcClient,
}

impl CalculatorClient {
    pub fn add(&mut self, a: i32, b: i32) -> Result<i32, RpcError> {
        let value = self.inner.call("Calculator", "add", json!([a, b]))?;
        decode_i32(value)
    }
    // subtract / multiply / divide 同构 ...
}

与手写客户端的对比

手写调用需要拼服务名/方法名字符串、手工构造 json! 参数、拿到 Value 后自行断言类型:

let result: Value = client.call("Calculator", "add", json!([1, 2]))?;
let result = result.as_i64().ok_or(...)?;   // 手工解码

生成代码把这三件事全部固化:方法名与服务名来自 IDL,参数直接用原生类型, 返回值按声明类型解码——调用方写 client.add(1, 2) 即可,接口变更时重新编译 IDL 即暴露所有不匹配,而不是运行时才发现。

自动生成代码的演示

cargo run -p tcpclient --example codegen_demo

启动临时服务端(注册 Calculator)并用生成的 CalculatorClient 完成四次调用。 端到端验收见 tcpclient/tests/codegen.rs(含安全连接与错误透传场景)。

安全传输

客户端                                       服务端
  │ 生成临时 RSA-2048 密钥对                    │
  │ ── RPCS 握手帧: 客户端公钥(DER) ──────────▶ │
  │                                            │ 生成 32 字节会话密钥
  │ ◀── RPCS 握手帧: RSA-OAEP(会话密钥) ────── │
  │ RSA 私钥解密 → 得到会话密钥                  │
  │                                            │
  │ ══ RPC1 业务帧: payload = AES-256-GCM(JSON)══│  双向加密,同一连接共享上下文

设计要点:

  • 为什么混合加密:RSA-OAEP 单块明文上限约 190 字节且慢,直接加密业务报文 既放不下大消息也浪费性能;AES-GCM 无长度限制且快,只缺一个安全分发密钥的渠道—— 恰好是 RSA 擅长的场景;
  • 解密失败必须报错:GCM 校验失败(密钥不匹配/篡改)返回 invalid_frame 错误帧并断开连接,绝不 panic;
  • 明文模式始终可用:安全是传输层扩展,不是替代。

服务端用法(并发 + 安全)

use service::stub::CalculatorService;
use tcpserver::RpcServer;

fn main() -> Result<(), service::error::RpcError> {
    let mut server = RpcServer::bind("127.0.0.1:3000")?;
    server.register_service("calculator", CalculatorService);
    let server = server.worker_threads(4);
    // server.require_secure()  // 可选:只接受安全连接
    server.run()
}

服务端用法(基础并发)

use service::stub::CalculatorService;
use tcpserver::RpcServer;

fn main() -> Result<(), service::error::RpcError> {
    let mut server = RpcServer::bind("127.0.0.1:3000")?;
    server.register_service("calculator", CalculatorService);
    let server = server.worker_threads(4); // 线程池模型;不调用则每连接一个线程
    server.run()
}

客户端用法

use serde_json::json;
use tcpclient::RpcClient;

fn main() -> Result<(), service::error::RpcError> {
    let mut client = RpcClient::connect("127.0.0.1:3000")?;
    // 或安全模式:RpcClient::connect_secure("127.0.0.1:3000")?
    let result = client.call("calculator", "add", json!([1, 2]))?;    // 3
    let result = client.call("calculator", "multiply", json!([3, 4]))?; // 12
    Ok(())
}

服务端返回业务错误(服务/方法不存在、参数错误等)时,call 返回携带对应错误码的 Err(RpcError);传输层失败映射为传输错误码。

并发模型

接受线程(RpcServer::run)
      │  accept 得到连接,包装成任务
      ▼
   任务队列(mpsc channel + Mutex)
      │
┌─────┼─────┬─────┐
▼     ▼     ▼     ▼
worker worker worker worker     ← 固定 N 个(worker_threads(N),演示与测试用 4)
│
└─ 每个任务 = 处理一个完整连接(帧循环)直到对端关闭

不调用 worker_threads 时退化为”每连接一个线程”模型:accept 后直接 thread::spawn,实现最简单但线程数随客户端增长。

并发测试数据(实测)

环境:i7-1165G7(4 核 8 线程)/ 40 GB / Windows 10 / rustc 1.98.0 / debug 构建 / 回环地址。

指标 结果
10 客户端并发,每人 100 次调用(共 1000 次) 全部成功,0 失败
共享状态服务(计数器)1000 次并发自增 返回值恰好为 1..=1000,无丢失无重复
4 个 sleep(300ms) 请求,4 worker 并行 总耗时 306ms(串行下限 1200ms)
吞吐对比:1000 次调用,1 worker 52.8ms(平均 52.8µs/次)
吞吐对比:1000 次调用,4 worker 28.9ms(平均 28.9µs/次),约 1.8×

(回环 + 简单加法负载下加速比达不到理论 4×:客户端线程本身、accept 与调度共享同一台机器。)

传输协议:自定义帧格式

TCP 是字节流协议,一次 read 可能读到半条消息或多条消息。 所有请求/响应以长度前缀帧传输:

+----------+---------+---------+------------------+
| Magic    | Version | Length  | Payload          |
| 4 bytes  | 1 byte  | 4 bytes | Length bytes    |
| "RPC1"   | 1       | 大端u32  | JSON 消息字节    |
+----------+---------+---------+------------------+
  • 发送侧:write_all 先写满 9 字节头,再写满 payload,长度由序列化结果计算;
  • 接收侧:read_exact 先读满头部,校验 magic/版本/长度后,再 read_exact 读满 payload;
  • 半包由 read_exact 的”读不满就等”解决,粘包由”每次恰好消费一帧字节数”解决;
  • 单帧 payload 上限 1 MiB(DEFAULT_MAX_FRAME_SIZE),超限在读 payload 之前拒绝;
  • 非法 magic/版本/长度时流已失步,服务端回一条 invalid_frame 错误帧后断开连接。

消息格式

请求(帧 payload):

{"request_id":1,"service":"calculator","method":"add","params":[1,2]}

响应(帧 payload):

{"request_id":1,"result":3}

出错时:

{"request_id":1,"error":{"code":"method_not_found","message":"service 'calculator' has no method 'power'"}}

request_id 由客户端生成,响应原样带回,用于请求/响应配对。 未知请求字段被忽略(向前兼容)。

错误码表

code 含义
invalid_frame 帧头 magic/版本/长度不合法(连接将被断开)
invalid_request 请求结构不合法
decode_error payload JSON 解析失败
service_not_found 请求的服务不存在(由注册表返回)
method_not_found 服务存在但方法不存在(由服务实现返回)
invalid_params 参数个数或类型错误(含除零)
internal_error 服务端内部错误
connection_failed 建立连接失败(客户端运行时产生)
connection_closed 连接在对端响应前被关闭
write_failed 请求写入失败
read_failed 响应读取失败
secure_mode_required 服务端要求安全连接,明文请求被拒绝

内置服务

Calculator(service/src/stub.rs),参数均为 [a: i32, b: i32]:

方法 示例 结果
add [1, 2] 3
subtract [5, 3] 2
multiply [4, 6] 24
divide [8, 2] 4(b=0 时返回 invalid_params)

平台化:如何新增一个服务

新增服务只需要两步,不触碰传输层与服务端连接处理:

  1. 实现 RpcService trait(参考 service/src/stub.rs 的 CalculatorService):
pub struct EchoService;

impl RpcService for EchoService {
    fn call(&self, method: &str, params: Value) -> Result<Value, RpcError> {
        // 按 method 分发,方法不存在返回 MethodNotFound
    }
}
  1. 在 tcpserver/src/main.rs 启动时注册:
server.register_service("echo", EchoService);

客户端只需把请求中的 service/method/params 换成对应值即可调用。 tcpserver/tests/platform.rs 中有一个只存在于测试代码里的 StringService, 它未改动任何运行时代码就被远程调用成功,是这一特性的直接验收证据。

目录结构

rpc-framework/
├── Cargo.toml              # workspace 根
├── README.md / DESIGN.md / PROTOCOL.md / TESTING.md / CHANGELOG.md
├── rpc-idl/                # 轻量 IDL 编译器(纯标准库)
│   ├── Cargo.toml
│   └── src/
│       ├── lib.rs          # compile() 完整流水线
│       ├── ast.rs          # AST 与类型映射
│       ├── lexer.rs        # 词法分析(行/列定位、注释)
│       ├── parser.rs       # 递归下降语法分析
│       ├── validator.rs    # 语义检查
│       ├── generator.rs    # Rust 客户端 Stub 生成(确定性输出)
│       └── main.rs         # 命令行入口
├── examples/
│   ├── calculator.rpc      # IDL 示例:Calculator 四方法
│   └── README.md           # 示例说明
├── generated/
│   └── calculator_client.rs # 由 rpc-idl 生成(可重新生成并 diff 验证)
├── clients/
│   └── python/             # 跨语言客户端(纯标准库)
│       ├── rpc_client.py   # 帧协议 + RpcClient
│       ├── calculator_demo.py
│       └── README.md
├── benchmarks/
│   ├── serial_benchmark.md       # 串行实测数据(明文/安全 × 大小消息 × 复用/重连)
│   └── concurrent_benchmark.md   # 1/4/8/16 客户端吞吐实测
├── crypto/                 # RSA 密钥操作(握手专用)
│   └── src/
│       ├── lib.rs
│       └── rsa_crypto.rs   # RSA-2048/OAEP 密钥对生成、加解密、DER 编解码
├── service/                # RPC 核心
│   └── src/
│       ├── error.rs        # RpcErrorCode / RpcError
│       ├── frame.rs        # 帧编解码 write_frame/read_frame + 握手帧(RPCS)
│       ├── message.rs      # RpcRequest / RpcResponse(request_id + Value)
│       ├── registry.rs     # RpcService trait + ServiceRegistry
│       ├── security.rs     # PayloadProtector trait + 明文/AES-256-GCM 实现
│       └── stub.rs         # CalculatorService(add/subtract/multiply/divide)
├── tcpserver/              # RPC 服务端运行时
│   └── src/
│       ├── lib.rs          # 公共入口:pub use RpcServer
│       ├── server.rs       # RpcServer:bind / register_service / worker_threads / require_secure / run
│       ├── connection.rs   # 模式探测 + 握手 + 帧循环分发(内部模块)
│       ├── thread_pool.rs  # 固定大小线程池(内部模块)
│       └── main.rs         # 演示:注册 calculator,4 worker 线程池
│   └── tests/
│       ├── loopback.rs     # 功能回环测试(调用/错误路径)
│       ├── framing.rs      # 帧鲁棒性验收测试
│       └── platform.rs     # 平台化验收:自定义服务仅靠注册接入
├── tcpclient/              # RPC 客户端运行时
│   ├── src/
│   │   ├── lib.rs          # 公共入口:pub use RpcClient
│   │   ├── client.rs       # RpcClient:connect / connect_secure / call
│   │   └── main.rs         # 演示:一次连接依次调用 calculator 四个方法
│   ├── examples/
│   │   ├── codegen_demo.rs # 自动生成客户端的调用演示
│   │   └── bench_client.rs # 性能基准(矩阵可配)
│   └── tests/
│       ├── runtime.rs      # 端到端验收:一次连接多次调用 + 错误映射
│       ├── concurrency.rs  # 并发验收 + 吞吐基准(基准默认忽略)
│       ├── secure.rs       # 安全传输验收(加密/篡改/大消息/强制模式)
│       └── codegen.rs      # 生成客户端的端到端验收
└── tools/
    └── probe.ps1           # 帧协议手工探测脚本(PowerShell)

编译与运行

要求:Rust 1.70+(在 rustc 1.98.0 / x86_64-pc-windows-msvc 上验证通过)。

cargo build
cargo test

演示(需要两个终端,先启动服务端):

cargo run -p tcpserver                            # 服务端
cargo run -p tcpclient                            # 交互模式(明文),自定义输入
cargo run -p tcpclient -- --secure                # 交互模式(安全连接)
cargo run -p tcpclient -- calculator add 1 2      # 单次调用后退出

跨语言调用(Python 3.8+,纯标准库,服务端不关):

python clients/python/calculator_demo.py

IDL 生成客户端与性能基准:

cargo run -p rpc-idl -- examples/calculator.rpc --out-dir generated
cargo run -p tcpclient --example codegen_demo
cargo run --release -p tcpclient --example bench_client -- --clients 8 --calls 1000

(性能数据与矩阵见 benchmarks/;基准运行时 服务端日志自动静默,或手动设置环境变量 RPC_QUIET=1。)

服务端输出:

[server] RPC server listening on 127.0.0.1:3000 (services: calculator)
[server] concurrency: fixed thread pool, 4 workers
[server] security: auto (plaintext or secure handshake)

客户端输出(交互模式,在 rpc> 提示符下自定义服务/方法/参数;参数按 JSON 解析, 数字与 true/false 直接输入,字符串可加双引号):

[client] connected to 127.0.0.1:3000 (plaintext mode)
输入 help 查看用法,quit 退出。
rpc> calculator add 1 2
RPC calculator.add(1, 2) = 3
rpc> calculator multiply 7 6
RPC calculator.multiply(7, 6) = 42
rpc> calculator divide 1 0
RPC calculator.divide error: [invalid_params] division by zero
rpc> weather get
RPC weather.get error: [service_not_found] unknown service 'weather'
rpc> quit
[client] bye

测试

  • service 单元测试 32 个:错误码序列化(含传输层错误码)、帧字节布局、 字节级分片读取(逐字节喂入)、连续帧不混淆、非法 magic/版本/超长拒绝、截断报错、 注册表分发/覆盖/未知服务、Calculator 四方法与各类参数错误;
  • tcpserver/tests/loopback.rs 8 个:四方法调用、同一连接连续调用、request_id 回带、 未知服务/方法、参数错误、除零、非法 JSON 的错误码断言;
  • tcpserver/tests/framing.rs 7 个:真实 TCP 上的半包、粘包(两帧连写)、大于 1KB payload、 非法 magic 拒绝并断开、超长帧在读 payload 前拒绝、非法 JSON 返回结构化错误且连接可继续用、 同一连接连续 100 次调用;
  • tcpserver/tests/platform.rs 4 个:测试内自定义服务(未改动运行时)可被远程调用、 与内置服务共存、自定义服务的方法/参数错误结构化、未注册服务返回 service_not_found;
  • tcpclient/tests/runtime.rs 4 个(公共 API 端到端):同一 RpcClient 连续四次调用 不重建连接、业务错误透传为对应错误码、连接失败 → connection_failed、 服务端中途断开 → connection_closed;
  • tcpclient/tests/concurrency.rs 3 个 + 1 个默认忽略的吞吐基准: 10 客户端并发各 100 次调用全部正确、共享状态计数服务 1000 次并发自增无丢失无重复、 4 worker 并行处理 4 个 sleep 请求(306ms,串行下限 1200ms); 基准 bench_single_worker_vs_four_workers 用 cargo test -p tcpclient --test concurrency -- --ignored --nocapture 运行;
  • tcpclient/tests/secure.rs 6 个(安全传输验收):安全模式四方法调用且共享同一 安全上下文、明文与安全客户端共存于同一服务端、线上字节是密文(不能按 JSON 解析, 解密后可读)、4KB 大消息经安全通道传输、篡改密文收到结构化错误并被断开、 require_secure 服务端拒绝明文(secure_mode_required)但接受安全连接;
  • tcpclient/tests/codegen.rs 4 个(生成代码验收):生成客户端四方法调用、 业务错误透传、服务名不匹配 → service_not_found、生成客户端在安全连接上工作;
  • rpc-idl 32 个:词法(记号流/注释/定位/非法字符)、语法(完整解析/零参数方法/ 多服务/缺分号缺 returns 报错)、语义(未知类型/重复方法/重复服务/Rust 关键字/ 空服务/参数上限/多错误汇总)、生成(命名/内容/按需解码器/确定性输出)、 流水线集成(位置定位/错误收集/多服务多文件);
  • crypto 单元测试 6 个:密钥生成、OAEP 往返、DER 往返、非法 DER、 错误密钥解密、篡改密文拒绝;
  • service/src/security.rs 单元测试 8 个:明文恒等、AES 小/大(100KB)消息往返、 密文非明文且非合法 JSON、随机 nonce(相同明文密文不同)、篡改/错误密钥/截断拒绝。
关于

基于Rust编程语言开发的RPC框架

296.0 KB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9 京公网安备 11010802047560号