为了账号安全,请及时绑定邮箱和手机立即绑定

Protobuf序列化和反序列化入门教程

标签:
Python C++ Go
概述

本文详细介绍了如何使用Protocol Buffers(简称 Protobuf)进行数据的序列化和反序列化,包括创建消息定义文件、生成消息类、以及在Python、Java、C++和Go等语言中的具体实现。文中通过实际案例演示了如何在分布式系统中传输用户数据,并提供了常见问题的排查步骤和解决方案。Protobuf序列化和反序列化过程被全面覆盖,确保数据在网络传输中的高效性和一致性。

Protobuf简介

Protobuf定义

Protocol Buffers(简称 Protobuf)是由 Google 开发的一种灵活、高效、自动化的结构化数据序列化格式。它提供了一种简单的方法,可以将结构化数据序列化为一种高效且可扩展的格式,同时还可以逆向解析序列化的数据。Protobuf 数据格式非常紧凑,适合于在网络上进行数据传输,以及在不同编程语言间进行数据交换。

Protobuf用途

Protobuf 主要用于以下场景:

  1. 数据序列化:将结构化数据序列化为二进制格式,以便在网络或文件系统中传输。
  2. 数据交换:在不同平台或编程语言之间传输数据,保持数据结构的一致性。
  3. 数据存储:在数据库或文件系统中存储数据。
  4. 配置文件:简化配置文件的编写和解析。

Protobuf优势

  1. 高效性:Protobuf 的序列化格式非常紧凑,传输和解析速度快。
  2. 语言无关性:支持多种编程语言,如 Java、C++、Python、Go 等,可以在不同语言间无缝交换数据。
  3. 扩展性:新增字段不会影响旧版本的解析器,允许逐步添加和移除字段。
  4. 灵活性:Protobuf 的描述文件提供了一种简洁的方式来定义数据结构,使开发人员易于理解和使用。

安装Protobuf

安装Protobuf环境

在开始使用 Protobuf 之前,需要安装 Protobuf 编译工具 protoc。以下是安装步骤:

  1. 安装 Protobuf 编译器

    • 对于 Linux 系统,可以通过包管理器安装:
      sudo apt-get install protobuf-compiler
    • 对于 macOS 系统,可以使用 Homebrew 安装:
      brew install protobuf
    • 对于 Windows 系统,可以到 Protobuf 的官方 GitHub 仓库下载预编译的二进制文件并安装。
  2. 安装编程语言绑定

    • 安装 Java 绑定:
      sudo apt-get install libprotoc-dev
      sudo apt-get install protobuf-compiler-grpc
    • 安装 Python 绑定:
      pip install protobuf
  3. 安装 Go 绑定

    go get github.com/golang/protobuf/protoc-gen-go
  4. 安装 C++ 绑定
    sudo apt-get install protobuf-compiler
    sudo apt-get install libprotobuf-dev
    sudo apt-get install protobuf-compiler-grpc

验证安装是否成功

验证安装是否成功的过程如下:

  1. 检查 Protobuf 编译器版本
    • 使用 protoc --version 命令检查编译器版本:
      protoc --version
  2. 测试编译器
    • 创建一个简单的 Protobuf 消息定义文件 example.proto,内容如下:
      syntax = "proto3";
      package example;
      message Person {
      string name = 1;
      int32 id = 2;
      string email = 3;
      }
    • 使用 protoc 编译器生成消息类:
      protoc --cpp_out=. example.proto
      protoc --java_out=. example.proto
      protoc --python_out=. example.proto
      protoc --go_out=. example.proto
    • 检查生成的文件是否存在,并且没有错误输出。

Protobuf序列化

创建Protobuf消息定义文件

Protobuf 消息定义文件是一种描述结构化数据格式的文本文件,文件后缀名为 .proto。以下是一个简单的 Protobuf 消息定义文件示例:

syntax = "proto3";
package example;
message Person {
    string name = 1;
    int32 id = 2;
    string email = 3;
}

生成消息类

生成的消息类用于在不同的编程语言中表示和操作 Protobuf 消息。以下是使用 protoc 编译器生成消息类的命令:

protoc --cpp_out=. example.proto
protoc --java_out=. example.proto
protoc --python_out=. example.proto
protoc --go_out=. example.proto

序列化数据到二进制文件

序列化是指将内存中的数据结构转换为字节流,以便在网络或文件系统中传输。以下是使用不同编程语言序列化数据到二进制文件的示例:

Python 示例
import example_pb2

person = example_pb2.Person()
person.name = "Alice"
person.id = 1234
person.email = "alice@example.com"

