最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

IM通訊協(xié)議專題學習(七):手把手教你如何在NodeJS中從零使用Protobuf

2023-01-05 16:33 作者:nickkckckck  | 我要投稿

1、前言

Protobuf是Google開源的一種混合語言數(shù)據(jù)標準,已被各種互聯(lián)網(wǎng)項目大量使用。

Protobuf最大的特點是數(shù)據(jù)格式擁有極高的壓縮比,這在移動互聯(lián)時代是極具價值的(因為移動網(wǎng)絡(luò)流量到目前為止仍然昂貴的),如果你的APP能比競品更省流量,無疑這也將成為您產(chǎn)品的亮點之一?,F(xiàn)在,尤其IM、消息推送這類應(yīng)用中,Protobuf的應(yīng)用更是非常廣泛,基于它的優(yōu)秀表現(xiàn),微信和手機QQ這樣的主流IM應(yīng)用也早已在使用它。

現(xiàn)在隨著WebSocket協(xié)議的越來越成熟,瀏覽器支持的越來越好,Web端的即時通訊應(yīng)用也逐漸擁有了真正的“實時”能力,相關(guān)的技術(shù)和應(yīng)用也是層出不窮,而Protobuf也同樣可以用在WebSocket的通信中。而且目前比較活躍的WebSocket開源方案中,都是用NodeJS實現(xiàn)的,比如:socket.io和sockjs都是如此,因而本文介紹Protobuf在NodeJS上的使用,也恰是時候。

學習交流:

- 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》

- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點此)

(本文同步發(fā)布于:http://www.52im.net/thread-4111-1-1.html)

2、系列文章

本文是系列文章中的第?7?篇,本系列總目錄如下:

  1. 《IM通訊協(xié)議專題學習(一):Protobuf從入門到精通,一篇就夠!》

  2. 《IM通訊協(xié)議專題學習(二):快速理解Protobuf的背景、原理、使用、優(yōu)缺點》

  3. 《IM通訊協(xié)議專題學習(三):由淺入深,從根上理解Protobuf的編解碼原理》

  4. 《IM通訊協(xié)議專題學習(四):從Base64到Protobuf,詳解Protobuf的數(shù)據(jù)編碼原理》

  5. 《IM通訊協(xié)議專題學習(五):Protobuf到底比JSON快幾倍?全方位實測!》

  6. 《IM通訊協(xié)議專題學習(六):手把手教你如何在Android上從零使用Protobuf》(稍后發(fā)布..)

  7. 《IM通訊協(xié)議專題學習(七):手把手教你如何在NodeJS中從零使用Protobuf》(* 本文

  8. 《IM通訊協(xié)議專題學習(八):金蝶隨手記團隊的Protobuf應(yīng)用實踐(原理篇) 》(稍后發(fā)布..)

  9. 《IM通訊協(xié)議專題學習(九):金蝶隨手記團隊的Protobuf應(yīng)用實踐(實戰(zhàn)篇) 》(稍后發(fā)布..)

3、Protobuf是個什么鬼?

Protocol Buffer(下文簡稱Protobuf)是Google提供的一種數(shù)據(jù)序列化協(xié)議,下面是我從網(wǎng)上找到的Google官方對Protobuf的定義:

Protocol Buffers 是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,可以用于結(jié)構(gòu)化數(shù)據(jù)序列化,很適合做數(shù)據(jù)存儲或 RPC 數(shù)據(jù)交換格式。它可用于通訊協(xié)議、數(shù)據(jù)存儲等領(lǐng)域的語言無關(guān)、平臺無關(guān)、可擴展的序列化結(jié)構(gòu)數(shù)據(jù)格式。目前提供了 C++、Java、Python 三種語言的 API。

道理我們都懂,然后并沒有什么卵用,看完上面這段定義,對于Protobuf是什么我還是一臉懵逼。

4、NodeJS開發(fā)者為何要跟Protobuf打交道

作為JavaScript開發(fā)者,對我們最友好的數(shù)據(jù)序列化協(xié)議當然是大名鼎鼎的JSON啦!我們本能的會想protobuf是什么鬼?還我JSON!

這就要說到protobuf的歷史了。

Protobuf由Google出品,08年的時候Google把這個項目開源了,官方支持C++,Java,C#,Go和Python五種語言,但是由于其設(shè)計得很簡單,所以衍生出很多第三方的支持,基本上常用的PHP,C,Actoin Script,Javascript,Perl等多種語言都已有第三方的庫。

由于protobuf協(xié)議相較于之前流行的XML更加的簡潔高效(后面會提到這是為什么),因此許多后臺接口都是基于protobuf定制的數(shù)據(jù)序列化協(xié)議。而作為NodeJS開發(fā)者,跟C++或JAVA編寫的后臺服務(wù)接口打交道那是家常便飯的事兒,因此我們很有必要掌握protobuf協(xié)議。

為什么說使用使用類似protobuf的二進制協(xié)議通信更好呢?

  • 1)二進制協(xié)議對于電腦來說更容易解析,在解析速度上是http這樣的文本協(xié)議不可比擬的;

  • 2)有tcp和udp兩種選擇,在一些場景下,udp傳輸?shù)男蕰撸?/p>

  • 3)在后臺開發(fā)中,后臺與后臺的通信一般就是基于二進制協(xié)議的。甚至某些native app和服務(wù)器的通信也選擇了二進制協(xié)議(例如騰訊視頻)。但由于web前端的存在,后臺同學往往需要特地開發(fā)維護一套http接口專供我們使用,如果web也能使用二進制協(xié)議,可以節(jié)省許多后臺開發(fā)的成本。

