Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 494, comments - 13, trackbacks - 0, articles - 1

          1、前言

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

          Protobuf最大的特點是數(shù)據(jù)格式擁有極高的壓縮比,這在移動互聯(lián)時代是極具價值的(因為移動網(wǎng)絡(luò)流量到目前為止仍然昂貴的),如果你的APP能比競品更省流量,無疑這也將成為您產(chǎn)品的亮點之一。現(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.iosockjs都是如此,因而本文介紹Protobuf在NodeJS上的使用,也恰是時候。

          學(xué)習(xí)交流:

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

          2、系列文章

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

          1. IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門到精通,一篇就夠!
          2. IM通訊協(xié)議專題學(xué)習(xí)(二):快速理解Protobuf的背景、原理、使用、優(yōu)缺點
          3. IM通訊協(xié)議專題學(xué)習(xí)(三):由淺入深,從根上理解Protobuf的編解碼原理
          4. IM通訊協(xié)議專題學(xué)習(xí)(四):從Base64到Protobuf,詳解Protobuf的數(shù)據(jù)編碼原理
          5. IM通訊協(xié)議專題學(xué)習(xí)(五):Protobuf到底比JSON快幾倍?全方位實測!
          6. 《IM通訊協(xié)議專題學(xué)習(xí)(六):手把手教你如何在Android上從零使用Protobuf》(稍后發(fā)布..)
          7. IM通訊協(xié)議專題學(xué)習(xí)(七):手把手教你如何在NodeJS中從零使用Protobuf》(* 本文
          8. 《IM通訊協(xié)議專題學(xué)習(xí)(八):金蝶隨手記團隊的Protobuf應(yīng)用實踐(原理篇) 》(稍后發(fā)布..)
          9. 《IM通訊協(xié)議專題學(xué)習(xí)(九):金蝶隨手記團隊的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é)議當(dāng)然是大名鼎鼎的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ù)男蕰撸?/li>
          • 3)在后臺開發(fā)中,后臺與后臺的通信一般就是基于二進制協(xié)議的。甚至某些native app和服務(wù)器的通信也選擇了二進制協(xié)議(例如騰訊視頻)。但由于web前端的存在,后臺同學(xué)往往需要特地開發(fā)維護一套http接口專供我們使用,如果web也能使用二進制協(xié)議,可以節(jié)省許多后臺開發(fā)的成本。

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

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

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

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

           

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

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

          6.1 概述

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

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

          準(zhǔn)備用于演示的結(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

          }

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

          packageName.MessageName.proto

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

          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 編譯器生成目標(biāo)語言所需要的源代碼文件。將這些生成的代碼和應(yīng)用程序一起編譯。

          可是在某些情況下,人們無法預(yù)先知道 .proto 文件,他們需要動態(tài)處理一些未知的 .proto 文件。比如一個通用的消息轉(zhuǎn)發(fā)中間件,它不可能預(yù)知需要處理怎樣的消息。這需要動態(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 域。這使得人們可以定義更加復(fù)雜的數(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,從而讓定義復(fù)雜的數(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)換成指定類型的變量。

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

          反觀 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



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 十堰市| 共和县| 繁昌县| 体育| 自贡市| 什邡市| 出国| 太谷县| 陆川县| 游戏| 常山县| 松溪县| 公安县| 民权县| 九龙县| 长武县| 尼勒克县| 高邑县| 西贡区| 台江县| 珠海市| 田阳县| 铅山县| 鄂托克前旗| 武夷山市| 绍兴市| 镇雄县| 临桂县| 兴化市| 西和县| 衡山县| 汽车| 石首市| 岚皋县| 额尔古纳市| 甘泉县| 怀仁县| 博兴县| 得荣县| 哈巴河县| 镶黄旗|