Table of contents
Open Table of contents
架构:抽象与实现分离
serde(框架层)
定义两个 trait: Serialize, Deserialize
定义中间数据模型: bool, i64, string, seq, map, struct, enum...
提供 #[derive] 宏 + #[serde(...)] 属性
↓ 连接
格式 crate(实现层)— 各自独立,互不依赖
serde_json → JSON
bincode → 紧凑二进制
toml → TOML 配置文件
serde_yaml → YAML
ciborium → CBOR(二进制 JSON)
postcard → 嵌入式友好的二进制
rmp-serde → MessagePack
你写一次 #[derive(Serialize, Deserialize)],所有格式 crate 都能用:
#[derive(Serialize, Deserialize)]
struct Player { name: String, level: u32 }
serde_json::to_string(&player)?; // {"name":"Bear","level":15}
bincode::serialize(&player)?; // [4, 66, 101, 97, 114, 15, 0, 0, 0]
toml::to_string(&player)?; // name = "Bear"\nlevel = 15
Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] } # 框架 + derive 宏
serde_json = "1" # JSON 格式
bincode = "1" # 二进制格式
toml = "0.8" # TOML 格式
features = ["derive"] 启用 #[derive(Serialize, Deserialize)],几乎必开。
serde 核心:Serialize 和 Deserialize trait
// 简化概念
trait Serialize {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
// "把自己描述给一个 serializer"
// serializer 是 JSON/bincode/TOML 等具体实现
}
trait Deserialize<'de> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>;
// "从一个 deserializer 中构造自己"
}
你不需要手写这些。#[derive] 自动生成。
#[derive] 基本用法
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Player {
name: String,
level: u32,
health: f32,
items: Vec<String>,
position: Option<[f32; 3]>,
}
// 序列化
let player = Player {
name: "Bear".into(),
level: 15,
health: 87.5,
items: vec!["AK-47".into(), "Bandage".into()],
position: Some([10.0, 0.0, 5.0]),
};
let json = serde_json::to_string(&player)?;
// {"name":"Bear","level":15,"health":87.5,"items":["AK-47","Bandage"],"position":[10.0,0.0,5.0]}
// 反序列化
let player: Player = serde_json::from_str(&json)?;
支持的类型
基本类型: bool, i8~i128, u8~u128, f32, f64, char, String, &str
容器: Vec<T>, HashMap<K,V>, HashSet<T>, BTreeMap, VecDeque
Option: Option<T> → JSON null / 值
元组: (A, B, C) → JSON 数组
数组: [T; N] → JSON 数组
枚举: 见下文
嵌套结构体: 递归序列化
Box/Arc/Rc: 透明序列化内部值
#[serde(…)] 属性详解
容器属性(加在 struct/enum 上)
// 字段命名风格转换
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // Rust snake_case → JSON camelCase
struct ApiResponse {
status_code: u16, // → "statusCode"
error_message: String, // → "errorMessage"
player_name: String, // → "playerName"
}
// 可用的命名风格:
// "camelCase" → statusCode
// "PascalCase" → StatusCode
// "snake_case" → status_code(Rust 默认,通常不需要)
// "SCREAMING_SNAKE_CASE" → STATUS_CODE
// "kebab-case" → status-code
// 反序列化时拒绝未知字段(默认是忽略)
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Config {
host: String,
port: u16,
}
// 如果 JSON 里有 {"host":"...", "port":8080, "typo_field": true} → 报错
字段属性(加在字段上)
#[derive(Serialize, Deserialize)]
struct Player {
// 重命名单个字段
#[serde(rename = "playerId")]
id: u32,
// 反序列化时如果字段缺失,用默认值
#[serde(default)]
level: u32, // 缺失时 = 0(u32 的 Default)
// 自定义默认值
#[serde(default = "default_health")]
health: f32,
// 序列化时跳过这个字段
#[serde(skip_serializing)]
password_hash: String,
// 反序列化时跳过(用默认值)
#[serde(skip_deserializing)]
computed_score: f64,
// 双向都跳过
#[serde(skip)]
cache: Option<Vec<u8>>,
// 如果值是 None 就不输出这个字段(而不是输出 null)
#[serde(skip_serializing_if = "Option::is_none")]
nickname: Option<String>,
// 扁平化嵌套结构
#[serde(flatten)]
metadata: Metadata,
}
fn default_health() -> f32 { 100.0 }
#[derive(Serialize, Deserialize)]
struct Metadata {
created_at: String,
updated_at: String,
}
// flatten 效果: metadata 的字段直接出现在 Player 的 JSON 中
// {"playerId":1,"level":15,"health":100.0,"created_at":"...","updated_at":"..."}
// 而不是 {"playerId":1,"metadata":{"created_at":"...","updated_at":"..."}}
枚举序列化方式
// 外部标签(默认)
#[derive(Serialize, Deserialize)]
enum GameEvent {
PlayerJoined { player_id: u32 },
ItemDropped { item_id: u32, position: [f32; 3] },
MatchEnded,
}
// → {"PlayerJoined":{"player_id":42}}
// → {"ItemDropped":{"item_id":1,"position":[1.0,2.0,3.0]}}
// → "MatchEnded"
// 内部标签(推荐用于 API)
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum GameEvent {
PlayerJoined { player_id: u32 },
ItemDropped { item_id: u32, position: [f32; 3] },
MatchEnded,
}
// → {"type":"PlayerJoined","player_id":42}
// → {"type":"ItemDropped","item_id":1,"position":[1.0,2.0,3.0]}
// → {"type":"MatchEnded"}
// 相邻标签
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum GameEvent {
PlayerJoined { player_id: u32 },
Damage(u32),
}
// → {"type":"PlayerJoined","data":{"player_id":42}}
// → {"type":"Damage","data":50}
// 无标签(按内容匹配)
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
Int(i64),
Float(f64),
Text(String),
}
// → 42 (尝试 Int → 成功)
// → 3.14 (尝试 Int → 失败,尝试 Float → 成功)
// → "hello" (尝试 Int → 失败,Float → 失败,Text → 成功)
格式 crate 对比
serde_json — JSON
serde_json = "1"
use serde_json::{json, Value};
// 序列化
let s = serde_json::to_string(&player)?; // 紧凑
let s = serde_json::to_string_pretty(&player)?; // 格式化
// 反序列化
let p: Player = serde_json::from_str(&json_str)?;
let p: Player = serde_json::from_slice(&bytes)?; // 从字节切片
let p: Player = serde_json::from_reader(file)?; // 从 Read
// 动态 JSON(不需要结构体)
let v: Value = serde_json::from_str(r#"{"key": "value"}"#)?;
let key = v["key"].as_str().unwrap();
// json! 宏构造动态值
let v = json!({
"error": {"code": "NOT_FOUND", "message": "player not found"}
});
用在哪:HTTP API 请求响应、配置文件、日志结构化输出。
bincode — 紧凑二进制
bincode = "1"
// 序列化
let bytes: Vec<u8> = bincode::serialize(&player)?;
// 比 JSON 小很多:Player 大约 20 字节 vs JSON 100+ 字节
// 反序列化
let player: Player = bincode::deserialize(&bytes)?;
特点:
- 极小体积(无字段名、无分隔符,纯数据)
- 极快速度(接近 memcpy)
- 不自描述(接收方必须知道结构体定义才能解码)
- 不跨语言(格式是 Rust 专有的)
用在哪:游戏网络协议(UDP 状态同步)、进程间通信、缓存到 Redis 的二进制值。
toml — TOML 配置
toml = "0.8"
#[derive(Serialize, Deserialize)]
struct Config {
server: ServerConfig,
database: DatabaseConfig,
}
#[derive(Serialize, Deserialize)]
struct ServerConfig {
host: String,
port: u16,
}
#[derive(Serialize, Deserialize)]
struct DatabaseConfig {
url: String,
max_connections: u32,
}
// 读取配置文件
let content = std::fs::read_to_string("config.toml")?;
let config: Config = toml::from_str(&content)?;
# config.toml
[server]
host = "0.0.0.0"
port = 3000
[database]
url = "postgres://tarkov:123@localhost/mini_tarkov"
max_connections = 20
用在哪:Rust 项目配置文件(Cargo.toml 自己就是 TOML)。
postcard — 嵌入式 / 零拷贝二进制
postcard = { version = "1", features = ["alloc"] }
let bytes = postcard::to_allocvec(&player)?;
let player: Player = postcard::from_bytes(&bytes)?;
特点:比 bincode 更紧凑(varint 编码),支持 #![no_std]。
用在哪:嵌入式设备通信、对包体积极致要求的场景。
rmp-serde — MessagePack
rmp-serde = "1"
let bytes = rmp_serde::to_vec(&player)?;
let player: Player = rmp_serde::from_slice(&bytes)?;
特点:二进制 JSON——比 JSON 小,比 bincode 跨语言。自描述(带字段名)。
用在哪:需要跨语言 + 比 JSON 高效的场景。
格式选型对比
体积 速度 人类可读 跨语言 自描述 用在哪
─────────────────────────────────────────────────────────────────
serde_json 大 中等 ✓ ✓ ✓ HTTP API
bincode 极小 极快 ✗ ✗ ✗ Rust 内部通信/游戏协议
toml 中 慢 ✓ ✓ ✓ 配置文件
postcard 极小 快 ✗ △ ✗ 嵌入式
rmp(MsgPack) 小 快 ✗ ✓ ✓ 跨语言高效通信
protobuf 小 快 ✗ ✓ ✓ gRPC / 严格 schema
mini_tarkov 的选择:
- HTTP/gRPC API → serde_json(客户端交互)或 protobuf(tonic gRPC)
- UDP 状态同步 → bincode(极小极快,两端都是 Rust 就够了)
- 配置文件 → toml
- Redis 缓存值 → bincode 或 serde_json(取决于是否需要用 redis-cli 人工查看)
高级用法
自定义序列化(单个字段)
use serde::{Serialize, Deserialize, Serializer, Deserializer};
use chrono::{DateTime, Utc};
#[derive(Serialize, Deserialize)]
struct Event {
name: String,
// 自定义:DateTime 序列化为 Unix 时间戳
#[serde(serialize_with = "serialize_timestamp")]
#[serde(deserialize_with = "deserialize_timestamp")]
time: DateTime<Utc>,
}
fn serialize_timestamp<S: Serializer>(dt: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_i64(dt.timestamp())
}
fn deserialize_timestamp<'de, D: Deserializer<'de>>(d: D) -> Result<DateTime<Utc>, D::Error> {
let ts = i64::deserialize(d)?;
DateTime::from_timestamp(ts, 0)
.ok_or_else(|| serde::de::Error::custom("invalid timestamp"))
}
// → {"name":"match_started","time":1716300000}
// 而不是 {"name":"match_started","time":"2024-05-21T10:00:00Z"}
DeserializeOwned vs Deserialize<‘de>
// Deserialize<'de> — 可以借用输入数据(零拷贝)
fn parse<'a>(json: &'a str) -> Result<Player<'a>, Error>
// Player 里的 &str 字段直接指向 json 字符串的内存,不复制
// DeserializeOwned — 不借用输入,结果拥有所有数据
fn parse(json: &str) -> Result<Player, Error>
// Player 里是 String,独立拥有数据,json 可以随时释放
// axum 的 Json<T> 要求 T: DeserializeOwned
// 因为请求 body 读完后 buffer 就释放了,T 不能借用它