在大公司,最重要的就是優(yōu)化效率、節(jié)省成本,因此二進制協(xié)議明顯優(yōu)于http這樣的文本協(xié)議。

下面舉兩個簡單的例子,應(yīng)該有助于我們理解protobuf。

5、選擇支持protobuf的NodeJS第三方模塊

當前在Github上比較熱門的支持protobuf的NodeJS第三方模塊有如下3個:

?

根據(jù)star數(shù)和文檔完善程度兩方面綜合考慮,我們決定選擇protobuf.js(后面2個的地址:Google protobuf js、protocol-buffers)。

6、使用 Protobuf 和NodeJS開發(fā)一個簡單的例子

6.1 概述

我打算使用 Protobuf 和NodeJS開發(fā)一個十分簡單的例子程序。該程序由兩部分組成:第一部分被稱為 Writer,第二部分叫做 Reader。

Writer 負責將一些結(jié)構(gòu)化的數(shù)據(jù)寫入一個磁盤文件,Reader 則負責從該磁盤文件中讀取結(jié)構(gòu)化數(shù)據(jù)并打印到屏幕上。

準備用于演示的結(jié)構(gòu)化數(shù)據(jù)是 HelloWorld,它包含兩個基本數(shù)據(jù):

  • 1)ID:為一個整數(shù)類型的數(shù)據(jù);

  • 2)Str:這是一個字符串。

6.2 書寫.proto文件

首先我們需要編寫一個 proto 文件,定義我們程序中需要處理的結(jié)構(gòu)化數(shù)據(jù),在 protobuf 的術(shù)語中,結(jié)構(gòu)化數(shù)據(jù)被稱為 Message。proto 文件非常類似 java 或者 C 語言的數(shù)據(jù)定義。代碼清單 1 顯示了例子應(yīng)用中的 proto 文件內(nèi)容。

清單 1. proto 文件:

package lm;

message helloworld

{

???required int32???? id = 1;? // ID

???required string??? str = 2;? // str

???optional int32???? opt = 3;? //optional field

}

一個比較好的習慣是認真對待 proto 文件的文件名。比如將命名規(guī)則定于如下:

packageName.MessageName.proto

在上例中,package 名字叫做 lm,定義了一個消息 helloworld,該消息有三個成員,類型為 int32 的 id,另一個為類型為 string 的成員 str。opt 是一個可選的成員,即消息中可以不包含該成員。1、2、3這幾個數(shù)字是這三個字段的唯一標識符,這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不能夠再改變。

6.3 編譯 .proto 文件

我們可以使用protobuf.js提供的命令行工具來編譯 .proto 文件。

用法:

# pbjs <filename> [options] [> outFile]

我們來看看options:

??--help, -h??????? Show help? [boolean] 查看幫助

??--version, -v???? Show version number? [boolean] 查看版本號

??--source, -s????? Specifies the source format. Valid formats are:

???????????????????????json?????? Plain JSON descriptor

???????????????????????proto????? Plain .proto descriptor

指定來源文件格式,可以是json或proto文件。

??--target, -t????? Specifies the target format. Valid formats are:

???????????????????????amd??????? Runtime structures as AMD module

