為了一份Mock數(shù)據(jù),開(kāi)啟了Protobuf的救贖之路
一、背景
近期在做一個(gè)需求,該需求需要和后端進(jìn)行交互,為了并行開(kāi)發(fā),就跟后端產(chǎn)生了如下的對(duì)話:
前端:老鐵,可以給份mock數(shù)據(jù)嗎?
后端:mock數(shù)據(jù)太麻煩了,你自己來(lái)吧!!!
前端:我怎么知道數(shù)據(jù)長(zhǎng)啥樣,如何mock呀!(可憐)
后端:按照約定的接口mock就行,直接給我拋出了一個(gè)proto文件
前端:此時(shí)已經(jīng)一臉懵逼狀態(tài),proto是個(gè)啥?如何根據(jù)proto來(lái)mock一份數(shù)據(jù)?后端為什么要用proto,JSON不香嗎?為了彌補(bǔ)上自己欠缺的一環(huán),開(kāi)啟了Protobuf的救贖之路。
二、Protobuf是什么?
Protobuf 作為一種跨平臺(tái)、語(yǔ)言無(wú)關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)的方法,已廣泛應(yīng)用于網(wǎng)絡(luò)數(shù)據(jù)交換及存儲(chǔ)。其目前已經(jīng)支持的開(kāi)發(fā)語(yǔ)言有多種(C++、Java、Python、Objective-C、C#、JavaNano、JavaScript、Ruby、Go、PHP),詳情可參考(https://github.com/52im/protobuf)。其具有如下優(yōu)缺點(diǎn):
優(yōu)點(diǎn)
(1)序列化后體積小,適合網(wǎng)絡(luò)傳輸
(2)支持跨平臺(tái)、多語(yǔ)言
(3)具有較好的升級(jí)和兼容性(具有向后兼容的特性,更新數(shù)據(jù)結(jié)構(gòu)以后,老版本依舊可以兼容)
(4)序列化和反序列化的速度較快
缺點(diǎn)
Protobuf是二進(jìn)制協(xié)議,編碼后的數(shù)據(jù)可讀性差
三、Protobuf的結(jié)構(gòu)
Protobuf用法的使用有很多,本次就通過(guò)一個(gè)例子來(lái)看看其基本使用,具體使用可以在網(wǎng)上搜索相關(guān)文檔進(jìn)行學(xué)習(xí)。
- syntax = "proto2"
- package transferData;
- message transferMessage {
- required string name = 1;
- required int32 age = 2;
- enum SexEnum {
- Boy = 0;
- Girl = 1;
- }
- optional SexEnum SexEnum = 3;
- }
1.syntax = "proto2";
該行用于指定語(yǔ)法版本,目前有兩個(gè)版本proto2和proto3,兩個(gè)版本不兼容,如果不指定,默認(rèn)語(yǔ)法是proto2.
2.package transferData;
用于定義該包的包名;
3.message
message是Protobuf中最基本的數(shù)據(jù)單元,其中可以嵌套message或其它的基礎(chǔ)數(shù)據(jù)類(lèi)型的成員;
4.屬性
message中的每一行就是一個(gè)屬性,例如required string name = 1,其組成如下所示:
標(biāo)注 | 類(lèi)型 | 屬性名 | 屬性順序號(hào) | [options] |
---|---|---|---|---|
required | string | name | = 1 | 一些可選項(xiàng) |
(1)標(biāo)注有三種:
required:必選屬性;
optional:可選屬性;
repeated:重復(fù)字段,類(lèi)似于動(dòng)態(tài)數(shù)組;
(2)類(lèi)型有多種,每種語(yǔ)言不同,例如:int32、int64、int、float、double、string等;
(3)屬性名:用于表征該屬性的名稱;
(4)屬性順序號(hào):protobuf為了提高數(shù)據(jù)的壓縮和可選性等功能定義的,需要按照順序進(jìn)行定義,且不允許有重復(fù);
(5)[options]:protobuf提供了一些內(nèi)置的options可供選擇想,可大大提高protobuf的擴(kuò)展性。
5.enum
定義消息類(lèi)型時(shí),可能需要某字段值是一些預(yù)設(shè)值之一,此時(shí)枚舉類(lèi)型就能夠發(fā)揮作用了。
注:protobuf還有很多用法,此處只做了簡(jiǎn)單介紹,有喜歡的同學(xué)可進(jìn)一步自己深入學(xué)習(xí)。
四、實(shí)戰(zhàn)
聊了那么多,下面就進(jìn)入實(shí)戰(zhàn)環(huán)節(jié),實(shí)戰(zhàn)將在node運(yùn)行環(huán)境下,構(gòu)建TCP連接,然后由客戶端發(fā)送經(jīng)過(guò)Protobuf序列化的內(nèi)容至服務(wù)端,然后服務(wù)端接收到信息之后進(jìn)行解析,其中proto文件的序列化和反序列化將使用protobuf.js包,其是一個(gè)純 JavaScript 實(shí)現(xiàn),支持node.js和瀏覽器。它易于使用,速度極快,并且可以使用.proto文件開(kāi)箱即用!(https://www.npmjs.com/package/protobufjs)
4.1 基本使用
本次解析.proto文件使用的是protobuf.js包,常用的方法主要有以下幾個(gè):
1.load()
用該函數(shù)加載對(duì)應(yīng)的.proto文件,加載完成之后才能夠使用里面的message以及進(jìn)行后續(xù)的操作;
2.lookupType()
在加載完.proto后,需要對(duì)使用的message進(jìn)行初始化,即完成message實(shí)例化的過(guò)程;
3.verify()
該函數(shù)用于驗(yàn)證普通對(duì)象是某滿足對(duì)應(yīng)的message結(jié)構(gòu);
4.encode()
編碼一個(gè)message實(shí)例或者可利用的普通js對(duì)象;
5.decode()
解碼buffer至一個(gè)message實(shí)例,解碼失敗會(huì)排除錯(cuò)誤;
6.create()
從一系列屬性創(chuàng)建一個(gè)新的message實(shí)例,其優(yōu)于通過(guò)fromObject創(chuàng)建,是由于其不會(huì)產(chǎn)生冗余的轉(zhuǎn)換;
7.fromObject()
將任何無(wú)效的普通js對(duì)象轉(zhuǎn)換為message實(shí)例;
8.toObject()
轉(zhuǎn)換一個(gè)message實(shí)例去一個(gè)任意的普通js對(duì)象。
該庫(kù)的使用還有一些其它方法,可以通過(guò)看其對(duì)應(yīng)文檔進(jìn)行學(xué)習(xí)。對(duì)于上述轉(zhuǎn)換關(guān)系如下圖所示(來(lái)自于官方文檔):
4.2 服務(wù)端
其是服務(wù)端,當(dāng)接收到客戶端發(fā)送的消息后,利用protobufjs庫(kù)中的decode函數(shù)進(jìn)行解析,獲取解析后的結(jié)果。
- const net = require('net');
- const protobuf = require('protobufjs');
- const decodeData = data => {
- protobuf.load('./transfer.proto')
- .then(root => {
- const transferMessage = root.lookupType('transferData.transferMessage');
- const result = transferMessage.decode(data);
- console.log(result); // transferMessage { name: '狍狍', age: 1, sexEnum: 1 }
- })
- .catch(console.log);
- }
- const server = net.createServer(socket => {
- socket.on('data', data =>{
- decodeData(data);
- });
- socket.on('close', () => {
- console.log('client disconnected!!!');
- });
- });
- server.on('error', err => {
- throw new Error(err);
- });
- server.listen(8081, () => {
- console.log('server port is 8081');
- });
4.3 客戶端
其是客戶端對(duì)應(yīng)的代碼,利用protobufjs庫(kù)進(jìn)行相應(yīng)的操作,將序列化后的內(nèi)容發(fā)送至服務(wù)端。
- const net = require('net');
- const protobuf = require('protobufjs');
- const data = {
- name: '狍狍',
- age: 1,
- sexEnum: 1
- };
- let client = new net.Socket();
- client.connect({
- port: 8081
- });
- client.on('connect', () => {
- setMessage(data);
- });
- client.on('data', data => {
- console.log(data);
- client.end();
- });
- function setMessage(data) {
- protobuf.load('./transfer.proto')
- .then(root =>{
- // 根據(jù)proto文件中的內(nèi)容對(duì)message進(jìn)行實(shí)例化
- const transferMessage = root.lookupType('transferData.transferMessage');
- // 驗(yàn)證
- const errMsg = transferMessage.verify(data);
- console.log('errMsg', errMsg);
- if (errMsg) {
- throw new Error(errMsg);
- }
- // 轉(zhuǎn)換為message實(shí)例
- const messageFromObj = transferMessage.fromObject(data);
- console.log('messageFromObj', messageFromObj);
- // 編碼
- const buffer = transferMessage.encode(messageFromObj).finish();
- console.log(buffer);
- // 發(fā)送
- client.write(buffer);
- })
- .catch(console.log);
- }