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