???????????????????????commonjs?? Runtime structures as CommonJS module

???????????????????????js???????? Runtime structures

???????????????????????json?????? Plain JSON descriptor

???????????????????????proto????? Plain .proto descriptor

指定生成文件格式,可以是符合amd或者commonjs規(guī)范的js文件,或者是單純的js/json/proto文件。

??--using, -u?????? Specifies an option to apply to the volatile builder

????????????????????loading the source, e.g. convertFieldsToCamelCase.

??--min, -m???????? Minifies the output.? [default: false] 壓縮生成文件

??--path, -p??????? Adds a directory to the include path.

??--legacy, -l????? Includes legacy descriptors from google/protobuf/ if

????????????????????explicitly referenced.? [default: false]

??--quiet, -q?????? Suppresses any informatory output to stderr.? [default: false]

??--use, -i???????? Specifies an option to apply to the emitted builder

????????????????????utilized by your program, e.g. populateAccessors.

??--exports, -e???? Specifies the namespace to export. Defaults to export

????????????????????the root namespace.

??--dependency, -d? Library dependency to use when generating classes.

????????????????????Defaults to 'protobufjs' for CommonJS, 'ProtoBuf' for

????????????????????AMD modules and 'dcodeIO.ProtoBuf' for classes.

重點關(guān)注- -target就好,由于我們是在Node環(huán)境中使用,因此選擇生成符合commonjs規(guī)范的文件。

命令如下:

# ./pbjs ../../lm.message.proto? -t commonjs > ../../lm.message.js

得到編譯后的符合commonjs規(guī)范的js文件:

module.exports = require("protobufjs").newBuilder({})['import']({

????"package": "lm",

????"messages": [

????????{

????????????"name": "helloworld",

????????????"fields": [

????????????????{

????????????????????"rule": "required",

????????????????????"type": "int32",

????????????????????"name": "id",

????????????????????"id": 1

????????????????},

????????????????{

????????????????????"rule": "required",

????????????????????"type": "string",

????????????????????"name": "str",

????????????????????"id": 2

????????????????},

????????????????{

????????????????????"rule": "optional",

????????????????????"type": "int32",

????????????????????"name": "opt",

????????????????????"id": 3

????????????????}

????????????]

????????}

????]

}).build();

6.4 編寫 Writer

var HelloWorld = require('./lm.helloworld.js')['lm']['helloworld'];

var fs = require('fs');

// 除了這種傳入一個對象的方式, 你也可以使用get/set 函數(shù)用來修改和讀取結(jié)構(gòu)化數(shù)據(jù)中的數(shù)據(jù)成員

varhw = newHelloWorld({

????'id': 101,

????'str': 'Hello'

})

varbuffer = hw.encode();

fs.writeFile('./test.log', buffer.toBuffer(), function(err) {

????if(!err) {

????????console.log('done!');

????}

});

6.5 編寫Reader

var HelloWorld = require('./lm.helloworld.js')['lm']['helloworld'];

var fs = require('fs');

var buffer = fs.readFile('./test.log', function(err, data) {

????if(!err) {

????????console.log(data); // 來看看Node里的Buffer對象長什么樣子。

????????var message = HelloWorld.decode(data);

????????console.log(message);

????}

})

6.6 運行結(jié)果

由于我們沒有在Writer中給可選字段opt字段賦值,因此Reader讀出來的opt字段值為null。

這個例子本身并無意義,但只要您稍加修改就可以將它變成更加有用的程序。比如將磁盤替換為網(wǎng)絡(luò) socket,那么就可以實現(xiàn)基于網(wǎng)絡(luò)的數(shù)據(jù)交換任務(wù)。而存儲和交換正是 Protobuf 最有效的應(yīng)用領(lǐng)域。

7、使用 Protobuf 和NodeJS實現(xiàn)基于網(wǎng)絡(luò)數(shù)據(jù)交換的例子

俗話說得好:“世界上沒有什么技術(shù)問題是不能用一個helloworld的栗子解釋清楚的,如果不行,那就用兩個!”

在這個栗子中,我們來實現(xiàn)基于網(wǎng)絡(luò)的數(shù)據(jù)交換任務(wù)。

7.1 編寫.proto

cover.helloworld.proto文件:

package cover;

message helloworld {

????message helloCoverReq {

????????required string name = 1;

????}

????message helloCoverRsp {

????????required int32 retcode = 1;

????????optional string reply = 2;

????}

}

