关于即时通讯:IM通讯协议专题学习七手把手教你如何在NodeJS中从零使用Protobuf

41次阅读

共计 10140 个字符,预计需要花费 26 分钟才能阅读完成。

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…)

正文完
 0