1、前言

Protobuf是Google开源的一种混合语言数据规范,已被各种互联网我的项目大量应用。

Protobuf最大的特点是数据格式领有极高的压缩比,这在挪动互联时代是极具价值的(因为挪动网络流量到目前为止依然低廉的),如果你的APP能比竞品更省流量,无疑这也将成为您产品的亮点之一。

当初,尤其IM、音讯推送这类利用中,Protobuf的利用更是十分宽泛,基于它的优良体现,微信和手机QQ这样的支流IM利用也早已在应用它。当初随着WebSocket协定的越来越成熟,浏览器反对的越来越好,Web端的即时通讯利用也逐步领有了真正的“实时”能力,相干的技术和利用也是层出不穷,而Protobuf也同样能够用在WebSocket的通信中。

而且目前比拟沉闷的WebSocket开源计划中,都是用NodeJS实现的,比方:socket.io和sockjs都是如此,因此本文介绍Protobuf在NodeJS上的应用,也恰是时候。

学习交换:

  • 挪动端IM开发入门文章:《新手入门一篇就够:从零开发挪动端IM》
  • 开源IM框架源码:https://github.com/JackJiang2...(备用地址点此)
    (本文同步公布于:http://www.52im.net/thread-41...)

    2、系列文章

    本文是系列文章中的第 7 篇,本系列总目录如下:
    《IM通信协定专题学习(一):Protobuf从入门到精通,一篇就够!》
    《IM通信协定专题学习(二):疾速了解Protobuf的背景、原理、应用、优缺点》
    《IM通信协定专题学习(三):由浅入深,从根上了解Protobuf的编解码原理》
    《IM通信协定专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码原理》
    《IM通信协定专题学习(五):Protobuf到底比JSON快几倍?全方位实测!》
    《IM通信协定专题学习(六):手把手教你如何在Android上从零应用Protobuf》(稍后公布..)
    《IM通信协定专题学习(七):手把手教你如何在NodeJS中从零应用Protobuf》(* 本文)
    《IM通信协定专题学习(八):金蝶顺手记团队的Protobuf利用实际(原理篇) 》(稍后公布..)
    《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 js、protocol-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'.build();6.4 编写 Writervar HelloWorld = require('./lm.helloworld.js')'lm';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 编写Readervar HelloWorld = require('./lm.helloworld.js')'lm';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 编写.protocover.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 书写serverserver.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 嵌套Messagemessage
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 过程也能够通过几个位移操作组成的表达式计算即可实现。速度十分快。

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-41...)