Protocol-Buffers浅出指南

56次阅读

共计 3898 个字符,预计需要花费 10 分钟才能阅读完成。

什么是 socket

在了解 pb 协议之前,首先要明确 socket 是什么,socket 是网络套接字,即 IP+ 端口号的形式,更准确的说套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将 I / O 插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是 IP 地址与端口的组合。

总而言之:
1、socket 是系统底层提供的一个被应用程序调用的通用接口
2、socket 的形式:IP 地址 + 端口号
3、每一个 socket 都会有一个应用程序与之对应。

Q&A?

1、Protocol Buffers 是什么?

Protocol Buffers 是一种广泛使用结构化数据存储格式,可以用于结构化数据的序列化 / 反序列化,也是很多 rpc 框架的基础之一,和 json、xml 类似。

2、pb 协议是哪一层的协议?

Protocol Buffers 是数据存储格式,基于此我们可以对请求和响应包结构进行再定义,就有了 pb 协议,直接处理的是字节流,不再需要基于 http 协议解析和传输。所以 pb 协议是应用层协议。

3、pb 协议需要考虑数据安全问题吗?http 有 https 加密,那 pb 协议呢?

pb 协议暂时适用于内网服务器通信,并没有进行数据加密。

4、pb 协议没有区分 get post,甚至还没有状态码的概念?网络连接出错怎么办?服务端没有响应数据又怎么办呢?

pb 协议和 http 协议类似,get、post 区分本身不影响请求,而对于其他所描述的情况,都要根据 socket 接口再封装,做错误处理、超时检测等。

5、相比 JSON 和 XML 有什么优势呢?

JSON 和 XML 都基于 HTTP 协议进行数据传输,同时需要对 JSON 和 XML 字符串进行解析,相比 pb 协议的方式,pb 协议基于字节流进行处理解析,pb 协议传输的数据量更小,处理速度更快捷。

6、这种数据传输方式仅适用于服务端通信吗?客户端可以使用吗?

并不是,其他客户端也可以使用。在前端 web 页面中,可以通过 websocket 传输 ArrayBuffer 的形式,直接传输字节流到达服务端,之后从服务端拿到传输回来的二进制流,进行解析;或者基于 formdata 也可以传输二进制文件对象,通过把 Arraybuffer 放进 blob 对象中,传输给后台;当然也可以通过 charCode 的方式把 buffer 转换成字符串,但是这样会更加耗时。但是要注意的一点是,客户端和服务端都要同步更新 proto 文件。

protocol buffers 的使用

proto 文件中的数据定义方式如下:
Message 消息名 {

 字段规则 字段类型 字段名 = 分配标识号 [default=xxx];

}

1、字段规则:required(必须设置)、optional(可以有 0 或 1 个)、repeated(可以有 0 或多个)
required:实例中必须包含的字段
optional:实例中可以选择性包含的字段,若实例没有指定,则为默认值,若没有设置该字段的默认值,其值是该类型的默认值。如 string 默认值为””,bool 默认值为 false, 整数默认值为 0。
repeated: 可以有多个值的字段,这类变量类似于 vector,可以存储此类型的多个值。

2、字段类型:可以是标准类型、枚举类型、自定义 message 类型

3、分配标识名:1、2、3……
在 proto 数据结构中,每一个变量都有唯一的数字标识。这些标识符的作用是在二进制格式中识别各个字段的,一旦开始使用就不可再改变。
此处需要注意的是 1 -15 之内的标号在存储的时候只占一个字节,而大于 15 到 162047 之间的需要占两个字符,所以我们尽量为频繁使用的字段分配 1 -15 内的标识号
。另外 19000-19999 之内的标识号已经被预留,不可用。最大标识号为 2^29-1。

举个简单的栗子:

Package MYPACKAGE;
message Person {
    required string name=1;
    required int32 id=2;
    optional string email=3;

    enum PhoneType {
        MOBILE=0;
        HOME=1;
        WORK=2;
    }

    message PhoneNumber {
        required string number=1;
        optional PhoneType type=2 [default=HOME];
    }

    repeated PhoneNumber phone=4;
}

message AddressBook {repeated Person person=1;}

可以看到我们定义了一个包,报名为 MYPACKGE;

