本文作者:Timothy
原文链接:《Passing arbitrary JSON into a Rust Remote Procedure Call (RPC) service》(https://medium.com/wasm/passing-arbitrary-json-into-a-rust-remote-procedure-call-rpc-service-54f3b3ec04e0)
本文主要讨论了如何将任意格式的 JSON 传入 Rust 远程过程调用(RPC)服务中的问题,分析了将 JSON 显示映射到 Rust 代码存在的缺陷,以及允许 RPC 应用程序接受、解析任意嵌套的 JSON的解决方案。
在《去中心化计算的未来:通过 RPC 从微服务过渡到 WASM》一文中介绍了 RPC 的使用,这是 WebAssembly (Wasm) 执行生态系统的一部分 。 我们从行程间通讯的角度讨论了 Wasm,证明了实现分布式机器之间无处不在的互操作性的前景,这能让这些计算机通过 RPC 单独执行离散的 Wasm 函数。
与上篇文章有关的工作还在继续。 具体来说,RPC 功能已经过渡到 Rust (作为对 C 的补充)。 这些变化带来了一些有趣的问题和新思维,我想与大家分享一下。
我理解 Rust 需要将任何预期的 JSON 显式映射到 Rust 代码,然后在代码成为生产部署的一部分之前进行编译。 下面的简单示例演示一个 “person” 数据对象是如何(通过 HTTP POST 发送)能被映射到 Rust 结构。
curl --header "Content-Type: application/json" --request POST --data '{ "name": "John Smith", "age": 19 }' localhost:8000/create_person
pub struct Person{
pub name: String,
pub age: i32
}
当我着手编写和编译描述输入数据的 Rust 数据结构时,我意识到这种方法存在一些重大的问题。
这种方法“非常复杂”。
在将数据映射到源代码时,需要事先了解输入的数据结构。 而在一些实际的用例中,在设计阶段我们不是总能知道输入的数据结构。
图源:澳大利亚墨尔本的 Stephen Edmonds
源代码中使用的结构数据类型是平面的。 这意味着,如果我们要解析和遍历嵌套数据,那么就需要在生产源代码中构建和维护多个复杂的数据结构,这些数据结构反映了 JSON 数据。
传入的 JSON 数据可能会发生轻微的更改。 这可能超出你的控制。 这可能取决于数据来自哪里,或者提供数据的软件的业务规则 、 计划是什么。 重点是,如果 JSON 与我们应用程序的源代码中的结构紧密地映射在一起,那么轻微的 JSON 调整可能会成为重大改变,这可能会使我们的应用程序停滞不前; 要求源代码的变化和产品的重新部署。
下面的解决方案允许 RPC 应用程序接受和解析任意嵌套的 JSON。 下面的示例将显示,可以使用最少预期数据来部署应用程序。 这个例子使用了 rocket: : Data struct
。
我花费了很多时间找到这个解决方案,并成功测试出来。 最终的结果非常简单: 代码量很少,并且总共只有两个依赖项。 因此,我认为值得与 Wasm 社区分享。
下面的操作指南演示了如何创建一个可以接受、识别和处理任意嵌套的 JSON 对象的 Rust RPC 服务器。系统准备(Ubuntu)
sudo apt-get update
sudo apt-get -y upgrade
Rust安装
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
Rust配置和整理
rustup override set nightly
rustup update && cargo update
创建RPC项目
cd ~
cargo new rpc
cd rpc
将 rocket 和 serde_json 依赖项添加到 Cargo.toml
[dependencies]
rocket = "0.4.2"
serde_json = "1.0"
创建~/rpc/src/main.rs (http://main.rs/) 文件
#![feature(proc_macro_hygiene, decl_macro)]
use std::str;
use rocket::Data;
use serde_json::{Value};
use rocket::response::content;
#[macro_use] extern crate rocket;
#[post("/add_person", data = "")]
fn add_person(bytes_vec: Data) -> content::Json<&'static str>{
if bytes_vec.peek_complete() {
let string_text = str::from_utf8(&bytes_vec.peek()).unwrap();
let v: Value = serde_json::from_str(string_text).unwrap();
println!("Person: {:?}", v["person"]["name"]);
}
content::Json("{'response':'success'}")
}
fn main() {
rocket::ignite().mount("/", routes![add_person]).launch();
}
创建应用
cd ~
cd rpc
cargo build --release
启动应用
cd ~
cd ~
cd rpc
./target/release/rpc
使用 HTTP POST 测试
curl --header "Content-Type: application/json" --request POST --data '{"person": { "name": "John Smith", "age": 19 }}' http://localhost:8000/add_person
您可能注意到,我们没有传递平面数据。我们传入了一个嵌套的数据结构。
您会注意到,Rust 代码v [“ person”] [“ name”]
能够将数据遍历到正确的点(person 对象的名称键值为 John Smith)。
Person: String("John Smith")
让我们来谈一谈更广泛的应用或数据来源!从这个 JSON 结构中引入或删除了数据的情况。我们来看看如果更改了数据中与该应用程序不直接相关的部分,该应用程序是否仍然可以继续工作。让我们传入这个新的完全不同的数据结构。
{
"person": {
"id": 512279332,
"name": "John Smith",
"occupation": "Sales Person",
"department": "Furniture",
"awards": [{
"name": "November sales person of the month",
"date_issued": 20191123
},
{
"name": "December sales person of the month",
"date_issued": 20191223
}
]
}
}
可以看到,这和原始的 JSON 完全不同;age
已被删除,添加了新的键值对(例如department
),并且还添加了一个新的嵌套对象数组,称为awards
。
调用新数据
curl --header "Content-Type: application/json" --request POST --data '{"person": {"id": 512279332,"name": "John Smith","occupation": "Sales Person","department": "Furniture","awards": {"name": "Sales person of the month","date_issued": 20191123}}}' http://localhost:8000/add_person
Rust 代码 v["person"]["name"]
任然返回
Person: String(“John Smith”)
可以看到,尽管 JSON 进行了重大更改,但应用程序的原始源代码仍返回期望/预期的结果。
现在让我们缩小并查看应用程序如何查看 JSON 对象 person
的根。我们通过将 Rust 代码从v["person"]["name"]
更新到 v["person"]
然后向 RPC 服务器发出以下 HTTP POST 调用。
curl --header "Content-Type: application/json" --request POST --data '{
"person": {
"id": 512279332,
"name": "John Smith",
"occupation": "Sales Person",
"department": "Furniture",
"awards": [{
"name": "November sales person of the month",
"date_issued": 20191123
},
{
"name": "December sales person of the month",
"date_issued": 20191223
}
]
}
}' http://localhost:8000/add_person
可以看到,println
输出实际上会自动识别每种单独的数据类型。
Person: Object({"awards": Array([Object({"date_issued": Number(20191123), "name": String("November sales person of the month")}), Object({"date_issued": Number(20191223), "name": String("December sales person of the month")})]), "department": String("Furniture"), "id": Number(512279332), "name": String("John Smith"), "occupation": String("Sales Person")})
我们可以看到,有一个名为 awards
的键的 Array 对象,其中包含几个嵌套对象等。这是对传入数据的正确解释。
这里要注意的是,这些数据都没有在 Rust 源代码中定义或提前编译。
允许调用代码传入任意字符串的同时,本质上也是发出接受 DDOS 风格攻击的公开邀请。 在源代码中 JSON 和 Rust 结构之间的刻意映射(这可以防止这种情况)是以花费精力和缺乏灵活性为代价的。 我认为上述解决方案是可以解决这两方面的问题的最好方案。 欢迎大家分享观点和想法。