从登录场景看通用序列化协议:JSON 与 Protobuf 实践
文章目录
- 引言:为什么需要通用序列化协议?
- 一、JSON 序列化:文本协议的直观实践
- 1.1 环境准备:选择 nlohmann/json 库
- 1.2 LoginRequest 的 JSON 序列化与反序列化
- 1.3 LoginResponse 的 JSON 序列化与反序列化
- 1.4 客户端与服务端的 JSON 适配修改
- 客户端修改(核心逻辑)
- 服务端修改(核心逻辑)
- 二. Protobuf 序列化:二进制协议的高效实现
- 2.1 第一步:定义 .proto 协议文件
- 2.2 第二步:编译 .proto 生成代码
- 编译命令(C++ 为例)
- 2.3 第三步:C++ 中使用 Protobuf 处理登录数据
- 客户端核心逻辑(发送登录请求)
- 服务端核心逻辑(处理登录请求)
- 三、JSON 与 Protobuf 登录场景对比
- 四、总结
引言:为什么需要通用序列化协议?
在上一篇博客中,我们通过手写序列化实现了 TCP 登录场景的数据传输,解决了字节序、数据边界等核心问题。但手写方案存在明显局限:仅适用于特定场景(如登录),若新增 “注册”“修改密码” 等业务,需重复开发序列化逻辑,且跨语言通信(如 C++ 服务端与 Java 客户端)时兼容性极差。
通用序列化协议正是为解决这些问题而生 —— 它们预先定义了数据的编码 / 解码规则,支持多语言、多场景复用,无需手动处理字节序和字段边界。本文将以登录场景为核心,分别介绍两种最常用的通用协议:
- JSON:文本格式,易读易调试,适合对性能要求不极致的场景(如 Web 接口、简单客户端 - 服务端通信);
- Protobuf:Google 推出的二进制协议,体积小、解析快,适合高性能网络通信(如游戏、物联网设备)。
一、JSON 序列化:文本协议的直观实践
JSON(JavaScript Object Notation)是一种轻量级文本序列化格式,基于键值对结构,天然支持字符串、数字、布尔等基础类型,人类可直接阅读,是开发中最易上手的协议之一。
1.1 环境准备:选择 nlohmann/json 库
C++ 本身没有内置 JSON 处理能力,我们选择 nlohmann/json 库 —— 它是单头文件库(无需编译链接),API 直观,完全兼容 C++11 及以上标准,最适合入门实践。
- 下载地址:GitHub 仓库
- 使用方式:将
json.hpp头文件复制到项目目录,直接#include "json.hpp"即可。
1.2 LoginRequest 的 JSON 序列化与反序列化
登录请求(LoginRequest)包含 username(用户名)和 password(密码)两个字符串字段。我们基于 nlohmann/json 实现其序列化(对象→JSON 字符串)和反序列化(JSON 字符串→对象)。
#include "json.hpp"
#include <string>// 引入 nlohmann 命名空间,简化代码
using json = nlohmann::json;class LoginRequest {
public:LoginRequest() = default;LoginRequest(const std::string& username, const std::string& password): _username(username), _password(password) {}// 1. 序列化:LoginRequest 对象 → JSON 字符串std::string SerializeToJson() const {// 构建 JSON 对象(键值对对应类成员)json json_obj;json_obj["username"] = _username; // 键名可自定义,需与反序列化一致json_obj["password"] = _password;// 将 JSON 对象转为字符串(dump() 无参数时生成紧凑格式,便于网络传输)return json_obj.dump();}// 2. 反序列化:JSON 字符串 → LoginRequest 对象bool DeserializeFromJson(const std::string& json_str) {try {// 解析 JSON 字符串为 JSON 对象json json_obj = json::parse(json_str);// 从 JSON 对象中提取字段(需确保键名存在,否则会抛异常)_username = json_obj.at("username").get<std::string>();_password = json_obj.at("password").get<std::string>();return true; // 解析成功} catch (const json::exception& e) {// 捕获解析异常(如 JSON 格式错误、键不存在)std::cerr << "JSON 反序列化失败:" << e.what() << std::endl;return false;}}// Getter(与上一篇保持一致,便于服务端获取数据)std::string GetUserName() const { return _username; }std::string GetPassWord() const { return _password; }private:std::string _username;std::string _password;
};
关键说明:
- JSON 序列化无需处理字节序:因为 JSON 是文本格式,数据以 ASCII 字符传输,所有平台解析规则一致;
- 异常处理:
json::parse()和at()方法在格式错误或键不存在时会抛异常,必须捕获以避免程序崩溃; - 字段映射:JSON 的键名(如 “username”)需与反序列化时的键名严格一致,否则会解析失败。
1.3 LoginResponse 的 JSON 序列化与反序列化
登录响应(LoginResponse)包含 status_code(状态码:0 成功,非 0 失败)、msg(提示消息)、token(登录成功时的令牌)三个字段,实现逻辑与 LoginRequest 一致。
class LoginResponse {
public:LoginResponse() = default;LoginResponse(uint32_t status_code, const std::string& msg, const std::string& token): _status_code(status_code), _msg(msg), _token(token) {}// 序列化:LoginResponse → JSON 字符串std::string SerializeToJson() const {json json_obj;json_obj["status_code"] = _status_code; // 数字类型直接赋值json_obj["msg"] = _msg;json_obj["token"] = _token;return json_obj.dump();}// 反序列化:JSON 字符串 → LoginResponsebool DeserializeFromJson(const std::string& json_str) {try {json json_obj = json::parse(json_str);_status_code = json_obj.at("status_code").get<uint32_t>(); // 提取数字类型_msg = json_obj.at("msg").get<std::string>();_token = json_obj.at("token").get<std::string>();return true;} catch (const json::exception& e) {std::cerr << "JSON 反序列化失败:" << e.what() << std::endl;return false;}}// Getter(便于客户端获取响应结果)uint32_t GetStatusCode() const { return _status_code; }std::string GetMsg() const { return _msg; }std::string GetToken() const { return _token; }private:uint32_t _status_code = 0;std::string _msg;std::string _token;
};
1.4 客户端与服务端的 JSON 适配修改
基于上述 JSON 序列化逻辑,我们只需简单修改上一篇的客户端和服务端代码,即可替换手写序列化方案。
客户端修改(核心逻辑)
客户端需构建 LoginRequest 对象,序列化为 JSON 字符串后通过 TCP 发送:
// 1. 获取用户输入(用户名、密码)
std::string username, password;
std::cout << "请输入用户名:";
std::getline(std::cin, username);
std::cout << "请输入密码:";
std::getline(std::cin, password);// 2. 构建 LoginRequest 并序列化为 JSON 字符串
LoginRequest login_req(username, password);
std::string req_json = login_req.SerializeToJson();// 3. 发送 JSON 数据(TCP 发送逻辑与上一篇一致,直接发送字符串即可)
// 注意:JSON 是文本,无需额外处理长度(服务端可按 TCP 粘包处理逻辑读取完整数据)
send(client_fd, req_json.c_str(), req_json.size(), 0);// 4. 接收服务端响应并反序列化为 LoginResponse
char resp_buf[1024] = {0};
ssize_t recv_len = recv(client_fd, resp_buf, sizeof(resp_buf)-1, 0);
if (recv_len > 0) {LoginResponse login_resp;if (login_resp.DeserializeFromJson(std::string(resp_buf, recv_len))) {std::cout << "登录结果:" << login_resp.GetMsg() << std::endl;if (login_resp.GetStatusCode() == 0) {std::cout << "Token:" << login_resp.GetToken() << std::endl;}}
}
服务端修改(核心逻辑)
服务端接收 JSON 字符串后,反序列化为 LoginRequest,验证后构建 LoginResponse 并序列化为 JSON 发送:
// 服务端数据处理函数(替换上一篇的手写序列化处理)
std::string LoginJsonHandler(const std::string& client_data) {// 1. 反序列化客户端 JSON 数据为 LoginRequestLoginRequest req;if (!req.DeserializeFromJson(client_data)) {// 解析失败,返回错误响应return LoginResponse(1, "无效的 JSON 请求格式", "").SerializeToJson();}// 2. 模拟登录验证(与上一篇逻辑一致)bool auth_success = (req.GetUserName() == "admin" && req.GetPassWord() == "123456");// 3. 构建响应并序列化为 JSONLoginResponse resp;if (auth_success) {resp = LoginResponse(0, "登录成功", "fake_token_" + std::to_string(rand()%10000));} else {resp = LoginResponse(2, "用户名或密码错误", "");}return resp.SerializeToJson();
}
二. Protobuf 序列化:二进制协议的高效实现
Protobuf(Protocol Buffers)是 Google 开源的二进制序列化协议,通过自定义 “协议文件”(.proto)定义数据结构,再通过编译器生成对应语言的代码,最终实现高效的序列化与反序列化。
Protobuf 的核心优势是体积小(二进制比 JSON 文本小 30%-50%)和解析快(二进制解析无需文本解析的字符处理逻辑),但缺点是不可读(需工具解析二进制数据)。
2.1 第一步:定义 .proto 协议文件
.proto 文件是 Protobuf 的 “契约”,用于描述数据结构(如 LoginRequest 和 LoginResponse 的字段名、类型、编号)。文件名通常为 xxx.proto,我们命名为 login.proto。
// 1. 指定 Protobuf 版本(必须写在最顶部,v3 是当前主流版本)
syntax = "proto3";// 2. 指定生成代码的命名空间(C++ 专用,避免类名冲突)
option csharp_namespace = "LoginProto";// 3. 定义登录请求消息(message 对应 C++ 中的类)
message LoginRequest {// 字段格式:类型 字段名 = 字段编号;// 字段编号:1-15 占用1字节,16-2047 占用2字节,建议常用字段用小编号string username = 1; // 用户名string password = 2; // 密码
}// 4. 定义登录响应消息
message LoginResponse {uint32 status_code = 1; // 状态码(0成功,非0失败)string msg = 2; // 提示消息string token = 3; // 登录令牌(成功时返回)
}
关键语法说明:
syntax = "proto3":指定使用 Protobuf 3 版本,若不写默认是 v2,语法有差异;message:定义数据结构,生成 C++ 代码时会对应一个类(如LoginRequest类);- 字段编号:必须唯一,且一旦确定不能修改(否则会破坏兼容性),用于二进制数据中标识字段,与字段名无关;
- 支持的基础类型:
string(字符串)、uint32(无符号 32 位整数)、int32(有符号 32 位整数)、bool(布尔值)等。
2.2 第二步:编译 .proto 生成代码
Protobuf 提供编译器 protoc,需先安装编译器(根据系统选择安装方式):
- Windows:从 Protobuf 官网 下载编译好的
protoc.exe; - Linux:通过包管理安装
sudo apt-get install protobuf-compiler;
编译命令(C++ 为例)
在 .proto 文件所在目录执行以下命令,生成 C++ 头文件和源文件:
# 命令格式:protoc --cpp_out=输出目录 协议文件名
protoc --cpp_out=./ login.proto
执行后会生成两个文件:
login.pb.h:头文件,包含LoginRequest和LoginResponse类的声明;login.pb.cc:源文件,包含序列化、反序列化等方法的实现。
将这两个文件加入项目,编译时需链接 Protobuf 库(Linux 下链接 libprotobuf.so,Windows 下链接 libprotobuf.lib)。
2.3 第三步:C++ 中使用 Protobuf 处理登录数据
Protobuf 生成的类已内置序列化(SerializeToString)和反序列化(ParseFromString)方法,无需手动实现,直接调用即可。
客户端核心逻辑(发送登录请求)
#include "login.pb.h" // 包含生成的 Protobuf 头文件
#include <iostream>
#include <string>
#include <sys/socket.h> // TCP 相关头文件(Linux 为例)
#include <arpa/inet.h>int main() {// 1. 创建 TCP 客户端(逻辑与上一篇一致,略)int client_fd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888);inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));// 2. 获取用户输入,构建 Protobuf 的 LoginRequest 对象std::string username, password;std::cout << "请输入用户名:";std::getline(std::cin, username);std::cout << "请输入密码:";std::getline(std::cin, password);LoginRequest login_req;// 设置字段值(生成的类提供 set_字段名() 方法)login_req.set_username(username);login_req.set_password(password);// 3. 序列化:Protobuf 对象 → 二进制字符串(非文本,不可读)std::string req_data;login_req.SerializeToString(&req_data); // 结果存入 req_datastd::cout << "Protobuf 序列化后长度:" << req_data.size() << " 字节" << std::endl;// 4. 发送数据(TCP 发送逻辑与上一篇一致)send(client_fd, req_data.c_str(), req_data.size(), 0);// 5. 接收服务端响应并反序列化char resp_buf[1024] = {0};ssize_t recv_len = recv(client_fd, resp_buf, sizeof(resp_buf)-1, 0);if (recv_len > 0) {LoginResponse login_resp;// 反序列化:二进制字符串 → Protobuf 对象if (login_resp.ParseFromString(std::string(resp_buf, recv_len))) {std::cout << "登录结果:" << login_resp.msg() << std::endl;if (login_resp.status_code() == 0) {std::cout << "Token:" << login_resp.token() << std::endl;}} else {std::cerr << "Protobuf 反序列化失败" << std::endl;}}close(client_fd);return 0;
}
服务端核心逻辑(处理登录请求)
#include "login.pb.h"
#include <string>
#include <cstdlib> // rand() 函数// 服务端数据处理函数(替换手写序列化)
std::string LoginProtobufHandler(const std::string& client_data) {// 1. 反序列化:客户端二进制数据 → LoginRequest 对象LoginRequest req;if (!req.ParseFromString(client_data)) {// 解析失败,构建错误响应LoginResponse resp;resp.set_status_code(1);resp.set_msg("无效的 Protobuf 请求格式");resp.set_token("");std::string resp_data;resp.SerializeToString(&resp_data);return resp_data;}// 2. 模拟登录验证(与上一篇一致)bool auth_success = (req.username() == "admin" && req.password() == "123456");// 3. 构建响应并序列化LoginResponse resp;if (auth_success) {resp.set_status_code(0);resp.set_msg("登录成功");resp.set_token("fake_token_" + std::to_string(rand()%10000));} else {resp.set_status_code(2);resp.set_msg("用户名或密码错误");resp.set_token("");}std::string resp_data;resp.SerializeToString(&resp_data);return resp_data;
}
关键说明:
- 生成的方法:Protobuf 为每个字段生成
set_字段名()(设置值)和字段名()(获取值)方法,如set_username()和username(); - 序列化 / 反序列化:核心方法
SerializeToString(&str)(对象→二进制字符串)和ParseFromString(str)(二进制字符串→对象),返回bool表示成功与否; - 二进制特性:序列化后的字符串是二进制数据,直接打印会显示乱码,需通过
ParseFromString解析后才能读取字段值。
三、JSON 与 Protobuf 登录场景对比
为了更清晰地选择适合的协议,我们基于登录场景(username="admin",password="123456")做横向对比:
| 对比维度 | JSON | Protobuf |
|---|---|---|
| 数据格式 | 文本(可读) | 二进制(不可读) |
| 序列化后大小 | 约 40 字节({"username":"admin","password":"123456"}) | 约 18 字节(二进制压缩) |
| 解析速度 | 较慢(需处理文本字符) | 极快(直接解析二进制) |
| 易用性 | 高(单头文件,无需编译) | 中(需写 .proto 并编译生成代码) |
| 跨语言兼容性 | 高(所有语言支持 JSON) | 高(官方支持 C++/Java/Python 等) |
| 适用场景 | 调试场景、Web 接口、低性能要求通信 | 高性能通信(游戏、物联网)、跨语言服务调用 |
四、总结
本文基于登录场景,对比实现了 JSON 和 Protobuf 两种通用序列化协议的核心逻辑,可得出以下结论:
- 手写序列化:仅适合简单、固定的场景,灵活性和兼容性差,不推荐在实际项目中大规模使用;
- JSON:优先用于 “易读性” 和 “快速开发” 场景(如前后端交互、调试日志),选择 nlohmann/json 库可大幅降低开发成本;
- Protobuf:优先用于 “高性能” 和 “低带宽” 场景(如服务端间通信、物联网设备),需掌握 .proto 文件编写和编译流程。
在实际开发中,需根据业务场景(性能、可读性、跨语言需求)选择合适的序列化协议,而非盲目追求 “最快” 或 “最易写”。