并且定义了结构化消息 Person 以及 AddressBook。在定义过程中还使用了枚举类型以及嵌套结构体消息。

目前 pb 所支持的标准数据类型如下:

说说编解码

从一个简单的官方示例开始看编解码:

message Test1 {optional int32 a = 1;}

Test1 数据结构中存在一个 key 为 a,假如我们将 a 赋值为 150,编码出来的结果为:
08 96 01
以上为 16 进制编码后的结果,08 为一个字节,表示为 00001000,由于第一位保留不使用,所以实际为 0001000,pb 协议规定后三位表示数据类型,即为 0,表示为 int32 或 int64 等数据类型。
(注:数据类型映射表如下:)

前四位 0001,即为 1,表示键 a 所对应的标识号,为 1。
虽说 a 键所定义的数据类型为 int32,但并不意味着我们需要用 4 个字节才存储这个 value,pb 编码中,在读取 varint 类型数据时,保留第一位来判断是否还有后续的字节需要读取。
则 96 表示 1001 0110,第一位为 1,表示还需要读取下一个字节,01 表示 00000001,首位为 0,不需要读取下一个字节,则 int 到这里读取结束。
最后只需要将读取到的结果拼接,即为我们需要的 int 的最终数值
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110
→ 10010110
→ 128 + 16 + 4 + 2 = 150
那么假如是字符串呢?

message Test2 {optional string b = 2;}

将 b 的 value 值设置为 testing,这时候需要编码的是字符串,结果会是这样:
12 07 74 65 73 74 69 6e 67
12 解码出来 表示为 2 号标识号,数据类型为 2。
07 表示后续需要读取 7 个字节。
后面的 7 个字节分别对应 testing 字符串的 ascii 编码。
聪明的你可能已经发现了,无论 string、byte 还是自定义结构体 message,repeated,都归属于数据类型 2,length-delimited,他们都有同一个特性,就是长度不确定,不可限制,所以他们的存储方式和字符串也是类似的。

message Test3 {optional Test1 c = 3;}

假如我要存储最初定义的 test1 结构,那么这个时候对应的编码结果是:
1a 03 08 96 01
1a 表示 数据类型为 2,标识号为 3,
03 表示有 3 个字节,
08 96 01 其实就是我们最初 test1 的编码结果。

nodejs 中的使用

在 nodejs 中,只需要引入 protobufjs 模块,便可以开心快乐地使用 pb 协议了。

package MY_NAMESPACE;

message Person {
    optional string name = 1; 
    optional int32 money = 2;
}
const protobuf = require('protobufjs')
protobuf.load("mytest.proto", function(err, appProto) {if (err)
      throw err;

  var Person = appProto.lookupType("MY_NAMESPACE.Person");
  var payload = { 
    name: "王二狗",
    money: 12
  };

  var errMsg = Person.verify(payload);
  if (errMsg)
      throw Error(errMsg);

  var message = Person.create(payload); 
  var buffer = Person.encode(payload).finish();
  console.log(buffer)

  var message = Person.decode(buffer);
  var object = Person.toObject(message, {
      longs: String,
      enums: String,
      bytes: String,
  });
  console.log(object)
});

可以在命令行看到相应的输出结果如下:

如果你仔细阅读了刚才所介绍的编解码,想必你也看懂了这个 buffer!

定义包结构、投入使用

在上述案例中,可以看到我们已经把要传输的数据转换成了 buffer,但是问题是还需要指定包名(命名空间)以及命令字,那么作为请求方怎么让服务端知道我们的请求是对应哪一个 proto 文件、哪一个命令字呢?
我们需要在额外传输一份数据来告诉服务端 namespace 和 cmdname,这就需要我们额外定义一个头部信息。
如果接口要鉴权呢?又有其他额外信息要传输呢?也是一样的,定义一个通用的头部信息 proto 文件,在发送请求时将头部 buffer 和内容 buffer 一起传输。
而头部 buffer 和内容 buffer 一般都是非定长的,需要我们提供额外的长度信息,所以你的包结构可以设计成这样:

总结

学完了 pb 协议,知道了编解码原理,学习了如何使用,也知道 pb 协议包要怎么设计,本节课到此结束!同学们,下课!

更多问题以及新特性请戳官网:https://developers.google.com…

正文完
 0