浅谈Google Protobuf

by on
分类: big-data | 阅读量

本文为本人原创,欢迎分享,转载请注明【本文链接】

Thumper

1、前言

信息与物质传递的速度,决定了人类社会的发展速度。

更少的信息产生更多的价值,是信息传播时代人们一直所追求的。

2、 Google Protocol Buffer初识

回归正题,对于熟悉RPC和HTTP技术的同学来说,使用RPC框架或者HTTP Restful方案能有效降低网络应用程序的开发量,降低系统模块之间的耦合度,使得开发分布式网络应用程序更加容易。随着微服务架构的流行,在很多RPC的设计中,都采用了高性能的编解码技术,数据交互格式的重要性不言而喻。

    对于一门优秀的数据交互格式,我认为有以下几大关键要素:

    (1) 格式简单,易于人阅读;

    (2) 足够轻量化,易于计算机解析处理;

    (3) 兼容性强,易于多平台多语言间传输;

    简而言之,就是要 “更快、更小、更灵活”。说到主流的数据交互格式,一定少不了JSON和XML,对于两者的优劣高低,有兴趣的同学可以加入到这场“国家德比”式的争论中《为什么都反对 XML 而支持使用 JSON?》。

    google protocol buffer (以下统一简称protobuf) 是google发明的一款平台无关,语言无关,可扩展的序列化结构数据格式。

    鲁迅曾说过:”天下武功,唯快不破”,而”近乎变态的性能”正是protobuf的最大优势之一。相比其他类似的数据交互技术,有性能的测试结果如下:

序列化测试结果

Thumper

