<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Jack Jiang

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

    1、前言

    Protobuf是Google開源的一種混合語言數據標準,已被各種互聯網項目大量使用。

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

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

    學習交流:

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

    2、系列文章

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

    1. IM通訊協議專題學習(一):Protobuf從入門到精通,一篇就夠!
    2. IM通訊協議專題學習(二):快速理解Protobuf的背景、原理、使用、優缺點
    3. IM通訊協議專題學習(三):由淺入深,從根上理解Protobuf的編解碼原理
    4. IM通訊協議專題學習(四):從Base64到Protobuf,詳解Protobuf的數據編碼原理
    5. IM通訊協議專題學習(五):Protobuf到底比JSON快幾倍?全方位實測!
    6. 《IM通訊協議專題學習(六):手把手教你如何在Android上從零使用Protobuf》(稍后發布..)
    7. IM通訊協議專題學習(七):手把手教你如何在NodeJS中從零使用Protobuf》(* 本文
    8. 《IM通訊協議專題學習(八):金蝶隨手記團隊的Protobuf應用實踐(原理篇) 》(稍后發布..)
    9. 《IM通訊協議專題學習(九):金蝶隨手記團隊的Protobuf應用實踐(實戰篇) 》(稍后發布..)

    3、Protobuf是個什么鬼?

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

    Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可以用于結構化數據序列化,很適合做數據存儲或 RPC 數據交換格式。它可用于通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。目前提供了 C++、Java、Python 三種語言的 API。

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

    4、NodeJS開發者為何要跟Protobuf打交道

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

    這就要說到protobuf的歷史了。

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

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

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

    • 1)二進制協議對于電腦來說更容易解析,在解析速度上是http這樣的文本協議不可比擬的;
    • 2)有tcp和udp兩種選擇,在一些場景下,udp傳輸的效率會更高;
    • 3)在后臺開發中,后臺與后臺的通信一般就是基于二進制協議的。甚至某些native app和服務器的通信也選擇了二進制協議(例如騰訊視頻)。但由于web前端的存在,后臺同學往往需要特地開發維護一套http接口專供我們使用,如果web也能使用二進制協議,可以節省許多后臺開發的成本。

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

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

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

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

     

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

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

    6.1 概述

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

    Writer 負責將一些結構化的數據寫入一個磁盤文件,Reader 則負責從該磁盤文件中讀取結構化數據并打印到屏幕上。

    準備用于演示的結構化數據是 HelloWorld,它包含兩個基本數據:

    • 1)ID:為一個整數類型的數據;
    • 2)Str:這是一個字符串。

    6.2 書寫.proto文件

    首先我們需要編寫一個 proto 文件,定義我們程序中需要處理的結構化數據,在 protobuf 的術語中,結構化數據被稱為 Message。proto 文件非常類似 java 或者 C 語言的數據定義。代碼清單 1 顯示了例子應用中的 proto 文件內容。

    清單 1. proto 文件:

    package lm;

    message helloworld

    {

       required int32     id = 1;  // ID

       required string    str = 2;  // str

       optional int32     opt = 3;  //optional field

    }

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

    packageName.MessageName.proto

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

    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規范的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.

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

    命令如下:

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

    得到編譯后的符合commonjs規范的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 函數用來修改和讀取結構化數據中的數據成員

    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 運行結果

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

    這個例子本身并無意義,但只要您稍加修改就可以將它變成更加有用的程序。比如將磁盤替換為網絡 socket,那么就可以實現基于網絡的數據交換任務。而存儲和交換正是 Protobuf 最有效的應用領域。

    7、使用 Protobuf 和NodeJS實現基于網絡數據交換的例子

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

    在這個栗子中,我們來實現基于網絡的數據交換任務。

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

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

    我們這里決定利用protobuf文件可以動態編譯的特性,在代碼中直接讀取proto文件,動態生成我們需要的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 運行結果

     

    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 域。這使得人們可以定義更加復雜的數據結構。

    8.2 Import Message

    在一個 .proto 文件中,還可以用 Import 關鍵字引入在其他 .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包內。

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

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

    9、總結一下Protobuf

    9.1 優點

    簡單說來 Protobuf 的主要優點就是:簡潔,快。

    為什么這么說呢?

    1)簡潔:

    因為Protocol Buffer 信息的表示非常緊湊,這意味著消息的體積減少,自然需要更少的資源。比如網絡上傳輸的字節數更少,需要的 IO 更少等,從而提高性能。

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

    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 個字節,這些奇怪的數字需要稍微解釋一下,其含義用 ASCII 表示如下:

    <helloworld>

       <id>101</id>

       <name>hello</name>

    </helloworld>

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

    2)快:

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

    • 1)XML 需要從文件中讀取出字符串,再轉換為 XML 文檔對象結構模型;
    • 2)之后,再從 XML 文檔對象結構模型中讀取指定節點的字符串;
    • 3)最后再將這個字符串轉換成指定類型的變量。

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

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

    9.2 缺點

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

    10、參考資料

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

    [2] Protobuf官方手冊

    [3] Why do we use Base64?

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

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

    [5] 如何選擇即時通訊應用的數據傳輸格式

    [7] 強列建議將Protobuf作為你的即時通訊應用數據傳輸格式

    [8] APP與后臺通信數據格式的演進:從文本協議到二進制協議

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

    [10] 移動端IM開發需要面對的技術問題(含通信協議選擇)

    [11] 簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端

    [12] 理論聯系實際:一套典型的IM通信協議設計詳解

    [13] 58到家實時消息系統的協議設計等技術實踐分享

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



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


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


    網站導航:
     
    Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
    主站蜘蛛池模板: 男女超爽刺激视频免费播放 | 亚洲精品一卡2卡3卡三卡四卡| 成人免费区一区二区三区 | 亚洲天天做日日做天天欢毛片| 久草免费福利在线| 亚洲综合另类小说色区色噜噜| 成年网在线观看免费观看网址| 久久久国产精品福利免费| 亚洲国产综合无码一区| 久久成人免费播放网站| 亚洲精品无码久久毛片波多野吉衣 | 亚洲AV无码久久精品成人| 久操视频在线免费观看| 久久亚洲精品中文字幕| 久久99九九国产免费看小说| 亚洲中文字幕久久精品无码VA| 国产精品四虎在线观看免费| 一级免费黄色毛片| 亚洲AV成人片色在线观看高潮| 亚洲精品免费在线视频| 亚洲国产午夜精品理论片在线播放 | 免费在线看黄网站| 亚洲伊人久久精品| 日本免费一二区在线电影| www在线观看播放免费视频日本| 亚洲av无码乱码国产精品fc2| 无码国产精品一区二区免费式影视 | 一本大道一卡二大卡三卡免费| 亚洲va久久久噜噜噜久久| 黄瓜视频影院在线观看免费| 国产精品亚洲专区一区| 亚洲AV无码久久精品蜜桃| 毛片a级毛片免费播放100| 亚洲视频网站在线观看| 四虎影院在线免费播放| 成人精品视频99在线观看免费| 亚洲综合区小说区激情区| 2019中文字幕免费电影在线播放 | 亚洲αv久久久噜噜噜噜噜| 国色精品卡一卡2卡3卡4卡免费| 黄色免费在线网址|