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