Protobuf 是一种用于 序列化
和 反序列化
对象的格式规范(rpc 通信协议)。其他的序列化方式还有 hessian、thrift 等等。它们与 JSON 类似,又有些不同。下面介绍在 Node.js 中如何使用 protobufs,以及如何在 Express 中使用它。
注意❗️ RPC 可以直接建立在 tcp 之上,也可以建立在 http 协议之上。对于 rpc 来说,这都是一样的,都是把通讯的内容塞进报文里。
其实 http 是最常用的承载 RPC 的通信协议之一。而且我们可以在 http 上传输 xml 和 json 这样的文本协议,也可以是 protobuf 和 thrift 这样的二进制协议,这都不是问题。大家常用的 REST api 就可以很好的封装成 rpc。当然,http 这种协议是笨重一些,但它的穿透性比较好,配套的设施也齐全,也比较简单直观,还是比较收欢迎的。比如说著名的 grpc 就通过 http 来传输。
Protobuf 介绍
Protobuf 与 非结构化格式(如 JSON、XML)最大的区别在于,你必须为 protobufs 定义数据类型,最常用的方式是定义 .proto
文件。下面是 .proto
文件的示例,它将 User
定义为具有两个属性的对象:name
字符串;age
整型。
package userpackage;
syntax = "proto3";
message User {
string name = 1;
int32 age = 2;
}
npm 中有一些处理 protobuf 的包,其中下载量最多的是 protobufjs。所以,我们在本文中用它来演示。首先,导入 protobufjs
并且调用 load()
函数来告诉 protobufjs 加载 user.proto
文件:
const protobuf = require('protobufjs');
run().catch(err => console.log(err));
async function run() {
const root = await protobuf.load('user.proto');
const User = root.lookupType('userpackage.User');
}
现在,protobufjs 知道了 User 类型, 你可以使用 User
来验证或序列化对象。下面是调用 User.verify()
函数来验证对象的 name
和 age
属性类型是否正确:
console.log(User.verify({ name: 'test', age: 2 })); // null
console.log(User.verify({ propertyDoesntExist: 'test' })); // null
console.log(User.verify({ age: 'not a number' })); // "age: integer expected"
注意,protobuf 定义的属性默认非必填。所以上面例子中,对于没有定义 name
属性的对象 verify()
并没有报错。
Encoding and Decoding
虽然 protobufs 可以做基本的数据验证,但它最主要用于 序列化
和 反序列化
对象。序列化对象,你可以调用 User.encode(obj).finish()
, 它会返回一个包含该对象 protobuf 表示的 buffer。
const buf = User.encode({ name: 'Bill', age: 30 }).finish();
console.log(Buffer.isBuffer(buf)); // true
console.log(buf.toString('utf8')); // Gnarly string that contains "Bill"
console.log(buf.toString('hex')); // 0a0442696c6c101e
反序列化对象,你可以调用 decode()
函数:
const buf = User.encode({ name: 'Bill', age: 30 }).finish();
const obj = User.decode(buf);
console.log(obj); // User { name: 'Bill', age: 30 }
Protobufs 通常用于在网络中传输数据,作为 JSON / XML 的替代方案。原因之一是序列化的 protobuf 体积要小得多,因为它不需要包含属性名。例如,下面展示了分别使用 protobuf 和 JSON 来表示数据,在大小上的差异:
const asProtobuf = User.encode({ name: 'Joe', age: 27 }).finish();
const asJSON = JSON.stringify({ name: 'Joe', age: 27 });
asProtobuf.length; // 7
asJSON.length; // 23, 3x bigger!
由于 protobuf 提前在 .proto
文件中知道了对象的 keys,所以 protobufs 比 JSON 更节省空间。虽然你可以使用 Mongoose aliases 等模式来缩小 JSON 的键名长度,但你永远不可能缩到和 protobufs 一样小,因为 protobufs 根本不序列化键名!
HTTP 发送 Protobufs
将数据存储在文件中或通过网络发送,protobufs 才会有价值。因此,让我们看看如何使用 protobufs 来处理 HTTP 请求和响应。下面是使用 Axios 向 Express 服务器发送 protobufs 的示例:
const axios = require('axios');
const express = require('express');
const protobuf = require('protobufjs');
const app = express();
run().catch(err => console.log(err));
async function run() {
const root = await protobuf.load('user.proto');
const doc = { name: 'Bill', age: 30 };
const User = root.lookupType('userpackage.User');
app.get('/user', function(req, res) {
res.send(User.encode(doc).finish());
});
app.post('/user', express.text({ type: '*/*' }), function(req, res) {
// Assume `req.body` contains the protobuf as a utf8-encoded string
const user = User.decode(Buffer.from(req.body));
Object.assign(doc, user);
res.end();
});
await app.listen(3000);
let data = await axios.get('http://localhost:3000/user').then(res => res.data);
// "Before POST User { name: 'Bill', age: 30 }"
console.log('Before POST', User.decode(Buffer.from(data)));
const postBody = User.encode({ name: 'Joe', age: 27 }).finish();
await axios.post('http://localhost:3000/user', postBody).
then(res => res.data);
data = await axios.get('http://localhost:3000/user').then(res => res.data);
// "After POST User { name: 'Joe', age: 27 }"
console.log('After POST', User.decode(Buffer.from(data)));
}
总结
Protobufs 是通过网络传输数据的一种不错的选择,尤其是与 grpc 结合在一起使用。protobufs 提供了最小的类型检查机制,因为 decode()
会在类型异常时报错,并且 protobufs 比 JSON 处理的数据更小!然而,缺点是 protobuf 不是人类可读的(不像 JSON 或 XML 那样的文本格式),而且没有 .proto
文件就无法解码数据。protobufs 更难调试,也更难使用,但如果你非常关心通过网络发送的数据大小,那么 protobufs 就非常有用!
你也可以在 Github 上找到本文的示例代码。