with open("person.bin", "wb") as f:
    f.write(person.SerializeToString())
Java 示例
import example.Person;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        example.Person person = example.Person.newBuilder()
                .setName("Alice")
                .setId(1234)
                .setEmail("alice@example.com")
                .build();

        try (FileOutputStream outputStream = new FileOutputStream("person.bin")) {
            person.writeTo(outputStream);
        }
    }
}
C++ 示例
#include "example.pb.h"
#include <fstream>

int main() {
    example::Person person;
    person.set_name("Alice");
    person.set_id(1234);
    person.set_email("alice@example.com");

    std::fstream output("person.bin", std::ios::out | std::ios::binary);
    if (!person.SerializeToOstream(&output)) {
        std::cerr << "Failed to write message.\n";
        return -1;
    }
    return 0;
}
Go 示例
package main

import (
    "example"
    "google.golang.org/protobuf/io"
    "os"
)

func main() {
    person := &example.Person{
        Name:  "Alice",
        Id:    1234,
        Email: "alice@example.com",
    }

    w := io.NewDelimitedWriter(os.Stdout)
    if err := w.WriteMessage(person); err != nil {
        panic(err)
    }
}

验证序列化结果

为了验证序列化是否正确,可以将生成的二进制文件读取并反序列化,然后对比原始数据和反序列化后的数据。

Python 示例
import example_pb2

with open("person.bin", "rb") as f:
    person = example_pb2.Person()
    person.ParseFromString(f.read())
    assert person.name == "Alice"
    assert person.id == 1234
    assert person.email == "alice@example.com"
Java 示例
import example.Person;
import java.io.FileInputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        try (FileInputStream inputStream = new FileInputStream("person.bin")) {
            example.Person person = example.Person.newBuilder().mergeFrom(inputStream).build();
            assert person.getName().equals("Alice");
            assert person.getId() == 1234;
            assert person.getEmail().equals("alice@example.com");
        }
    }
}
C++ 示例
#include "example.pb.h"
#include <fstream>

int main() {
    example::Person person;
    std::fstream input("person.bin", std::ios::in | std::ios::binary);
    if (!person.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse message.\n";
        return -1;
    }
    assert(person.name() == "Alice");
    assert(person.id() == 1234);
    assert(person.email() == "alice@example.com");
    return 0;
}
Go 示例
package main

import (
    "example"
    "io/ioutil"
    "os"
    "reflect"
)

func main() {
    data, err := ioutil.ReadFile("person.bin")
    if err != nil {
        panic(err)
    }
    person := &example.Person{}
    err = person.Unmarshal(data)
    if err != nil {
        panic(err)
    }
    assert(reflect.DeepEqual(person, &example.Person{Name: "Alice", Id: 1234, Email: "alice@example.com"}))
}

func assert(condition bool) {
    if !condition {
        panic("Assertion failed")
    }
}

Protobuf反序列化

读取二进制文件

反序列化是指将字节流还原为内存中的数据结构。以下是如何读取二进制文件的示例:

Python 示例
import example_pb2

with open("person.bin", "rb") as f:
    person = example_pb2.Person()
    person.ParseFromString(f.read())
    print(f"Name: {person.name}")
    print(f"ID: {person.id}")
    print(f"Email: {person.email}")
Java 示例
import example.Person;
import java.io.FileInputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        try (FileInputStream inputStream = new FileInputStream("person.bin")) {
            example.Person person = example.Person.newBuilder().mergeFrom(inputStream).build();
            System.out.println("Name: " + person.getName());
            System.out.println("ID: " + person.getId());
            System.out.println("Email: " + person.getEmail());
        }
    }
}
C++ 示例
#include "example.pb.h"
#include <fstream>

int main() {
    example::Person person;
    std::fstream input("person.bin", std::ios::in | std::ios::binary);
    if (!person.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse message.\n";
        return -1;
    }
    std::cout << "Name: " << person.name() << "\n";
    std::cout << "ID: " << person.id() << "\n";
    std::cout << "Email: " << person.email() << "\n";
    return 0;
}
Go 示例
package main

import (
    "example"
    "io/ioutil"
    "os"
)

func main() {
    data, err := ioutil.ReadFile("person.bin")
    if err != nil {
        panic(err)
    }
    person := &example.Person{}
    err = person.Unmarshal(data)
    if err != nil {
        panic(err)
    }
    println("Name:", person.Name)
    println("ID:", person.Id)
    println("Email:", person.Email)
}

反序列化成对象

