1

什么是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所支持的标准数据类型如下:

clipboard.png

说说编解码

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

message Test1 {
      optional int32 a = 1;
}

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

clipboard.png

前四位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)
});

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

clipboard.png

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

定义包结构、投入使用

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

clipboard.png

总结

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

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

clipboard.png


曾培森
1.1k 声望875 粉丝

学海无涯皮蛋瘦肉粥