json和probuf的区别

5 阅读10分钟

JSON 和 Protocol Buffers (protobuf) 是两种完全不同风格和目标的序列化方案。

核心结论

JSON 是给人看的,Protobuf 是给机器用的。

JSON 追求的是可读性通用性,而 Protobuf 追求的是性能效率强规范


全方位对比表

特性JSON (JavaScript Object Notation)Protocol Buffers (protobuf)
本质纯文本、人类可读的数据格式二进制、机器高效的编码协议
数据大小(存储键名、括号、引号等冗余字符)非常小(通常比 JSON 小 3-10 倍)
序列化/反序列化速度慢(需要解析语法、转换类型)非常快(直接按预定义格式编码/解码)
可读性,无需工具即可阅读和编辑,二进制格式,人眼无法直接理解
schema(模式)可选的、弱约束(如 JSON Schema)强制的、强类型.proto 文件是合同)
数据类型基础类型(字符串、数字、布尔、null、数组、对象)丰富的标量类型(int32, double, bool, enum, bytes 等)
向后/向前兼容需要手动处理,容易出错原生支持,通过字段编号的规则
语言支持几乎所有编程语言官方支持主流语言,其他语言有社区支持
网络开销极低

直观示例:对比数据表示

假设我们需要表示一个用户信息:

1. JSON 表示

{
  "userId": 12345,
  "name": "张三",
  "email": "zhangsan@example.com",
  "age": 30,
  "isVerified": true
}

大小:约 120 字节(可以看到,键名 "userId", "name" 等重复存储,还有引号、括号等结构字符)。

2. Protobuf 表示 首先,必须定义一个 .proto 模式文件

// 1. 定义消息结构
message User {
  int32 user_id = 1;   // 字段编号,不是值!
  string name = 2;
  string email = 3;
  optional int32 age = 4; // optional 表示可选字段
  bool is_verified = 5;
}

然后,在代码中序列化的二进制数据大概是这样的(十六进制表示,仅为示意):

08 B9 60 12 06 E5 BC A0 E4 B8 89 1A 16 7A 68 61 6E 67 73 61 6E 40 65 78 61 6D 70 6C 65 2E 63 6F 6D 20 1E 28 01

大小:约 30 字节。它不存储字段名 "user_id",只存储字段编号 1 和对应的值 12345


深度解析核心区别

1. 模式(Schema) vs 无模式(Schema-less)

这是最根本的设计哲学差异。

  • JSON:是自描述的。数据本身和它的结构(键值对)在一起。你可以直接拿到一个 JSON 文件开始解析,无需提前知道它的具体结构。这很灵活,但也容易因结构不明确而产生错误。
  • Protobuf:是契约优先的。你必须先严格地定义一个 .proto 文件,明确每个字段的名称、类型和一个唯一的编号。代码是根据这个“合同”生成的。这保证了前后端数据模型的一致性。

2. 编码方式:文本 vs 二进制

  • JSON:使用文本文档格式(通常是 UTF-8 编码)。优点是通用、可读;缺点是冗余信息多,解析需要分词、语法分析。
  • Protobuf:使用复杂的二进制编码规则(如 Varint 编码、ZigZag 编码),用尽可能少的字节表示数据。例如,数字 12345 会用 Varint 编码成 1-2 个字节,而不是 JSON 中的 5 个字符。优点是极致紧凑,解码速度极快(几乎就是内存拷贝)。

3. 兼容性设计

  • JSON:添加新字段很容易,但删除或重命名字段可能破坏客户端。兼容性需要开发者自己通过代码逻辑保证。
  • Protobuf:兼容性是一等公民
    • 规则:不能重用已使用的字段编号,不能更改字段的类型。新增的字段必须是 optionalrepeated
    • 原理:当新版本的解析器遇到旧数据中不存在的字段(通过字段编号识别)时,会直接忽略;旧版本的解析器遇到新字段时也会忽略。这完美实现了向后/向前兼容

在证券/金融场景下的选型

适用 JSON 的场景:

  1. RESTful API:与外部系统、合作伙伴或 Web 前端交互。JSON 的可读性对调试、文档化非常友好。
    • 例如:GET /api/stock/AAPL/profile 返回公司简介信息。
  2. 配置文件:App 的设置、功能开关等。
  3. 日志记录:便于开发人员直接阅读日志文件进行分析。

