Protobuf在Web端的使用应用
1. Protobuf 简介
Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API(即时通讯网注:Protobuf官方工程主页上显示的已支持的开发语言多达10种,分别有:C++、Java、Python、Objective-C、C#、JavaNano、JavaScript、Ruby、Go、PHP,基本上主流的语言都已支持,详见工程主页 protobuf)。
2. protobuf在Web端的使用
目录
. |… |– protobuf # protobuf 目录 | |– Class.proto | |– Grade.proto | |– MessageType.proto | |– School.proto |– server # 服务端目录 | |– db.ts | |– error.ts | |– index.ts | |– real.ts | |– utils.ts |– src |… | |– protoTs # protobuf生成ts目录 | | |– Class.ts | | |– Grade.ts | | |– MessageType.ts | | |– School.ts
|… `– vite.config.ts
准备基础
- 需要预先了解下Protobuf3的基础知识
- 推荐npm全局安装esno,好处是可以直接解析运行ts而不借助其他库
Web端使用vue3、TS、vite
依赖安装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# 安装 npm install protobufjs protoc @protobuf-ts/runtime ## protobufjs 核心包 ## protoc proto文件的编译器 ## @protobuf-ts/runtime ts运行时工具 # 众所周知,在vite构建工具下,无法使用commonjs模块,而protobufjs库最新版是无法安装的 npm install vite-plugin-commonjs @protobuf-ts/plugin -D ## vite-plugin-commonjs 将 CommonJS 模块转换为 ES6 模块 ## @protobuf-ts/plugin proto编译器插件 # 在vite.config.ts中添加如下配置,使用默认配置即可 import commonjs from 'vite-plugin-commonjs'; ... plugins: [ ..., commonjs(), ],
script配置
- [protobuf-ts配置文档][protobuf-ts配置文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14
"scripts": { ... "remove": "rm -rf src/protoTs/*", "generate": "npx protoc --ts_out src/protoTs/ --ts_opt long_type_string --ts_opt generate_dependencies --proto_path protobuf protobuf/*.proto", "protobuf": "npm run remove && npm run generate", "server": "esno server/index.ts" } # 将已经定义的protobuf生成ts,具体配置请查阅[protobuf-ts配置文档] pnpm run generate # 清理旧的ts并生成新的ts pnpm run protobuf
定义protobuf文件
syntax = "proto3"; package framework; // 消息请求体 message PBMessageRequest { uint32 type = 1; // 消息类型 optional bytes messageData = 2; // 请求数据 uint64 timestamp = 3; // 客户端时间戳 string version = 4; // api版本号 string token = 5; // 用户登录后服务器返回的 token,用于登录校验 } // 消息响应体 message PBMessageResponse { uint32 type = 1; // 消息类型 optional bytes messageData = 2; // 返回数据 optional uint32 resultCode = 3; // 返回的结果码 optional string resultInfo = 4; // 返回的结果消息提示文本(用于错误提示) } // 所有的接口 enum PBMessageType { getSchoolList = 0; // 获取学校列表, PBSchoolListReq => PBSchoolListRsp getSchoolTreeList = 1; // 获取学校树形列表, {} => PBSchoolListRsp }请求接口过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import { PBMessageRequest, PBMessageResponse, PBMessageType } from '@/protoTs/MessageType'; // 请求数据 const reqData = { token: token, type: _msgType, version: apiVersion, timeStamp: Date.now(), messageData: requestBody, }; // 将对象序列化成请求体实例 const req = PBMessageRequest.create(reqData); // 将请求数据encode成二进制,encode是proto.js提供的方法 function transformRequest(data: PBMessageRequest) { return PBMessageRequest.toBinary(data); }
处理响应过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
import { PBMessageRequest, PBMessageResponse, PBMessageType } from '@/protoTs/MessageType'; function isArrayBuffer (obj: any) { return Object.prototype.toString.call(obj) === '[object ArrayBuffer]'; } function transformResponseFactory(responseType: any) { return function transformResponse(rawResponse: number | number[] | null | undefined) { // 判断response是否是arrayBuffer if (rawResponse == null || !isArrayBuffer(rawResponse)) { return rawResponse; } try { // 请求时已设定responseType为'arraybuffer',所以这里需要预先处理数据 const enc = new TextDecoder('utf-8'); const raw = JSON.parse(enc.decode(new Uint8Array(rawResponse as number[]))); const buf = protobuf.util.newBuffer(Object.values(raw)); // const buf = protobuf.util.newBuffer(rawResponse); // decode响应体 const decodedResponse = PBMessageResponse.fromBinary(buf); if (decodedResponse.messageData && responseType) { // const model = protoRoot.lookup(responseType); // decodedResponse.messageData = model.decode(decodedResponse.messageData); decodedResponse.messageData = responseType.fromBinary(decodedResponse.messageData); } return decodedResponse; } catch (err) { throw err; } } }
完整请求封装代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
import protobuf from 'protobufjs' import { storage } from './storage'; import httpService, { apiVersion } from '@/config/services'; import { PBMessageRequest, PBMessageResponse, PBMessageType } from '@/protoTs/MessageType'; type ImesgType = keyof typeof PBMessageType; function getMessageTypeValue(msgType: ImesgType) { return PBMessageType[msgType]; } /** * * @param {*} path 接口地址 * @param {*} msgType 接口名称 * @param {*} responseType 返回体类型 * @param {*} requestBody 请求体参数 */ function request(path: string, msgType: ImesgType, responseType: any, requestBody?: Uint8Array, ) { // 得到api的枚举值 const token = storage.get('token'); const _msgType = getMessageTypeValue(msgType); // 请求需要的数据 const reqData = { token: token, type: _msgType, version: apiVersion, timeStamp: Date.now(), messageData: requestBody, }; // 将对象序列化成请求体实例 const req = PBMessageRequest.create(reqData); const transformResponse = transformResponseFactory(responseType); httpService.defaults.headers['satoken'] = token; // 调用axios发起请求 // 这里用到axios的配置项:transformRequest和transformResponse // transformRequest 发起请求时,调用transformRequest方法,目的是将req转换成二进制 // transformResponse 对返回的数据进行处理,目的是将二进制转换成真正的json数据 return httpService .post(path, req, { transformRequest, transformResponse }) .then(({ data, status }) => { if (status !== 200) throw new Error('服务器异常'); return data; },(err) => { throw err; }) } // 将请求数据encode成二进制 function transformRequest(data: PBMessageRequest) { return PBMessageRequest.toBinary(data); } function isArrayBuffer (obj: any) { return Object.prototype.toString.call(obj) === '[object ArrayBuffer]'; } function transformResponseFactory(responseType: any) { return function transformResponse(rawResponse: number | number[] | null | undefined) { // 判断response是否是arrayBuffer if (rawResponse == null || !isArrayBuffer(rawResponse)) { return rawResponse; } try { // 请求时已设定responseType为'arraybuffer',所以这里需要预先处理数据 const enc = new TextDecoder('utf-8'); const raw = JSON.parse(enc.decode(new Uint8Array(rawResponse as number[]))); const buf = protobuf.util.newBuffer(Object.values(raw)); // decode响应体 const decodedResponse = PBMessageResponse.fromBinary(buf); if (decodedResponse.messageData && responseType) { decodedResponse.messageData = responseType.fromBinary(decodedResponse.messageData); } return decodedResponse; } catch (err) { throw err; } } } // 在request下添加一个方法,方便用于处理请求参数 request.create = function (pbConstruct: any, data: any) { const _data = pbConstruct.create(data); return pbConstruct.toBinary(_data); } export default request;
3. protobuf在服务端的使用,以express为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { PBMessageResponse } from '@/protoTs/MessageType';
function genResponse(school: PBSchoolListRsp | undefined) {
const data = {
type: 2,
resultCode: 200,
messageData: PBSchoolListRsp.toBinary(school!),
};
return PBMessageResponse.toBinary(data);
}
app.post('/schoolTree', express.text({ type: '*/*' }), async function(req, res) {
// 请求头的信息
console.log('headers', req.headers);
// 因为请求体是buff的文本,需要先转化为buff
const buffer = Buffer.from(req.body);
// 从req.body解码得到请求体
const _reqData = PBMessageRequest.fromBinary(buffer);
console.log('reqdata', _reqData);
globalMap.clear();
try {
if (req.headers.satoken) {
globalMap.set('satoken', req.headers.satoken);
service.defaults.headers['satoken'] = req.headers.satoken;
await genData(2, generTreeSchool);
const _resData = genResponse(treeSchools);
// console.log('respdata', _resData);
res.send(_resData);
} else {
errorHandle(res, { succ: false, code: 902, msg: '未登录' });
}
} catch (error: any) {
errorHandle(res, error);
}
});
export function errorHandle(res: any, error: any) {
const resdata = PBMessageResponse.toBinary({
type: 1,
resultInfo: error?.msg,
resultCode: Number(error?.code),
});
res.send(resdata);
}
4. protobuf使用总结
请求时
按照对应业务定义的proto规则,对请求数据编码,再按照请求封装的数据结构对整个请求体统一编码,最后发送数据
响应时
先将文本类型的buffer转化为数据buffer,然后按照响应封装的数据结构对数据buffer解码,再按照对应的业务定义的proto规则,对已经初步解码的数据再次解码,最后获取真实数据