反序列化的过程将字节流转换为内存中的对象,然后可以进行进一步的操作。上述示例展示了如何将二进制文件内容反序列化为对应的 Protobuf 对象,并输出其属性。

处理反序列化后的数据

反序列化后的数据可以根据需要进行进一步处理,例如修改属性、执行业务逻辑等。以下是一个简单的示例:

Python 示例
import example_pb2

with open("person.bin", "rb") as f:
    person = example_pb2.Person()
    person.ParseFromString(f.read())
    print(f"Original Name: {person.name}")
    person.name = "Bob"
    print(f"Updated Name: {person.name}")
Java 示例
import example.Person;
import java.io.FileInputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        try (FileInputStream inputStream = new FileInputStream("person.bin")) {
            example.Person person = example.Person.newBuilder().mergeFrom(inputStream).build();
            System.out.println("Original Name: " + person.getName());
            person = person.toBuilder().setName("Bob").build();
            System.out.println("Updated Name: " + person.getName());
        }
    }
}
C++ 示例
#include "example.pb.h"
#include <fstream>

int main() {
    example::Person person;
    std::fstream input("person.bin", std::ios::in | std::ios::binary);
    if (!person.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse message.\n";
        return -1;
    }
    std::cout << "Original Name: " << person.name() << "\n";
    person.set_name("Bob");
    std::cout << "Updated Name: " << person.name() << "\n";
    return 0;
}
Go 示例
package main

import (
    "example"
    "io/ioutil"
    "os"
)

func main() {
    data, err := ioutil.ReadFile("person.bin")
    if err != nil {
        panic(err)
    }
    person := &example.Person{}
    err = person.Unmarshal(data)
    if err != nil {
        panic(err)
    }
    println("Original Name:", person.Name)
    person.Name = "Bob"
    println("Updated Name:", person.Name)
}

实际案例演示

案例背景

假设公司正在开发一个分布式系统,需要在不同的服务之间传输用户数据。为了确保高效的数据交换,选择使用 Protobuf 进行数据序列化和反序列化。

编写Protobuf消息定义

定义一个简单的用户数据消息 User.proto

syntax = "proto3";
package user;
message User {
    string name = 1;
    int32 age = 2;
    string email = 3;
}

完整序列化和反序列化过程

定义用户信息并序列化到文件
import user_pb2

user = user_pb2.User()
user.name = "Alice"
user.age = 25
user.email = "alice@example.com"

with open("user.bin", "wb") as f:
    f.write(user.SerializeToString())
import user.User;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        user.User user = user.User.newBuilder()
                .setName("Alice")
                .setAge(25)
                .setEmail("alice@example.com")
                .build();

        try (FileOutputStream outputStream = new FileOutputStream("user.bin")) {
            user.writeTo(outputStream);
        }
    }
}
#include "user.pb.h"
#include <fstream>

int main() {
    user::User user;
    user.set_name("Alice");
    user.set_age(25);
    user.set_email("alice@example.com");

    std::fstream output("user.bin", std::ios::out | std::ios::binary);
    if (!user.SerializeToOstream(&output)) {
        std::cerr << "Failed to write message.\n";
        return -1;
    }
    return 0;
}
package main

import (
    "example"
    "google.golang.org/protobuf/io"
    "os"
)

func main() {
    user := &user.User{
        Name:  "Alice",
        Age:   25,
        Email: "alice@example.com",
    }

    w := io.NewDelimitedWriter(os.Stdout)
    if err := w.WriteMessage(user); err != nil {
        panic(err)
    }
}
从文件中读取并反序列化用户信息
import user_pb2

with open("user.bin", "rb") as f:
    user = user_pb2.User()
    user.ParseFromString(f.read())
    print(f"Name: {user.name}")
    print(f"Age: {user.age}")
    print(f"Email: {user.email}")
import user.User;
import java.io.FileInputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        try (FileInputStream inputStream = new FileInputStream("user.bin")) {
            user.User user = user.User.newBuilder().mergeFrom(inputStream).build();
            System.out.println("Name: " + user.getName());
            System.out.println("Age: " + user.getAge());
            System.out.println("Email: " + user.getEmail());
        }
    }
}
#include "user.pb.h"
#include <fstream>

int main() {
    user::User user;
    std::fstream input("user.bin", std::ios::in | std::ios::binary);
    if (!user.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse message.\n";
        return -1;
    }
    std::cout << "Name: " << user.name() << "\n";
    std::cout << "Age: " << user.age() << "\n";
    std::cout << "Email: " << user.email() << "\n";
    return 0;
}
package main

