什么是 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…