适用 Protobuf 的场景:

  1. 高性能微服务间通信:在证券系统内部,各个服务(如行情服务、交易服务、风控服务)之间需要高速、低延迟的通信,Protobuf 是首选。
  2. 实时行情数据推送
    • 痛点:行情数据量巨大,每秒可能有数万次更新。使用 JSON 会占用大量带宽和 CPU。
    • 优势:Protobuf 极小的体积和极快的序列化速度,可以降低延迟、减少服务器负载。例如,一个订单簿的增量更新,用 Protobuf 可能只有几十字节,而 JSON 可能要几百字节。
  3. 移动端 App 与服务器的通信
    • 为了节省用户的流量并加快数据加载速度,所有重要的 API(尤其是高频调用的行情、资产更新接口)都应使用 Protobuf。
  4. 数据持久化:将结构化的数据(如 K 线数据、交易记录)以 Protobuf 格式存入磁盘或数据库,可以节省大量存储空间。

“JSON 和 Protobuf 最核心的区别在于设计目标不同。JSON 追求通用性和可读性,是一种文本格式;而 Protobuf 追求极致的性能和效率,是一种二进制协议。

具体体现在:

  1. 性能:Protobuf 的序列化速度更快,生成的数据体积更小,通常在3到10倍的差距,这对高并发和移动网络环境至关重要。
  2. 模式:Protobuf 要求强制的模式定义(.proto 文件),这保证了接口的稳定性和类型安全,并原生支持版本兼容。
  3. 可读性:JSON 的优势在于人类可读,易于调试。

在我们的证券App中,会对它们进行混合使用:

  • 对外的、不要求极高性能的 管理类 API 使用 JSON
  • 对内的、要求低延迟高吞吐的 核心业务接口(如实时行情、交易指令)会使用 Protobuf,以最大化性能并节省用户流量。”

核心流程概述

  1. 准备工作:将 .proto 文件集成到项目中,并生成对应的 Objective-C 模型类。
  2. 接收数据:从网络或蓝牙接收到原始的二进制数据(NSData)。
  3. 解析数据:使用 Protobuf 生成的类方法 parseFromData:error:NSData 反序列化成对象。
  4. 使用数据:像使用普通 Objective-C 对象一样,通过属性访问解析后的数据。

详细步骤与示例代码

第1步:定义 Protobuf 格式(.proto 文件)

首先,你需要拥有设备厂商提供的 Protobuf 格式定义文件。这是所有工作的基础。

例如,一个简化的扫地机器人状态上报 robot_status.proto 文件可能如下所示:

// 指定使用 proto3 语法
syntax = "proto3";

// 包名,会影响生成的 Objective-C 类前缀
package eufy_robot;

// 定义消息,最终会生成 Objective-C 类 ORobotStatus
message RobotStatus {
  string device_id = 1;         // 设备ID
  int32 battery_level = 2;      // 电池电量 (百分比)
  CleaningMode cleaning_mode = 3; // 清扫模式
  repeated float position = 4;   // 当前坐标 [x, y]
  bool is_charging = 5;          // 是否在充电
  ErrorCode error_code = 6;      // 错误码

  // 枚举定义:清扫模式
  enum CleaningMode {
    MODE_SILENT = 0;   // 安静
    MODE_STANDARD = 1; // 标准
    MODE_TURBO = 2;    // 强劲
  }

  // 枚举定义:错误码
  enum ErrorCode {
    ERROR_NONE = 0;          // 无错误
    ERROR_WHEEL_STUCK = 1;   // 轮子被卡住
    ERROR_DUST_BIN_FULL = 2; // 尘盒已满
  }
}

第2步:生成 Objective-C 模型类

  1. 安装 Protobuf 编译器
    • 使用 Homebrew:brew install protobuf
  2. 使用 protoc 命令生成代码
    • 在终端中执行以下命令:
    protoc --objc_out=./output robot_status.proto
    
    • --objc_out 指定输出 Objective-C 文件到 ./output 目录。
  3. 将生成的文件加入项目
    • 命令会生成两个文件:RobotStatus.pbobjc.hRobotStatus.pbobjc.m
    • 将这两个文件拖入你的 Xcode 项目中。
    • 重要:在项目的 Build Settings 中,找到 Other Linker Flags,添加 -lprotobuf

第3步:在代码中解析数据

现在,你可以在接收到设备上报的二进制数据后,进行解析了。

// 在相应的类中导入生成的头文件
#import "RobotStatus.pbobjc.h"

