❝越努力,越幸运 ❞
大家好,我是「柒八九」。一个「专注于前端开发技术/Rust
及AI
应用知识分享」的Coder
。
我们之前在Rust 赋能前端-开发一款属于你的前端脚手架中有过在Rust
项目中如何操作JSON
。
由于文章篇幅的原因,我们就没详细介绍这块的内容,而今天我们就抽空聊聊这个话题。-- 「如何在Rust中操作JSON,以及对最流行的库进行比较」
好了,天不早了,干点正事哇。
❝
❞
要在Rust
中处理JSON
,我们可以借助相关的JSON库
。其实市面上有很多相关的库,但是我们还是选择一种我们比较熟悉并且流行度高的库。--serde-json[1]
我们可以通过运行以下命令来安装它:
cargo add serde-json
完成后,我们可以像这样手动创建JSON
:
use serde_json::{Result, Value};
fn untyped_example() -> Result<()> {
// 一些JSON输入数据,作为一个&str。也许这些数据来自用户。
let data = r#"
{
"name": "Front789",
"age": 18,
"ability": [
"Front-end development",
"Rust",
"AI"
]
}"#;
// 将数据字符串解析为serde_json::Value。
let v: Value = serde_json::from_str(data)?;
// 通过使用方括号索引来访问数据的部分。
println!("我是{}。一个专注于{}/{}及{}应用知识分享**的Coder",
v["name"], v["ability"][0],v["ability"][1],v["ability"][2]);
Ok(())
}
然而,我们可以做得比这更好。例如,我们可以将JSON
序列化为结构体,这在许多应用中都有用途。我们可以在JSON模板
、Web服务
、CLI参数
(这点我们的f_cli[2]就使用了它)等方面使用它。
当然,我们也可以使用std::fs::write
来将这些JSON
数据写入到磁盘文件中。
Serde
是一个crate
,它帮助我们将数据序列化和反序列化为各种格式,其中一个流行的用途是用于JSON
。Serde
提供了两个主要的trait
来帮助我们完成这一点:Serialize
和Deserialize
。我们可以添加了一个派生宏实现来帮助我们完成这一点。
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct MyStruct {
message: String
}
fn convert_json_to_struct() {
// 从json!宏创建一个原始的JSON字符串,并将其转换为MyStruct结构体
let raw_json_string = json!({"message": "Hello Front789!"});
let my_struct: MyStruct = serde_json::from_str(raw_json_string).unwrap();
}
我们还可以创建「嵌套的JSON」,方法是将实现Serialize
和Deserialize
的结构体作为另一个也实现Serialize
和Deserialize
的结构体的字段:
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Post {
nested_json: PostMetadata,
title: String,
body: String
}
#[derive(Serialize, Deserialize)]
pub struct PostMetadata {
timestamp_created: DateTime<Utc>,
timestamp_last_updated: DateTime<Utc>,
categories: Vec<String>,
}
上面的代码可以用于我们用Rust
创建一个Web服务
(还记得我们之前介绍过的Rust Web 开发之Axum使用手册吗),并且返回一个嵌套JSON
。例如,当我们的Web服务器
收到一个POST
请求,其Body
中是一个Json
数据时,我们通常会将相关的Json类型
作为处理程序函数的参数传递。
use axum::Json;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Post {
nested_json: PostMetadata,
title: String,
body: String
}
#[derive(Serialize, Deserialize)]
pub struct PostMetadata {
timestamp_created: DateTime<Utc>,
timestamp_last_updated: DateTime<Utc>,
categories: Vec<String>,
}
async fn receive_some_json(
// 这个提取器消耗一个JSON主体,并将其转换为给定的结构类型
Json(json): Json<Post>
) -> Json<Post> {
println!("{:?}", json);
Json(json)
}
我们还可以从其字节表示形式转换为结构体:
let json_as_bytes = b"
{
\"message\": \"Hello Front789!\",
}";
let my_struct: MyStruct = serde_json::from_slice(json_as_bytes).unwrap();
上面的处理方式,在我们想将一个结构体存储在某个地方作为字节数组
,然后再将其转换回结构体时,有奇特的效果!
类似地,我们还可以从JSON
的「IO流」中读取JSON
并将其转换为结构体,使用.from_reader()
方法。以下代码中展示了如何在TCP流
中使用它:
use serde::Deserialize;
use std::error::Error;
use std::net::{TcpListener, TcpStream};
#[derive(Deserialize, Debug)]
struct User {
name: String,
age: String,
}
fn read_user_from_stream(tcp_stream: TcpStream) -> Result<User, Box<dyn Error>> {
let mut to_be_deserialized = serde_json::Deserializer::from_reader(tcp_stream);
let user = User::deserialize(&mut to_be_deserialized)?;
Ok(user)
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:7890").unwrap();
for stream in listener.incoming() {
println!("{:#?}", read_user_from_stream(stream.unwrap()));
}
}
这样,当我们在遇到需要处理JSON
的数据时,我们就可以直接从流中反序列化,而不是在内存中添加缓冲区。
其实,在大部分情况下,serde-json
已经能够满足我们的需求了。但是,在一些特殊情况下,例如数据量过大,此时serde-json
就有点吃力了。所以,市面上又有了一些提高 JSON
解析性能的crate
。(simd-json
/sonic-rs
)
从上图可知serde-json
有碾压式优势,也就是不到万不得已,我们还是使用serde-json
。不过,本着知己知彼,方能百战不殆。我们也需要知晓额外的解决方案。
这些 crates
大部分具有相同的 API
。除非另有说明,否则我们可以安全地在这些库之间切换,并期望在每个库中使用 JSON
时具有大致相同的接口。
❝
serde-json
是Rust
中下载和使用最多的JSON
库之一。 ❞
就性能而言,serde-json
本身并不慢。然而,然后对比其他两个crate
就有点稍逊了。这主要是因为它被采用非并行化的 CPU
使用架构。这样的话,serde-json
就无法在x86 CPU
的系统架构上,发挥更强的作用。
❝
x86
是一种广泛使用的中央处理单元 (CPU
) 计算机架构。它已成为个人计算机和服务器的主导架构。x86
这个名称源自 8086,这是英特尔® 发布的早期处理器。x86 CPU
使用「复杂指令集计算机」 (CISC
) 设计,允许它们在「单个周期内执行多条指令」。x想了解更多关于x86 CPU
的内容,可以参考x86介绍[3] ❞
simd-json[4] 是 simdjson C++ JSON
解析器的 Rust
版本,内置了 serde
兼容性。正如其名称所示,此库使用 SIMD
(单指令多数据)。这是一种用于能够使用并行处理处理多个数据点的技术,使其速度显著更快!然而,作为一个注意事项,它要求我们的系统具有 x86
能力,并且在运行时会选择最佳的 SIMD
特性集以获得性能。
文档中提到 simd-json
可以在本机目标编译时充分发挥作用。我们可以通过在运行程序时启用 rustc
中的以下编译器选项来实现此目标,例如:
rustc -C target-cpu=native
然而,如果我们像大多数使用 Cargo
的人一样,我们可能想使用 cargo run
。与示例中一样,我们可以在 .cargo/config
中创建一个配置,然后添加以下内容:
[build]
rustflags = ["-C", "target-cpu=native"]
在.cargo/config
配置相关的内容,我们在Rust
交叉编译Windows
环境时候,也涉及到。
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
一般来说,尽管这个库非常快,但应该注意到这个 crate
中有相当多的不安全代码,因为它是 C++ crate
的一个移植。这并不意味着我们不应该使用它,而是要谨慎使用。
还应该提到的是,为了获得最佳性能,通常最好启用 jemalloc
或 mimalloc
特性,以充分利用库。
通常情况下,simd-json
的 API 与 serde-json
相同,因此如果我们想在任何时候切换,通常不应该遇到任何问题。
sonic-rs[5] 是具有 SIMD
功能的 JSON
操作的 Rust
实现。这个库还有一个 C++
和 Go
的对应库!尽管它曾经需要 Rust nightly
工具链,但现在支持稳定的 Rust
。与 simd-json
类似,它也需要 x86 CPU
架构才能充分发挥作用。
与 simd-json
一样,要使用 sonic-rs
,我们需要在运行程序时启用 rustc
中的以下编译器选项:
rustc -C target-cpu=native
我们可以在 .cargo/config
中创建一个配置,然后添加以下内容以在使用 cargo run
时启用它:
[build]
rustflags = ["-C", "target-cpu=native"]
这样我们就可以构建支持 SIMD
的程序而无需做其他操作!
与 simd-json
类似,这个库中使用了相当多的不安全代码。然而,如果我们在库中搜索不安全代码,我们会发现比之前的库中的不安全代码可能更多。
sonic-rs
还具有一些额外的方法来进行惰性评估和提高速度。例如,如果我们想要一个 JSON
字符串文字,我们可以在反序列化时使用 LazyValue
类型将其转换为一个仍然带有斜杠的 JSON
字符串值。如果我们不怕不安全行为,或者确信它不会出错,还有很多未经检查的方法可供我们使用。
尽管 sonic-rs
是一个非常快的库,但它也是一个较新的 crate
,因此某些方法,如 from_reader
(允许从 IO 流读取)在 crate
中缺失。