字节数对比测试结果如下:(对性能测试感兴趣的同学可以移步 《几种序列化协议测试对比》

Thumper

对于protobuf而言,最为特别的一点在于——它的结构即为文件。以 .proto 结尾命名(常用的版本有proto2和proto3,本文将以新版本proto3为例)。前排吃瓜,近距离体会下protobuf的定义:

syntax = "proto3";

message MyPerson {     
    int32 id = ;  
    string name = 2;
}

与创建一个struct很相似,我们用关键词message定义了一个MyPerson的结构:第一个字段id的类型为int32,第二个字段name是string类型。等号的右侧的数字,不是对字段初始化,是表示定义的字段在message中的序号。

3、Protubuf的序列化原理

protobuf能拥有如此高效的性能,主要得益于它的序列化方式设计。下面我将用一个最简单的例子,来尝试剖析其中的原理:

(1)字节码示例

    基于上面MyPerson这个结构,我们用下面的一段简单的Java代码,用基于protobuf定义的message创建一个实例,将其序列化为字节码。

public static void main(String[] args) {
    Person.MyPerson.Builder pBuilder = Person.MyPerson.newBuilder();
    pBuilder.setId(1);          // 设置id为1
    pBuilder.setName("messi");  // 名字设置为梅西
    Person.MyPerson person1 = pBuilder.build();

    // 打印序列化字节码
    for (byte b : person1.toByteArray()) {
        System.out.println(b);
    }
}

这里类似于声明了一个名为MyPerson的结构体,然后在代码中生成实际的对象并管理使用该对象。翻译成我们熟悉的Json格式,你可以认为它对应的Json数据为:

{
    "id": 1,
    "name": "messi"
}

上面for循环中打印出的字节码Byte如下:

8 
1 
18 
5 
109 
101 
115 
115 
105     

由于protobuf是二进制格式传输的,让我们来把他们翻译成计算机最喜欢的二进制数,瞅瞅里面有什么玄机:

10进制:8      2进制:0 0001 000       key  1 & 0
10进制:1      2进制:00000001       

10进制:18     2进制:0 0010 010       key  2 & 2
10进制:5      2进制:00000101       
10进制:109    2进制:01101101      
10进制:101    2进制:01100101       
10进制:115    2进制:01110011     
10进制:115    2进制:01110011       
10进制:105    2进制:01101001         

(2)TLV存储

一头雾水?让我们先来科普下protobuf的序列化原理:

protobuf经过序列化后的字节码是很紧凑的,是 key-value 的形式。以上面定义的MyPerson为例子,我们定义的的数据结构大概就如下图这样:

Thumper

我把其称之为TLV(Tag - Length - Value)存储方式 ,即 标识 - 长度 - 字段值 存储方式。意味着我们无需用多余的分隔和终止符来分隔字段,有很高的空间利用率,甚至当字段没有赋值时,序列化的数据内容中都不会包含这个字段和内容。

也可以把上面的图画的再具体一些,序列化后每个字段的字节码可以划分为6个主要部分:MSB flag、tag、wire-type、length、value、padding,字节码结构可能会根据编码方式的不同而有所差异,如下图所示:

Thumper

(3)Varint编码基础

按照国际惯例,我们有必要先理解一下最基本的varints编码方式。我们以这个varint编码后的二进制数为例:

00000001

很简单吧,就是1,没有套路,纯天然无污染的。

那我们换一个大一些的,400?

10010000  00000011

什么鬼?这两个字节码无论怎么看,怎么算都弄不出400吧?

    varint编码重要且巧妙的一点在于,它能让字节码尽量的紧凑,不浪费一点能压榨的剩余空间,堪称encoding届的周扒皮。每个字节码中的最高位我们都称之为MSB(Most Significant Bit),用来标识后续的byte是否为当前数值的一部分,如果是1,说明下一个byte也是表示当前数值的一部分。知道了这些,现在我们分析下上面那个400的字节码:

1 0010000   最高位为1,说明下一个byte也是数值的一部分
0 0000011   最高位为0,说明当前byte是数值的最后一个部分   对于MSB而言,他本身只是一个标志位,不具备任何数值意义,所以我们在解析时要去掉这一位:

0010000  0000011   而protobuf字节序采用 little-endian的排放方式,所以我们要把两个byte的位置交换:

0000011  0010000  
2 ** (8) +  2 ** (7) +  2 ** (4) = 256 + 128 +16 = 400,终于知道400是这样计算出来的了。

(4)key的剖析

那问题又来了,我们了解了上面varint的编码方式,有啥用呢?回到(1)中的字节码,刚才你也许也注意到了,我把key的那行字符码分成了几个部分:

10进制:8      2进制:0 0001 000       key  1 & 0

没错了,看到最高位的0被我隔离出来了,你也许已猜到了 —— protobuf的key正是用varint方式编码的。但需要注意的一点是,对于上面这个8位的key,字节码是这样来划分的:前5位代表了对应protobuf中的字段序号,后3位代表了这个字段的类型。

field_number << 3   |   wire_type   
( 5位field_number  +   3 位 wire_type)

那么问题又来了—— What?我们只有5位来表示字段序号?去掉最大位的MSB,那protobuf的一个message里岂不是只能定义不超过 2 的4次方 = 16 个字段?

    所以(3)中的知识点又派上用场了,假设某一个类型为int32的字段,在protobuf中定义的序号是16,那么它的key的字节码一定是这样的:

10000000  00000001

 我们写成这样你会更有感觉了:

1 0000 000   
0 0000001   第一行字节码的头是MSB,代表下一个字节码也是数值的一部分,去掉MSB,像上面一样交换字节码,我们得到了:

0000001 0000 | 000   
field_number | wire_type

2 的4次方 = 16,这个字段的序号就是16,他的wire_type是0对应的varint。所以,当你定义的某个字段的序号较大时,我们只需要扩充后面的字节码就好了。需要通过MSB来获取数值的字节码部分,拼接后得到字段的真实序号 —— 这里也恰恰也反应了varint的优点,很适合作为整数尤其是较小整数的编码,大数或者负数则是使用ZigZag的编码方式。篇幅有限,对编码部分感兴趣的同学,推荐仔细阅读下这篇官方的文章 《Protobuf Encoding》。

(5)wire-type

protobuf支持丰富的数据类型,wire_type类型有如下几种:

Thumper

(6)解读字节码

大脑都快宕机了?是不是只想右上角关闭,并送上尴尬而不失礼貌的微笑?

Thumper

    不要放弃,我觉得你还能抢救一下。回到上面的字节码,你会发现,将上面的知识点串联起来对字节码进行剖析,一切将变得明朗。

    以第一个key  0 0001 000为例:

<filed_number: 0 0001 & wire_type: 000>  --->   <filed_number: 1 & wire_type: 0>     根据Varint的编码方式:最大位MSB为0,key只占一个字节码,filed_number为1,代表这个key是我们在message定义的第一个字段。

    而后三位wire_type是0,查看上面的wire_type类型表,查到0对应Varint,而其中包含了int32这个基本类型。所以我们要用Varint的编码方式解析value:MSB为0,所以只有一个字节码表示这个字段的value。0 000 0001去掉MSB为 000 0001 = 1。所以第一个字段id的值就是1。

    同理,我们看到key 00010 010 ,field_number是2,代表这是proto中定义的第二个字段。wire_type是2,对应到了Length-delimited中的string类型。而根据Length-delimited编码方式,key的下一个字节码是表示这个字段的长度,0 0000101是5,这样我们从key往下数5行,就可以解析出每一行字节码对应的字符串 messi了。

10进制:8     2进制:00001 000     key:   1 & 0    
                                       field_number为1,对应id字段
                                       wire_type为0,对应类型为varint

10进制:1     2进制:0 0000001            value: 1    id的值为1


10进制:18    2进制:00010 010     key:   2 & 2  
                                       field_number为2,对应name字段
                                       wire_type为2,对应类型为Length-delimited
              
10进制:5     2进制:0 0000101             length: 5   代表string的长度为5
10进制:109   2进制:0 1101101             value: 109  ASCII对应字母为m
10进制:101   2进制:0 1100101             value: 101  ASCII对应字母为e
10进制:115   2进制:0 1110011             value: 115  ASCII对应字母为s
10进制:115   2进制:0 1110011             value: 115  ASCII对应字母为s
10进制:105   2进制:0 1101001             value: 105  ASCII对应字母为i
    

4、牛刀小试

看完上面一堆原理,简单实战运动一下,假设我们现在要设计一个简单的球员信息注册服务。

使用protobuf的流程我大致总结为下:

Thumper

    protobuf支持多种语言和平台的使用,c++、java、python与golang等语言都有对应的使用tutorials。不同平台与语言之间的使用大同小异,下面测试实例的软件版本信息如下:

jdk                   1.8.0_144 
protobuf-java         3.5.0
protoc                protoc-3.6.0-win32  

(1)下载protoc-3.x.x-win32.zip

解压后bin目录下,有执行文件protoc.exe,用来将你写的.proto文件进行编译生成指定的文件。

(2)定义并编译proto文件

我们要简单设计一个用于消息通讯的 Msg.proto:

syntax = "proto3";

// 消息的整体结构
message CMsg
{
    string msghead = 1;
    string msgbody = 2;
}
 
// 消息的head,对应CMsg的msghead
message CMsgHead
{
    int32 msglen = 1;
    int32 msgseq = 2;
    int32 msgres = 3;
    string termid = 4;
}
 
// 消息的Body,对应CMsg的msgbody,球员注册的主要信息和结果返回
message CMsgReg
{
    int32 id = 1;
    string name = 2;
    int32 age = 3;
    int32 ret = 4;
    string termid = 5;
}

将Msg.proto文件放在同级bin目录下,确保在环境变量配置好的情况下,在cmd中,用一条编译命令执行编译,生成编译器编译后的Msg.java文件。

protoc --proto_path=F:\pb\protoc-3.5.0-win32\bin  --java_out=F:\pb\protoc-3.5.0-win32\bin  F:\pb\protoc-3.5.0-win32\bin\Msg.proto

其他语言同理,例如c++,将上面的 –java_out改成–cpp_out,就能生成对应的Msg.pb.h和Msg.pb.cc文件了。

    如果你只是一个使用者,并不需要过多关注其中的细节(省略实现细节),你也无需改动它,你要做的,就是把它拖入你的工程内使用即可。

package com.tencent.xxx;
// Generated by the protocol buffer compiler.  DO NOT EDIT!
// source: Msg.proto

public final class Msg {
    private Msg() {
    }

    public static void registerAllExtensions(
            com.google.protobuf.ExtensionRegistryLite registry) {
    }

    public static void registerAllExtensions(
            com.google.protobuf.ExtensionRegistry registry)

    public interface CMsgOrBuilder extends
            com.google.protobuf.MessageOrBuilder {...}

    public static final class CMsg extends
            com.google.protobuf.GeneratedMessageV3 implements
            CMsgOrBuilder {...}

    public interface CMsgHeadOrBuilder extends
            com.google.protobuf.MessageOrBuilder {...}

    public static final class CMsgHead extends
            com.google.protobuf.GeneratedMessageV3 implements
            CMsgHeadOrBuilder {...}

    public interface CMsgRegOrBuilder extends
            com.google.protobuf.MessageOrBuilder {...}

    public static final class CMsgReg extends
            com.google.protobuf.GeneratedMessageV3 implements
            CMsgRegOrBuilder {...}

    private static final com.google.protobuf.Descriptors.Descriptor
            internal_static_CMsg_descriptor;
    private static final
    com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
            internal_static_CMsg_fieldAccessorTable;
    private static final com.google.protobuf.Descriptors.Descriptor
            internal_static_CMsgHead_descriptor;
    private static final
    com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
            internal_static_CMsgHead_fieldAccessorTable;
    private static final com.google.protobuf.Descriptors.Descriptor
            internal_static_CMsgReg_descriptor;
    private static final
    com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
            internal_static_CMsgReg_fieldAccessorTable;

    public static com.google.protobuf.Descriptors.FileDescriptor
    getDescriptor() {
        return descriptor;
    }
    private static com.google.protobuf.Descriptors.FileDescriptor
            descriptor;
    static {...}
    // @@protoc_insertion_point(outer_class_scope)
}

(3)添加相关依赖库

我使用了 Maven3来管理我的Java工程,我们在pom.xml中添加一个关于protobuf工具库的使用依赖:

    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java-util</artifactId>
        <version>3.5.0</version>
    </dependency>  

(4)编写服务端代码

现在我们来编写我们的注册服务器,主要逻辑为读取客户侧发送的请求,并将stream解析为CMsg的实例对象:

DataOutputStream dataOutputStream;
InputStream istream = client.getInputStream();
dataOutputStream = new DataOutputStream(client.getOutputStream());
byte len[] = new byte[1024];
// 读取客户端发来的stream
int cnt = istream.read(len);
byte[] temp = new byte[cnt];

for (int i = 0; i < cnt; i++) {
    temp[i] = len[i];
}
// 解析为protobuf中定义的CMsg对象
CMsg msg = CMsg.parseFrom(temp);

在有了CMsg的对象之后,我们可以通过Msg.java中提供的方法,从而进一步获取head和body中的具体信息,并将其解析为protobuf中定义的CMsgHead和CMsgReg对象。同理,我们又可以进一步通过get方法,获取head和body中定义的具体信息了。

// 头信息
CMsgHead head = CMsgHead.parseFrom(msg.getMsghead().getBytes());
// Body信息
CMsgReg body = CMsgReg.parseFrom(msg.getMsgbody().getBytes());

完整的Server端代码 PlayerRegisterServer.java 如下:

package com.tencent.xxx;  
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import com.tencent.sniper.Msg.CMsg;
import com.tencent.sniper.Msg.CMsgHead;
import com.tencent.sniper.Msg.CMsgReg;
public class PlayerRegisterServer implements Runnable {
    private int id = 0;
    public void run() {
            try {
                System.out.println("Begin to register Football Player...");
                ServerSocket serverSocket = new ServerSocket(8888);

                while (true) {
                    System.out.println("waiting for new connection...");
                    Socket client = serverSocket.accept();
                    DataOutputStream dataOutputStream;

                    try {
                        InputStream istream = client.getInputStream();
                        dataOutputStream = new DataOutputStream(client.getOutputStream());
                        byte len[] = new byte[1024];

                        //读取客户端发来的stream
                        int cnt = istream.read(len);
                        byte[] temp = new byte[cnt];

                        for (int i = 0; i < cnt; i++) {
                            temp[i] = len[i];
                        }
                        // 解析获取客户端的msg
                        CMsg msg = CMsg.parseFrom(temp);
                        CMsgHead head = CMsgHead.parseFrom(msg.getMsghead().getBytes());

                        System.out.println("head len: " + head.getMsglen());
                        System.out.println("head res: " + head.getMsgres());
                        System.out.println("head seq: " + head.getMsgseq());
                        System.out.println("head termid: " + head.getTermid());

                        registerPlayer(dataOutputStream, CMsgReg.parseFrom(msg.getMsgbody().getBytes()));
                        istream.close();
                    } catch (Exception ex) {
                        System.out.println(ex.getMessage());
                        ex.printStackTrace();
                    } finally {
                        client.close();
                        System.out.println("server close\n");
                    }
                }
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }

        private void registerPlayer(DataOutputStream dataOutputStream, CMsgReg body) {
            System.out.println("start to register palyer...");
            System.out.println("player name: " + body.getName());
            System.out.println("player age: " + body.getAge());

            CMsgHead rhead = CMsgHead.newBuilder().setMsglen(10).
                    .setMsgseq(100).setMsgres(0)
                    .setTermid("Register Client: Head").build();

            int ret = 0;
            int pid = 0;
            // 只允许31岁以下的球员注册成功
            if (body.getAge() < 31) {
                pid = ++id;
            } else {
                ret = 1001;
            }

            CMsgReg rbody = CMsgReg.newBuilder().setId(pid).setName(body.getName())
                    .setAge(body.getAge()).setRet(ret).setTermid("Register Client: Body").build();

            CMsg rmsg = CMsg.newBuilder()
                    .setMsghead(rhead.toByteString().toStringUtf8())
                    .setMsgbody(rbody.toByteString().toStringUtf8()).build();

            byte[] backBytes = rmsg.toByteArray();

            try {
                dataOutputStream.write(backBytes, 0, backBytes.length);
                dataOutputStream.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public static void main(String[] args) {
            Thread desktopServerThread = new Thread(new PlayerRegisterServer());
            desktopServerThread.start();
        }
}  

(5)编写客户端代码:

客户端相对要简单一些,我们要做的就是创建protobuf定义的结构实例,然后发送到服务器注册信息:

// 按照定义的protobuf格式组装head
CMsgHead head = CMsgHead.newBuilder().setMsglen(5)
                .setMsgseq().setMsgres(1001)
                .setTermid("Register Client: Head").build();

// 按照定义的protobuf格式组装body
CMsgReg body = CMsgReg.newBuilder().setId(1).setName(name)
               .setAge(age).setRet(1001).setTermid("Register Client: Body").build();

// 按照定义的protobuf格式,将head和body拼接成msg
CMsg msg = CMsg.newBuilder()
           .setMsghead(head.toByteString().toStringUtf8())
           .setMsgbody(body.toByteString().toStringUtf8()).build();

我们还可以做个简单的测试,将protobuf序列化后字节码的大小与存储了相同信息的json串大小做个比较:

public void printSizeOfData(CMsg g) {
    System.out.println("\n" + "bytes size: " + g.toByteString().size());

    String pJsonData = "";
    try {
        pJsonData = JsonFormat.printer().print(g);
    } catch (Exception e) {
        e.printStackTrace();
    }

    System.out.println("json size:" + pJsonData.getBytes().length);
}

完整的客户端代码 PlayerRegisterClient.java 如下:

package com.tencent.xxx;

import java.io.InputStream;
import java.net.Socket;

import com.google.protobuf.util.JsonFormat;
import com.tencent.sniper.Msg.CMsg;
import com.tencent.sniper.Msg.CMsgHead;
import com.tencent.sniper.Msg.CMsgReg;

public class PlayerRegisterClient {
    public void run() {
        try {
            // 这里我们模拟注册三位球员
            registerPlayer("Cristiano Ronaldo", 33);
            registerPlayer("Leon Messi", 30);
            registerPlayer("Kylian Mbappé", 20);
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }

public void registerPlayer(String name, int age) throws Exception {
    Socket socket = null;
    socket = new Socket("localhost", 12345);

    // 按照定义的protobuf格式组装head
    CMsgHead head = CMsgHead.newBuilder().setMsglen(5)
            .setMsgseq(100).setMsgres(1001)
            .setTermid("Register Client: Head").build();

    // 按照定义的protobuf格式组装body
    CMsgReg body = CMsgReg.newBuilder().setId(1).setName(name)
            .setAge(age).setRet(1001).setTermid("Register Client: Body").build();

    // 按照定义的protobuf格式,将headbody拼接成msg
    CMsg msg = CMsg.newBuilder()
            .setMsghead(head.toByteString().toStringUtf8())
            .setMsgbody(body.toByteString().toStringUtf8()).build();

    // 向服务器发送请求
    System.out.println("sendMsg to register player...\n");
    msg.writeTo(socket.getOutputStream());

    // 接收返回结果并解析
    InputStream input = socket.getInputStream();
    byte[] by = recvMsg(input);
    getRegisterResult(CMsg.parseFrom(by));
    printSizeOfData(CMsg.parseFrom(by));

    input.close();
    socket.close();
}

public void getRegisterResult(CMsg g) {
    try {
        StringBuffer sb = new StringBuffer();

        CMsgReg body = CMsgReg.parseFrom(g.getMsgbody().getBytes());
        if (body.getRet() == 0) {
            System.out.println("successfully register player:");
            System.out.println("player id: " + body.getId());
            System.out.println("player name: " + body.getName());
            System.out.println("player age: " + body.getAge());
            System.out.println("player ret: " + body.getRet());
            System.out.println("player termid: " + body.getTermid());
            System.out.println(sb.toString());
        } else {
            System.out.println("Fail to register player [" + body.getName() +
                    "], he's too old to join the club!");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}

public byte[] recvMsg(InputStream inpustream) {
    System.out.println("[recvMsg]");
    byte[] temp = null;

    try {
        byte len[] = new byte[1024];
        int cnt = inpustream.read(len);

        temp = new byte[cnt];
        for (int i = 0; i < cnt; i++) {
            temp[i] = len[i];
        }

        return temp;
    } catch (Exception e) {
        System.out.println(e.toString());
        return temp;
    }
}

public void printSizeOfData(CMsg g) {
    System.out.println("\n" + "bytes size:" + g.toByteString().size());
    String pJsonData = "";
    try {
            pJsonData = JsonFormat.printer().print(g);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("json size:" + pJsonData.getBytes().length);
    }

    public static void main(String[] args) {
        PlayerRegisterClient pc = new PlayerRegisterClient();
        pc.run();
    }
}  

(6)运行结果

客户端测试结果:

sendMsg to register player...
[recvMsg]
Fail to register player [Cristiano Ronaldo], he's too old to join the club!

bytes size:84
json size:163


sendMsg to register player...
[recvMsg]
successfully register player:
player id: 1
player name: Leon Messi
player age: 30
player ret: 0
player termid: Client:Register Body

bytes size:74
json size:155


sendMsg to register player...
[recvMsg]
successfully register player:
player id: 2
player name: Kylian Mbappé
player age: 20
player ret: 0
player termid: Client:Register Body

bytes size:78
json size:163  

服务端测试结果:

Begin to register Football Player...
waiting for new connection...
head len: 5
head res: 0
head seq: 100
head termid: Register Client: Head
start to register palyer...
player name: Cristiano Ronaldo
player age: 33
server close


waiting for new connection...
head len: 5
head res: 0
head seq: 100
head termid: Register Client:Head
start to register palyer...
player name: Leon Messi
player age: 30
server close


waiting for new connection...
head len: 5
head res: 0
head seq: 100
head termid: Register Client:Head
start to register palyer...
player name: Kylian Mbappé
player age: 20
server close
waiting for new connection...

传输同样的数据和信息,与json的size相比,protobuf的威力可见一斑。至于xml。。。就不提了吧。

5、尾声闲谈

    如果需要进行频繁的数据交互,特别是对延迟敏感的场景(例如游戏),或者是在要在错综分布的系统间低成本交互(例如云),protobuf都是一个不错的数据交互格式。

    当然古人有云,程序员最忌讳盲目崇拜,PHP也不一定是世界上最好的语言。根据场景的不同,还是要更加全面的看待其利与弊。protobuf作为一项用二进制数据交互格式,相对于其他数据格式,可读性是较差的。或者当网络服务类型跨度大、变化频繁,特别是你的数据需要直接与Web交互时,或者需要与偏向底层的接口进行交互,用json/xml或者其他格式相对是明智之选。

    再说到protobuf,不得不提下基于protobuf 和 HTTP2的 “下一代RPC框架” —— grpc。传输协议和数据的交互格式是RPC框架中非常重要的两个因素,不得不说google是下了一招很秒的棋:既可以使用HTTP2(多路复用技术和对header的压缩技术值得推敲),又可以大力推广普及自创的protobuf。当然,提到优秀的RPC框架,有鹅厂的Tars,有grpc的劲敌Apache Thrift,还有阿里的Dubbo等等,都是值得学习研究的。上文中如有错误或不当的地方,也欢迎各位指正与交流。

protobuf, 大数据