import (
    "example"
    "io/ioutil"
    "os"
)

func main() {
    data, err := ioutil.ReadFile("user.bin")
    if err != nil {
        panic(err)
    }
    user := &example.User{}
    err = user.Unmarshal(data)
    if err != nil {
        panic(err)
    }
    println("Name:", user.Name)
    println("Age:", user.Age)
    println("Email:", user.Email)
}

检查序列化与反序列化结果是否一致

通过对比原始数据和反序列化后的数据,可以验证序列化和反序列化是否正确无误。

Python 示例
import user_pb2

original_user = user_pb2.User()
original_user.name = "Alice"
original_user.age = 25
original_user.email = "alice@example.com"

with open("user.bin", "rb") as f:
    user = user_pb2.User()
    user.ParseFromString(f.read())

    assert original_user.name == user.name
    assert original_user.age == user.age
    assert original_user.email == user.email
Java 示例
import user.User;
import java.io.FileInputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        user.User originalUser = user.User.newBuilder()
                .setName("Alice")
                .setAge(25)
                .setEmail("alice@example.com")
                .build();

        try (FileInputStream inputStream = new FileInputStream("user.bin")) {
            user.User user = user.User.newBuilder().mergeFrom(inputStream).build();

            assert originalUser.getName().equals(user.getName());
            assert originalUser.getAge() == user.getAge();
            assert originalUser.getEmail().equals(user.getEmail());
        }
    }
}
C++ 示例
#include "user.pb.h"
#include <fstream>

int main() {
    user::User originalUser;
    originalUser.set_name("Alice");
    originalUser.set_age(25);
    originalUser.set_email("alice@example.com");

    user::User user;
    std::fstream input("user.bin", std::ios::in | std::ios::binary);
    if (!user.ParseFromIstream(&input)) {
        std::cerr << "Failed to parse message.\n";
        return -1;
    }

    assert(originalUser.name() == user.name());
    assert(originalUser.age() == user.age());
    assert(originalUser.email() == user.email());

    return 0;
}
Go 示例
package main

import (
    "example"
    "io/ioutil"
    "os"
    "reflect"
)

func main() {
    originalUser := &example.User{
        Name:  "Alice",
        Age:   25,
        Email: "alice@example.com",
    }

    data, err := ioutil.ReadFile("user.bin")
    if err != nil {
        panic(err)
    }
    user := &example.User{}
    err = user.Unmarshal(data)
    if err != nil {
        panic(err)
    }

    assert(reflect.DeepEqual(originalUser, user))
}

func assert(condition bool) {
    if !condition {
        panic("Assertion failed")
    }
}

常见问题及解决方法

常见错误

  1. 编译错误:在生成消息类时遇到编译错误,通常是由于 .proto 文件格式错误或路径问题。
  2. 序列化失败:序列化时可能出现数据丢失或格式错误。
  3. 反序列化失败:反序列化时可能出现解析错误或数据格式不匹配。
  4. 版本不兼容:在不同版本的 Protobuf 之间进行数据交换时可能出现不兼容的问题。

错误排查步骤

  1. 检查 .proto 文件格式
    • 确保 .proto 文件语法正确,没有拼写错误或格式错误。
  2. 生成消息类
    • 使用 protoc 命令生成消息类,并检查生成文件是否存在且没有编译错误。
  3. 序列化问题
    • 检查序列化代码逻辑,确保数据正确填充并调用正确的序列化方法。
  4. 反序列化问题
    • 检查反序列化代码逻辑,确保数据正确读取并调用正确的反序列化方法。
  5. 版本兼容性
    • 确保所有参与数据交换的服务使用相同的 Protobuf 版本。

常见问题解答

  1. 如何处理跨版本的兼容性问题?

    • .proto 文件中使用 optionaldefault 关键字,以确保添加新字段时不会破坏旧版本的解析器。
    • 使用 backward_compatibility 插件来帮助处理版本升级问题。
  2. 如何提高序列化和反序列化的性能?

    • .proto 文件中使用 packedrepeated 选项来优化序列化效率。
    • 使用 protoc 编译器的优化选项,如 --optimize_for=SPEED--optimize_for=CODE_SIZE
  3. 为什么序列化后文件大小特别大?

    • 检查数据类型和结构,确保没有不必要的冗余数据。
    • 使用 packed 选项将多字段包装在单个字节流中,以减少序列化后的文件大小。
  4. 如何处理非标准数据类型?
    • 定义自定义消息类型来表示非标准数据类型。
    • 使用 messageenum 来封装复杂数据结构。
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消