// 假设这是一个处理接收到的设备数据的方法
- (void)didReceiveRobotData:(NSData *)binaryData {
    // 参数 binaryData 是从蓝牙 (如 CBCharacteristic 的 value) 或网络接收到的原始二进制数据
    
    NSError *error = nil;
    
    // 核心步骤:使用生成的类解析二进制数据
    ORobotStatus *robotStatus = [ORobotStatus parseFromData:binaryData error:&error];
    
    if (error) {
        NSLog(@"❌ Protobuf 解析失败: %@", error.localizedDescription);
        return;
    }
    
    if (!robotStatus) {
        NSLog(@"❌ 解析结果为空");
        return;
    }
    
    // 解析成功,现在可以像使用普通对象一样访问数据
    NSLog(@"====== 扫地机器人状态更新 ======");
    NSLog(@"设备ID: %@", robotStatus.deviceId);
    NSLog(@"电池电量: %d%%", robotStatus.batteryLevel);
    NSLog(@"是否在充电: %@", robotStatus.isCharging ? @"是" : @"否");
    
    // 访问枚举值
    switch (robotStatus.cleaningMode) {
        case ORobotStatus_CleaningMode_ModeSilent:
            NSLog(@"清扫模式: 安静模式");
            break;
        case ORobotStatus_CleaningMode_ModeStandard:
            NSLog(@"清扫模式: 标准模式");
            break;
        case ORobotStatus_CleaningMode_ModeTurbo:
            NSLog(@"清扫模式: 强劲模式");
            break;
        default:
            break;
    }
    
    // 访问 repeated 字段 (数组)
    if (robotStatus.positionArray.count >= 2) {
        float x = [robotStatus.positionArray valueAtIndex:0];
        float y = [robotStatus.positionArray valueAtIndex:1];
        NSLog(@"当前位置: (%.2f, %.2f)", x, y);
    }
    
    // 处理错误码
    if (robotStatus.errorCode != ORobotStatus_ErrorCode_ErrorNone) {
        NSLog(@"⚠️ 设备发生错误,错误码: %d", robotStatus.errorCode);
        [self handleRobotError:robotStatus.errorCode];
    }
    
    // 最后,将解析出的数据传递给UI或业务逻辑层
    [self updateUIWithRobotStatus:robotStatus];
}

// 更新UI的方法
- (void)updateUIWithRobotStatus:(ORobotStatus *)status {
    // 确保在主线程更新UI
    dispatch_async(dispatch_get_main_queue(), ^{
        self.batteryLevelLabel.text = [NSString stringWithFormat:@"%d%%", status.batteryLevel];
        self.cleaningModeLabel.text = [self cleaningModeToString:status.cleaningMode];
        // ... 更新其他UI组件
    });
}

关键注意事项

  1. 线程安全

    • 解析操作是 CPU 密集型任务,不要在主线程执行。确保 parseFromData:error: 在后台线程调用。
  2. 错误处理

    • 务必检查 error 和返回的 robotStatus 对象是否为 nil。解析失败的原因可能是数据损坏或与 .proto 定义不匹配。
  3. 枚举类型命名

    • 生成的 Objective-C 枚举会带有消息类型名前缀,例如 CleaningMode 会变成 ORobotStatus_CleaningMode
  4. 重复字段(数组)

    • 对于 repeated 字段,生成的是 GPB*Array 类型(如 GPBFloatArray),需要使用类似 valueAtIndex: 的方法来访问元素。
  5. 版本兼容性

    • 如果设备的 .proto 定义更新了,你需要重新生成 RobotStatus.pbobjc.h/.m 文件并替换项目中的旧文件。

“在 Objective-C 中解析 Protobuf 数据主要分为三步:

  1. 代码生成:首先使用 protoc 工具,根据设备厂商提供的 .proto 文件生成对应的 Objective-C 模型类。这会得到 .pbobjc.h/.m 文件,需要将它们和 -lprotobuf 链接标志添加到项目中。

  2. 数据解析:当从蓝牙或网络接收到二进制数据(NSData)后,在后台线程调用生成的 parseFromData:error: 类方法进行反序列化。这一步是核心,会将二进制流转换成我们可以操作的对象。

  3. 数据使用:解析成功后,就可以像使用普通 Objective-C 对象一样,通过点语法访问其属性。对于枚举、基本类型和重复字段,生成的头文件里有对应的访问方式。

在整个过程中,要特别注意错误处理、线程管理和版本兼容性问题。”