7.2 編寫client

一般情況下,使用 Protobuf 的人們都會先寫好 .proto 文件,再用 Protobuf 編譯器生成目標語言所需要的源代碼文件。將這些生成的代碼和應(yīng)用程序一起編譯。

可是在某些情況下,人們無法預先知道 .proto 文件,他們需要動態(tài)處理一些未知的 .proto 文件。比如一個通用的消息轉(zhuǎn)發(fā)中間件,它不可能預知需要處理怎樣的消息。這需要動態(tài)編譯 .proto 文件,并使用其中的 Message。

我們這里決定利用protobuf文件可以動態(tài)編譯的特性,在代碼中直接讀取proto文件,動態(tài)生成我們需要的commonjs模塊。

client.js:

var dgram = require('dgram');

var ProtoBuf = require("protobufjs");

var PORT = 33333;

var HOST = '127.0.0.1';

var builder = ProtoBuf.loadProtoFile("./cover.helloworld.proto"),

????Cover = builder.build("cover"),

????HelloCoverReq = Cover.helloworld.helloCoverReq;

????HelloCoverRsp = Cover.helloworld.helloCoverRsp;

?

var hCReq = newHelloCoverReq({

????name: 'R U coverguo?'

})

?

var buffer = hCReq.encode();

var socket = dgram.createSocket({

????type: 'udp4',

????fd: 8080

}, function(err, message) {

????if(err) {

????????console.log(err);

????}

????console.log(message);

});

var message = buffer.toBuffer();

socket.send(message, 0, message.length, PORT, HOST, function(err, bytes) {

????if(err) {

????????throw err;

????}

????console.log('UDP message sent to '+ HOST +':'+ PORT);

});

socket.on("message", function(msg, rinfo) {

????console.log("[UDP-CLIENT] Received message: "+ HelloCoverRsp.decode(msg).reply + " from "+ rinfo.address + ":"+ rinfo.port);

????console.log(HelloCoverRsp.decode(msg));

????socket.close();

????//udpSocket = null;

});

socket.on('close', function(){

????console.log('socket closed.');

});

socket.on('error', function(err){

????socket.close();

????console.log('socket err');

????console.log(err);

});

7.3 書寫server

server.js:

var PORT = 33333;

var HOST = '127.0.0.1';

var ProtoBuf = require("protobufjs");

var dgram = require('dgram');

var server = dgram.createSocket('udp4');

var builder = ProtoBuf.loadProtoFile("./cover.helloworld.proto"),

????Cover = builder.build("cover"),

????HelloCoverReq = Cover.helloworld.helloCoverReq;

????HelloCoverRsp = Cover.helloworld.helloCoverRsp;

server.on('listening', function() {

????var address = server.address();

????console.log('UDP Server listening on '+ address.address + ":"+ address.port);

});

server.on('message', function(message, remote) {

????console.log(remote.address + ':'+ remote.port +' - '+ message);

????console.log(HelloCoverReq.decode(message) + 'from client!');

????var hCRsp = newHelloCoverRsp({

????????retcode: 0,

????????reply: 'Yeah!I\'m handsome cover!'

????})

?

????var buffer = hCRsp.encode();

????var message = buffer.toBuffer();

????server.send(message, 0, message.length, remote.port, remote.address, function(err, bytes) {

????????if(err) {

????????????throw err;

????????}

????????console.log('UDP message reply to '+ remote.address +':'+ remote.port);

????})

});

server.bind(PORT, HOST);

7.4 運行結(jié)果

?

8、其他高級特性

8.1 嵌套Message

message Person {

??required string name = 1;

??required int32 id = 2;??????? // Unique ID number for this person.

??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 Person 中,定義了嵌套消息 PhoneNumber,并用來定義 Person 消息中的 phone 域。這使得人們可以定義更加復雜的數(shù)據(jù)結(jié)構(gòu)。

8.2 Import Message

在一個 .proto 文件中,還可以用 Import 關(guān)鍵字引入在其他 .proto 文件中定義的消息,這可以稱做 Import Message,或者 Dependency Message。

比如下例:

import common.header;

?message youMsg{

??required common.info_header header = 1;

??required string youPrivateData = 2;

?}

其中 ,common.info_header定義在common.header包內(nèi)。

Import Message 的用處主要在于提供了方便的代碼管理機制,類似 C 語言中的頭文件。您可以將一些公用的 Message 定義在一個 package 中,然后在別的 .proto 文件中引入該 package,進而使用其中的消息定義。

Google Protocol Buffer 可以很好地支持嵌套 Message 和引入 Message,從而讓定義復雜的數(shù)據(jù)結(jié)構(gòu)的工作變得非常輕松愉快。

9、總結(jié)一下Protobuf

9.1 優(yōu)點

簡單說來 Protobuf 的主要優(yōu)點就是:簡潔,快。

為什么這么說呢?

1)簡潔:

