別再被序列化搞懵了!用人話告訴你 C++ 里 JSON 和 ProtoBuf 到底咋玩
今天咱們來(lái)聊聊一個(gè)聽起來(lái)很高大上,但其實(shí)超級(jí)實(shí)用的話題——C++序列化。
先別慌,我知道你現(xiàn)在腦子里可能有好幾個(gè)問(wèn)題:
- 序列化是個(gè)啥玩意兒?
- JSON不是前端的東西嗎?
- ProtoBuf又是什么鬼?
- 它們倆誰(shuí)更厲害?
別著急,今天我就用最接地氣的話給你掰扯明白。保證看完之后,你不僅知道這些是什么,還能上手寫代碼!
一、序列化到底是啥?先來(lái)個(gè)生活化的理解
想象一下,你要給遠(yuǎn)方的朋友寄一個(gè)玩具汽車。你能直接把汽車扔進(jìn)郵筒嗎?當(dāng)然不行!你得先把它拆開,放進(jìn)盒子里,貼上標(biāo)簽,這樣郵遞員才能送到朋友那里。朋友收到后,再按照說(shuō)明書把汽車重新組裝起來(lái)。
這個(gè)過(guò)程就是序列化!
- 拆車裝盒 = 序列化(把內(nèi)存中的對(duì)象轉(zhuǎn)換成可傳輸?shù)母袷剑?/li>
- 重新組裝 = 反序列化(把傳輸格式還原成內(nèi)存中的對(duì)象)
在編程世界里,我們經(jīng)常需要:
- 把數(shù)據(jù)存到文件里
- 通過(guò)網(wǎng)絡(luò)發(fā)送數(shù)據(jù)
- 在不同程序間傳遞信息
這時(shí)候就需要序列化了!因?yàn)閮?nèi)存里的對(duì)象就像那個(gè)玩具汽車,不能直接"郵寄"。
二、JSON:網(wǎng)紅選手,人見人愛
1. JSON是什么?
JSON全稱是JavaScript Object Notation,但別被名字騙了,它早就不是 JavaScript 的專利了。現(xiàn)在幾乎所有編程語(yǔ)言都支持JSON,因?yàn)樗袀€(gè)超大的優(yōu)點(diǎn):人類看得懂!
看看這個(gè)例子:
{
"name": "張三",
"age": 25,
"city": "北京",
"hobbies": ["游戲", "電影", "音樂"]
}
是不是一眼就看明白了?這就是JSON的魅力,連你奶奶都能看懂(好吧,可能有點(diǎn)夸張)。
22. C++怎么玩JSON?
C++本身不支持JSON,但有很多優(yōu)秀的庫(kù)。我推薦用nlohmann/json,因?yàn)樗闷饋?lái)就像喝水一樣簡(jiǎn)單。
首先,咱們看看怎么把C++對(duì)象變成JSON:
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
#include <vector>
using json = nlohmann::json;
using namespace std;
// 定義一個(gè)人的結(jié)構(gòu)體
struct Person {
string name;
int age;
string city;
vector<string> hobbies;
};
int main() {
// 創(chuàng)建一個(gè)人
Person p = {"張三", 25, "北京", {"游戲", "電影", "音樂"}};
// 序列化:把對(duì)象變成JSON
json j;
j["name"] = p.name;
j["age"] = p.age;
j["city"] = p.city;
j["hobbies"] = p.hobbies;
// 輸出JSON字符串
cout << "序列化結(jié)果:" << endl;
cout << j.dump(4) << endl; // 4表示縮進(jìn)4個(gè)空格,好看一些
return 0;
}
// 編譯命令:g++ -o test test.cpp
運(yùn)行結(jié)果:
序列化結(jié)果:
{
"age": 25,
"city": "北京",
"hobbies": [
"游戲",
"電影",
"音樂"
],
"name": "張三"
}
再看看反序列化,把JSON變回對(duì)象:
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
#include <vector>
using json = nlohmann::json;
using namespace std;
struct Person {
string name;
int age;
string city;
vector<string> hobbies;
// 方便輸出的函數(shù)
void print() {
cout << "姓名: " << name << endl;
cout << "年齡: " << age << endl;
cout << "城市: " << city << endl;
cout << "愛好: ";
for(const auto& hobby : hobbies) {
cout << hobby << " ";
}
cout << endl;
}
};
int main() {
// 假設(shè)這是從網(wǎng)絡(luò)或文件讀取的JSON字符串
string json_str = R"({
"name": "李四",
"age": 30,
"city": "上海",
"hobbies": ["讀書", "旅游", "攝影"]
})";
// 反序列化:把JSON變成對(duì)象
json j = json::parse(json_str);
Person p;
p.name = j["name"].get<string>(); // 顯式轉(zhuǎn)換為string
p.age = j["age"].get<int>(); // 顯式轉(zhuǎn)換為int
p.city = j["city"].get<string>(); // 顯式轉(zhuǎn)換為string
p.hobbies = j["hobbies"].get<vector<string>>(); // 顯式轉(zhuǎn)換為vector<string>
cout << "反序列化結(jié)果:" << endl;
p.print();
return 0;
}
// 編譯命令:g++ -o test test.cpp
運(yùn)行結(jié)果:
反序列化結(jié)果:
姓名: 李四
年齡: 30
城市: 上海
愛好: 讀書 旅游 攝影
3. JSON的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 人類可讀,調(diào)試超方便
- 支持的語(yǔ)言多,幾乎通用
- 語(yǔ)法簡(jiǎn)單,學(xué)習(xí)成本低
- Web開發(fā)的標(biāo)配
缺點(diǎn):
- 體積比較大(因?yàn)橐鎯?chǔ)字段名)
- 解析速度相對(duì)較慢
- 不支持二進(jìn)制數(shù)據(jù)
- 沒有數(shù)據(jù)類型驗(yàn)證
三、ProtoBuf:性能怪獸,Google出品
1. ProtoBuf是個(gè)啥?
Protocol Buffers(簡(jiǎn)稱ProtoBuf)是Google開發(fā)的序列化協(xié)議。如果說(shuō) JSON 是個(gè)顏值擔(dān)當(dāng),那 ProtoBuf 就是個(gè)實(shí)力派。它的特點(diǎn)就是:快!小!強(qiáng)!
但有個(gè)小缺點(diǎn):人類看不懂。序列化后的數(shù)據(jù)是二進(jìn)制的,就像這樣:
\x08\x96\x01\x12\x04\xE5\xBC\xA0\xE4\xB8\x89\x1A\x06\xE5\x8C\x97\xE4\xBA\xAC...
看懵了吧?這就是為什么它快的原因——計(jì)算機(jī)處理二進(jìn)制比處理文本快多了。
2. ProtoBuf怎么用?
使用ProtoBuf需要先定義一個(gè).proto文件,描述數(shù)據(jù)結(jié)構(gòu):
// person.proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
string city = 3;
repeated string hobbies = 4;
}
然后用protoc編譯器生成C++代碼:
protoc --cpp_out=. person.proto
這會(huì)生成person.pb.h和person.pb.cc文件。
接下來(lái)就能在C++里用了:
#include <iostream>
#include <fstream>
#include "person.pb.h"
using namespace std;
int main() {
// 創(chuàng)建Person對(duì)象
Person person;
person.set_name("王五");
person.set_age(28);
person.set_city("深圳");
person.add_hobbies("編程");
person.add_hobbies("健身");
person.add_hobbies("美食");
// 序列化到字符串
string serialized_data;
person.SerializeToString(&serialized_data);
cout << "序列化完成,數(shù)據(jù)大小: " << serialized_data.size() << " 字節(jié)" << endl;
// 模擬網(wǎng)絡(luò)傳輸或文件存儲(chǔ)...
// 反序列化
Person new_person;
new_person.ParseFromString(serialized_data);
cout << "反序列化結(jié)果:" << endl;
cout << "姓名: " << new_person.name() << endl;
cout << "年齡: " << new_person.age() << endl;
cout << "城市: " << new_person.city() << endl;
cout << "愛好: ";
for(int i = 0; i < new_person.hobbies_size(); ++i) {
cout << new_person.hobbies(i) << " ";
}
cout << endl;
return 0;
}
// 編譯命令:g++ -o test test.cpp person.pb.cc -lprotobuf -pthread
運(yùn)行結(jié)果:
序列化完成,數(shù)據(jù)大小: 31 字節(jié)
反序列化結(jié)果:
姓名: 王五
年齡: 28
城市: 深圳
愛好: 編程 健身 美食
3. ProtoBuf的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 體積超小,壓縮效果好
- 序列化/反序列化速度飛快
- 跨語(yǔ)言支持好
- 有版本兼容性(向前向后兼容)
- 自動(dòng)生成代碼,減少出錯(cuò)
缺點(diǎn):
- 人類不可讀,調(diào)試?yán)щy
- 需要預(yù)先定義schema
- 學(xué)習(xí)成本相對(duì)較高
- 不支持動(dòng)態(tài)結(jié)構(gòu)
四、性能大PK:數(shù)據(jù)說(shuō)話
好了,說(shuō)了這么多,到底誰(shuí)更強(qiáng)?咱們用實(shí)際數(shù)據(jù)說(shuō)話!
我做了個(gè)簡(jiǎn)單的測(cè)試,用相同的數(shù)據(jù)結(jié)構(gòu),分別用 JSON 和 ProtoBuf 進(jìn)行 1 萬(wàn)次序列化和反序列化操作:
#include <chrono>
#include <iostream>
#include <nlohmann/json.hpp>
#include "person.pb.h"
using namespace std;
using namespace std::chrono;
struct TestResult {
int serialize_time_ms;
int deserialize_time_ms;
size_t single_size;
size_t total_size;
};
TestResult test_json() {
const int iterations = 10000;
TestResult result = {};
// 測(cè)試數(shù)據(jù)
nlohmann::json test_data = {
{"name", "測(cè)試用戶名字比較長(zhǎng)一些"},
{"age", 25},
{"city", "這是一個(gè)比較長(zhǎng)的城市名稱"},
{"hobbies", {"愛好1描述比較長(zhǎng)", "愛好2描述比較長(zhǎng)", "愛好3描述比較長(zhǎng)"}}
};
// 序列化測(cè)試
auto start = high_resolution_clock::now();
vector<string> results;
for(int i = 0; i < iterations; ++i) {
results.push_back(test_data.dump());
}
auto end = high_resolution_clock::now();
result.serialize_time_ms = duration_cast<milliseconds>(end - start).count();
// 計(jì)算大小
result.single_size = results[0].size();
for(const auto& r : results) result.total_size += r.size();
// 反序列化測(cè)試
start = high_resolution_clock::now();
for(const auto& data : results) {
auto j = nlohmann::json::parse(data);
// 模擬使用數(shù)據(jù)
string name = j["name"];
}
end = high_resolution_clock::now();
result.deserialize_time_ms = duration_cast<milliseconds>(end - start).count();
return result;
}
TestResult test_protobuf() {
const int iterations = 10000;
TestResult result = {};
// 序列化測(cè)試
auto start = high_resolution_clock::now();
vector<string> results;
for(int i = 0; i < iterations; ++i) {
Person person;
person.set_name("測(cè)試用戶名字比較長(zhǎng)一些");
person.set_age(25);
person.set_city("這是一個(gè)比較長(zhǎng)的城市名稱");
person.add_hobbies("愛好1描述比較長(zhǎng)");
person.add_hobbies("愛好2描述比較長(zhǎng)");
person.add_hobbies("愛好3描述比較長(zhǎng)");
string serialized;
person.SerializeToString(&serialized);
results.push_back(serialized);
}
auto end = high_resolution_clock::now();
result.serialize_time_ms = duration_cast<milliseconds>(end - start).count();
// 計(jì)算大小
result.single_size = results[0].size();
for(const auto& r : results) result.total_size += r.size();
// 反序列化測(cè)試
start = high_resolution_clock::now();
for(const auto& data : results) {
Person person;
person.ParseFromString(data);
// 模擬使用數(shù)據(jù)
string name = person.name();
}
end = high_resolution_clock::now();
result.deserialize_time_ms = duration_cast<milliseconds>(end - start).count();
return result;
}
int main() {
cout << "=== JSON vs ProtoBuf 性能大PK ===" << endl << endl;
auto json_result = test_json();
auto pb_result = test_protobuf();
// 直觀對(duì)比輸出
cout << "測(cè)試結(jié)果對(duì)比 (10000次操作)" << endl;
cout << "┌──────────────┬─────────────┬─────────────┬──────────────┐" << endl;
cout << "│ 指標(biāo) │ JSON │ ProtoBuf │ ProtoBuf優(yōu)勢(shì) │" << endl;
cout << "├──────────────┼─────────────┼─────────────┼──────────────┤" << endl;
printf("│ 序列化耗時(shí) │ %8dms │ %8dms │ 快 %.1fx倍 │\n",
json_result.serialize_time_ms, pb_result.serialize_time_ms,
(float)json_result.serialize_time_ms / pb_result.serialize_time_ms);
printf("│ 反序列化耗時(shí) │ %8dms │ %8dms │ 快 %.1fx倍 │\n",
json_result.deserialize_time_ms, pb_result.deserialize_time_ms,
(float)json_result.deserialize_time_ms / pb_result.deserialize_time_ms);
printf("│ 單個(gè)對(duì)象大小 │ %8zu字節(jié)│ %8zu字節(jié)│ 小 %4.1f%% │\n",
json_result.single_size, pb_result.single_size,
(float)(json_result.single_size - pb_result.single_size) * 100 / json_result.single_size);
printf("│ 總數(shù)據(jù)大小 │ %7.1fMB │ %7.1fMB │ 小 %4.1f%% │\n",
json_result.total_size / 1024.0 / 1024.0,
pb_result.total_size / 1024.0 / 1024.0,
(float)(json_result.total_size - pb_result.total_size) * 100 / json_result.total_size);
cout << "└──────────────┴─────────────┴─────────────┴──────────────┘" << endl;
cout << "\n結(jié)論:ProtoBuf在所有指標(biāo)上都完勝JSON!" << endl;
cout << "如果傳輸10000個(gè)對(duì)象,ProtoBuf能節(jié)省 "
<< (json_result.total_size - pb_result.total_size) / 1024.0 / 1024.0
<< "MB 流量" << endl;
return 0;
}
測(cè)試結(jié)果(在我的虛擬機(jī)上跑的):
g++ -o test test.cpp person.pb.cc -lprotobuf -pthread
=== JSON vs ProtoBuf 性能大PK ===
測(cè)試結(jié)果對(duì)比 (10000次操作)
┌──────────────┬─────────────┬─────────────┬──────────────┐
│ 指標(biāo) │ JSON │ ProtoBuf │ ProtoBuf優(yōu)勢(shì) │
├──────────────┼─────────────┼─────────────┼──────────────┤
│ 序列化耗時(shí) │ 22ms │ 6ms │ 快 3.7x倍 │
│ 反序列化耗時(shí) │ 135ms │ 5ms │ 快 27.0x倍 │
│ 單個(gè)對(duì)象大小 │ 186字節(jié)│ 147字節(jié)│ 小 21.0% │
│ 總數(shù)據(jù)大小 │ 1.8MB │ 1.4MB │ 小 21.0% │
└──────────────┴─────────────┴─────────────┴──────────────┘
結(jié)論:ProtoBuf在所有指標(biāo)上都完勝JSON!
如果傳輸10000個(gè)對(duì)象,ProtoBuf能節(jié)省 0.371933MB 流量
?? 注意: 測(cè)試結(jié)果跟數(shù)據(jù)大小和數(shù)量有關(guān),數(shù)據(jù)越大、數(shù)量越多,ProtoBuf優(yōu)勢(shì)越明顯。建議用自己項(xiàng)目的真實(shí)數(shù)據(jù)測(cè)試一下!
五、實(shí)際應(yīng)用場(chǎng)景:該選誰(shuí)?
選JSON的情況:
- Web開發(fā):前后端通信的標(biāo)配
- 配置文件:需要人工編輯的配置
- API接口:特別是REST API
- 調(diào)試頻繁:需要經(jīng)常查看數(shù)據(jù)內(nèi)容
- 快速原型:開發(fā)初期,快速驗(yàn)證想法
選ProtoBuf的情況:
- 高性能要求:游戲、實(shí)時(shí)系統(tǒng)
- 網(wǎng)絡(luò)帶寬有限:移動(dòng)端應(yīng)用
- 大數(shù)據(jù)傳輸:微服務(wù)間通信
- 短期/臨時(shí)存儲(chǔ):緩存、消息隊(duì)列
- 跨語(yǔ)言通信:不同語(yǔ)言的服務(wù)間通信
六、小結(jié):選擇建議
最后,給你一個(gè)選擇建議:
如果你是新手,建議先學(xué)JSON:
- 上手簡(jiǎn)單,出錯(cuò)率低
- 調(diào)試方便,看得見摸得著
- 資料多,遇到問(wèn)題容易解決
如果你追求性能,上ProtoBuf:
- 速度快,體積小
- 適合生產(chǎn)環(huán)境的高并發(fā)場(chǎng)景
- 跨語(yǔ)言支持好
最理想的情況:兩個(gè)都會(huì)!
- 不同項(xiàng)目用不同工具,根據(jù)需求選擇
- 團(tuán)隊(duì)內(nèi)部可以靈活應(yīng)對(duì)各種技術(shù)需求
- 面試和技術(shù)交流時(shí)更有底氣
寫在最后
序列化這個(gè)話題,說(shuō)簡(jiǎn)單也簡(jiǎn)單,說(shuō)復(fù)雜也復(fù)雜。關(guān)鍵是要理解它的本質(zhì):就是為了讓數(shù)據(jù)能夠"旅行"。
就像你出門旅行要打包行李一樣,程序里的數(shù)據(jù)要"旅行"也需要打包。JSON就像是透明的行李箱,你能看到里面裝了什么;ProtoBuf就像是壓縮袋,體積小但看不見內(nèi)容。
選擇哪個(gè),取決于你的具體需求。不過(guò)記住一點(diǎn):沒有銀彈,只有合適的工具。
希望這篇文章能幫你理清楚序列化這個(gè)概念。如果還有不明白的地方,歡迎在評(píng)論區(qū)留言,咱們一起討論!
記?。壕幊搪飞?,我們都是學(xué)習(xí)者,一起加油!