因為Protocol Buffer 信息的表示非常緊湊,這意味著消息的體積減少,自然需要更少的資源。比如網(wǎng)絡(luò)上傳輸?shù)淖止?jié)數(shù)更少,需要的 IO 更少等,從而提高性能。

對于代碼清單 1 中的消息,用 Protobuf 序列化后的字節(jié)序列為:

08 65 12 06 48 65 6C 6C 6F 77

而如果用 XML,則類似這樣:

31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65

?6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C

?6F 77 6F 72 6C 64 3E

一共 55 個字節(jié),這些奇怪的數(shù)字需要稍微解釋一下,其含義用 ASCII 表示如下:

<helloworld>

???<id>101</id>

???<name>hello</name>

</helloworld>

我相信與XML一樣同為文本序列化協(xié)議的JSON也不會好到哪里去。

2)快:

首先我們來了解一下 XML 的封解包過程:

  • 1)XML 需要從文件中讀取出字符串,再轉(zhuǎn)換為 XML 文檔對象結(jié)構(gòu)模型;

  • 2)之后,再從 XML 文檔對象結(jié)構(gòu)模型中讀取指定節(jié)點的字符串;

  • 3)最后再將這個字符串轉(zhuǎn)換成指定類型的變量。

這個過程非常復雜,其中將 XML 文件轉(zhuǎn)換為文檔對象結(jié)構(gòu)模型的過程通常需要完成詞法文法分析等大量消耗 CPU 的復雜計算。

反觀 Protobuf:它只需要簡單地將一個二進制序列,按照指定的格式讀取到編程語言對應(yīng)的結(jié)構(gòu)類型中就可以了。而消息的 decoding 過程也可以通過幾個位移操作組成的表達式計算即可完成。速度非常快。

9.2 缺點

作為二進制的序列化協(xié)議,它的缺點也顯而易見——人眼不可讀!

10、參考資料

[1]?Protobuf 官方開發(fā)者指南(中文譯版)

[2]?Protobuf官方手冊

[3]?Why do we use Base64?

[4]?The Base16, Base32, and Base64 Data Encodings

[5]?Protobuf從入門到精通,一篇就夠!

[5]?如何選擇即時通訊應(yīng)用的數(shù)據(jù)傳輸格式

[7]?強列建議將Protobuf作為你的即時通訊應(yīng)用數(shù)據(jù)傳輸格式

[8]?APP與后臺通信數(shù)據(jù)格式的演進:從文本協(xié)議到二進制協(xié)議

[9]?面試必考,史上最通俗大小端字節(jié)序詳解

[10]?移動端IM開發(fā)需要面對的技術(shù)問題(含通信協(xié)議選擇)

[11]?簡述移動端IM開發(fā)的那些坑:架構(gòu)設(shè)計、通信協(xié)議和客戶端

[12]?理論聯(lián)系實際:一套典型的IM通信協(xié)議設(shè)計詳解

[13]?58到家實時消息系統(tǒng)的協(xié)議設(shè)計等技術(shù)實踐分享

(本文同步發(fā)布于:http://www.52im.net/thread-4111-1-1.html)


IM通訊協(xié)議專題學習(七):手把手教你如何在NodeJS中從零使用Protobuf的評論 (共 條)

分享到微博請遵守國家法律
民勤县| 休宁县| 香港| 长宁区| 平邑县| 湖南省| 尚志市| 句容市| 昌平区| 新化县| 淳安县| 韶关市| 邵阳市| 莱州市| 富平县| 株洲市| 耒阳市| 靖远县| 杂多县| 常熟市| 格尔木市| 汉寿县| 雷波县| 同江市| 昭觉县| 灵璧县| 合川市| 金寨县| 新平| 玉环县| 通海县| 龙胜| 甘谷县| 河间市| 沧源| 沅陵县| 扎兰屯市| 牙克石市| 城口县| 鹤峰县| 天津市|