im通讯协议专题学习(六):手把手教你如何在android上从零使用protobuf -凯发k8网页登录

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

最近我负责的 livechat 客服聊天系统到了自研阶段,任务类似于做一个腾讯云im这样的通信层sdk。在和后台进行技术选型讨论后,确定了数据传输层协议格式使用 protobuf。

本文基于我对protobuf在android端的实际使用心得,手把手教你如何在android端im产品中使用protobuf,希望对你有帮助。

学习交流:

- 移动端im开发入门文章:《》

- 开源im框架源码:()

(本文已同步发布于:)

  • 《》
  • 《》
  • 《》
  • 《》
  • 《》
  • 《》(* 本文)
  • 《》
  • 《》
  • 《》

protobuf的全称是protocol buffers,它是 google 推出的一种与平台无关、语言无关、可扩展的轻便高效的序列化数据存储格式,类似于我们常用的 xml 和 json。

一个疑问:既然有了 xml 和 json,google 为啥还要推出 protobuf 呢?

存在即是合理,protobuf 的优势用两个字总结就是:小、快。

相同的数据内容,用 protobuf 序列化后的大小是 json 的十分之一,是 xml 格式的二十分之一,而且性能是他们的 5~100 倍。

通常情况下,我们使用 xml 或者 json 进行数据通信是没什么问题的,但是在性能优化的场景下,如果有办法压缩数据量、提高传输效率,显然会给用户带来更快更流畅的体验。因此我在做 livechat 自研技术选型时,protobuf 成为了我们进行数据传输协议格式的第一选择。

4.1概述

介绍两种安装 protobuf 的方式:

  • 1)github 下载 protobuf 并安装;
  • 2)brew 安装。

4.2github 下载 protobuf 并安装

protobuf 版本尽量保持前后台一致,这里是后台和我约定的一个版本()。

1)根据自己的系统选择相应的 zip 包:

以我下载的为例,解压后结构如下:

可以看到 bin 目录下有个 protoc 的可执行文件。

我们给它配置一下环境变量就可以使用了(以我的为例):

# protobuf 环境变量

exportprotobuf_home=/users/zhouying/downloads/protoc-3.19.2-osx-x86_64

exportpath=${path}:${protobuf_home}/bin

配置好后,使用 protoc --version 命令验证:

可以看到打印出了版本,证明我们安装成功了。

4.3brew 安装

直接使用以下命令就可以一键安装或卸载:

//一键安装 protobuf

brew installprotobuf

 

//一键卸载 protobuf

brew uninstall protobuf

ps:这种方式只适用于 mac 系统,而且安装的 protobuf 为最新版本,因此如果想要使用指定的版本,建议使用上小节里的github下载安装这种方式。

这里我使用的 ide 是 androidstudio。如果你还在用eclipse adt开发android程序,那我也不知道说啥好。

5.1创建一个以 .proto 为后缀的文件

如下图:

可以看到这个文件前面的 icon 是个文本类型的,而且你在编写代码时,没有任何提示,关键字也没高亮显示,此时我们需要集成一个插件来支持 protobuf 的编写。

5.2集成 protocol buffers 插件

当我们集成这个插件后,可以看到文件 icon 和代码高亮的变化,而且还会有语法提示(如下图所示)。

ok,接下来我们就可以愉快的编写 protobuf 了,但是要怎么写呢?你是不是还不会 protobuf 的语法,接着往下看。

关于 protobuf 的语法有很多,限于篇幅,我们这里只讲一些常用的(如果有兴趣进一步学习的,可以详读本系列的前几篇,比如《》、《》)。

6.1protobuf 常用关键字介绍

ps:在 proto3 中不支持 required (必须)字段。

6.2protobuf 基本数据类型

可变长度编码和固定长度编码区别:

//例如说我在 java 里面进行如下定义:

inta = 1;

//因为 int 类型占 4 个字节,1个字节占 8 位,我们把 1 的字节占位给列出来:

00000000000000000000000000000001

//可以看到 1 的前面 3 个字节占位都是 0,在 protobuf 里面是可以去掉的,于是就变成了:

00000001

//因此 1 在 protobuf 里面就只占用了一个字节,节省了空间

上面这种就是可变长度编码。而固定长度编码就是即使前面的字节占位是 0,也不能去掉,我就是要占这么多字节。

基本数据类型默认值:

6.3消息类型定义

在上面创建的 protobuf 文件中,定义一个学生,学生有姓名,年龄,邮箱和课程。

我们来实现一下它:

//指定 protobuf 版本

syntax = "proto3";

 

//指定包名

package erdai;

 

//定义一个学生的消息类

message student{

  //姓名

  string name = 1;

  //年龄

  int32 age = 2;

  //邮箱

  string email = 3;

  //课程

  repeated string course = 4; //相当于 java 的 list

}

有几个地方需要注意:

  • 1)一个 protobuf 文件里面可以添加多个消息类,也可以进行嵌套;
  • 2)上面的 1,2,3,4 并不是给字段赋值,而是给每个字段定义一个唯一的编号(这些编号用于二进制格式中标识你的字段,并且在使用你的消息类型后不应更改);
  • 3)1-15 的字段编号只占一个字节进行编码,16-2047 的字段编号占两个字节,包括字段编号和字段类型,因此建议更多的使用 1-15 的字段编号;
  • 4)可以指定最小字段编号为 1,最大字段编号为 2^29 - 1 或 536870911(另外不能使用 19000-19999 的标识号,因为 protobuf 协议实现对这些进行了预留,同样也不能使用任何以前保留(reserved) 的字段编号)。

6.4protobuf 枚举类型

message weather{

  int32 query = 1;

 

  //季节

  enum season{

    //允许对枚举常量设置别名

    option allow_alias = true;

    //枚举里面的 = 操作是对常量进行赋值操作

    //春

    spring = 0;

    //夏

    summer = 1;

    //秋 如果不设置别名,不允许存在两个相同的值

    fall = 2;

    autumn = 2;

    //冬

    winter = 3;

  }

 

  //对 season 进行编号

  season season = 2;

}

有几个地方需要注意:

  • 1)定义枚举类型使用 enum 关键字;
  • 2)枚举类型第一个字段的值为必须 0,否则编译会报错;
  • 3)枚举常量值必须在 32 位整型值的范围内(因为 enum 值是使用可变编码方式的,对负数不够高效,因此不推荐在 enum 中使用负数);
  • 4)枚举里面的 = 操作是对常量进行赋值操作,而枚举外面的 = 则是对当前字段进行编号。

6.5protobuf 集合

repeated string list = 1; //类似 java 的 list

map = 2; //类似 java 的 map

有几个地方需要注意:

  • 1)protobuf 中定义集合,就是在你定义好的属性前面加 repeated 关键字;
  • 2)protobuf 中定义 map 和 java 类似,只不过 map 是小写的。

6.6reserved 保留字段

当我们使用 reserved 关键字指定一个字段编号或字段名为保留字段后,我们就不能去使用它,否则编译器会报错(如下图所示)。

ok,简单的介绍了 protobuf 语法后,接下来我们使用 androidstudio 将 protobuf 文件转换为对应的 java 文件来进行开发。

首先我们要明白一点:protobuf 是一种与平台,语言无关的数据存储格式,因此我们要在其它语言如:java,kotlin,dart 等语言中使用它,则必须将 protobuf 文件转换为对应平台的语言文件去使用。

这里我们以转 java 文件为例,介绍两种转换的方式:

  • 1)集成 protobuf 插件转换;
  • 2)使用 protoc 命令行转换。

在使用这两种方式转换前,我们需要先集成 protobuf-java 这个第三方库,因为我们转换的 java 文件中会使用这个库里面的功能。

在android studio的工作配置中集成 protobuf-java:

implementation 'com.google.protobuf:protobuf-java:3.19.2'

最新版本可查看此链接:

7.1集成 protobuf 插件转换

插件链接是:

第一步:在项目的根 build.gradle 文件中,添加如下依赖:

buildscript {

    repositories {

        gradlepluginportal()

    }

    dependencies {

        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.19'

    }

}

第二步:在 app 的 build.gradle 文件中应用插件:

//1、如果使用的是 apply 方式,android 插件必须在 protobuf 插件之前,如下:

apply plugin: 'com.android.application'

apply plugin: 'com.google.protobuf'

 

//2、如果使用的是 gradle dsl 方式,则与顺序无关

plugins {

    id'com.google.protobuf'

    id'com.android.application'

}

第三步:在 app 的 build.gradle 文件中配置 protobuf 编译任务:

android {

    //...

}

 

protobuf {

    //配置 protoc 编译器

    protoc {

        artifact = 'com.google.protobuf:protoc:3.19.2'

    }

    //配置生成目录,编译后会在 build 的目录下生成对应的java文件

    generateprototasks {

        all().each { task ->

            task.builtins {

                remove java

            }

            task.builtins {

                java {}

            }

        }

    }

}

第四步:指定 protobuf 文件所在路径:

android {

    //...

 

    sourcesets {

        main {

            //实际测试指不指定无所谓,不影响 java 文件生成

            proto {

                srcdir 'src/main/proto'

            }

        }

    }

}

经过上面 4 步,我们的插件就算配置完成了,接下来我们测试一下。

以我们上面那个 student.proto 为例,里面的内容如下:

//指定 protobuf 版本

syntax = "proto3";

 

//指定包名

package erdai;

 

//定义一个学生的消息类

message student{

  //姓名

  string name = 1;

  //年龄

  int32 age = 2;

  //邮箱

  string email = 3;

  //课程

  repeated string course = 4;

 

}

 

//定义一个天气的消息类

message weather{

  int32 query = 1;

 

  //季节

  enum season{

    option allow_alias = true;

    //春

    spring = 0;

    //夏

    summer = 1;

    //秋

    fall = 2;

    autumn = 2;

    //冬

    winter = 3;

  }

 

  season season = 2;

}

编译后,会在 app/build/generated/source/proto/ 目录下生成如下文件:

这种方式会自动给我们生成对应的 java 类,就是集成的时候麻烦一点。

7.2使用 protoc 命令行转换

执行如下命令:

protoc --java_out=${"你要生成的 java 文件目录"} ${"protobuf 文件位置"}

 

//以我的为例:

protoc --java_out=./app/src/main/java ./app/src/main/proto/student.proto

生成的文件:

这种方式的优点是使用非常简单,但是每一次创建新的 protobuf 文件都需要执行一下命令。

7.3生成的 java 文件介绍

关于这个类的内容我简单介绍下,大家也可以生成后自行看源码(如下图)。

接下来简单进行说明。

1)成的类 studentouterclass 有 4 个子类:

student (实现了 studentorbuilder 接口)

weather (实现了 weatherorbuilder 接口)

studentorbuilder (接口)

weatherorbuilder (接口)

2)student 是 studentouterclass 的静态内部类,构造方法私有化,需通过 builder 模式创建对象。另外 student 提供了属性的 get 方法,无 set 方法,builder 作为 student 的静态内部类提供了属性的 get,set,clear方法,clear 就是清空某个属性回到初始值。

3)weather 同 2。

接下来我们对刚才生成的 java 类进行使用。

8.1基本调用

fun main(){

    //构建 protobuf 对象

    val student = studentouterclass.student.newbuilder()

        .setname("erdai")

        .setage(18)

        .setemail("erdai666@qq.com")

        .addallcourse(mutablelistof("math", "english", "computer"))

        .build()

    println(student)

    println()

    println(studentouterclass.weather.season.spring.number)

    println(studentouterclass.weather.season.summer.number)

    println(studentouterclass.weather.season.autumn.number)

    println(studentouterclass.weather.season.winter.number)

}

 

//打印结果

name: "erdai"

age: 18

email: "erdai666@qq.com"

course: "math"

course: "english"

course: "computer"

 

0

1

2

3

8.2序列化和反序列化

fun main(){

    //1、构建 protobuf 对象

    val student = studentouterclass.student.newbuilder()

        .setname("erdai")

        .setage(18)

        .setemail("erdai666@qq.com")

        .addallcourse(mutablelistof("math", "english", "computer"))

        .build()

    //2、序列化并返回一个包含其原始字节的字节数组

    val bytearray: bytearray = student.tobytearray()

    //3、反序列化从字节数组中解析消息

    val parsestudent: studentouterclass.student = studentouterclass.student.parsefrom(bytearray)

}

首先我们需要添加一个第三方依赖:

implementation 'com.google.protobuf:protobuf-java-util:3.19.2'

protobuf 转 json:

fun main(){

    //1、构建 protobuf 对象

    val student = studentouterclass.student.newbuilder()

        .setname("erdai")

        .setage(18)

        .setemail("erdai666@qq.com")

        .addallcourse(mutablelistof("math", "english", "computer"))

        .build()

    //2、protobuf 转 json

    val json = jsonformat.printer().print(student)

    //3、打印 json

    println(json)

}

 

//打印结果

{

  "name": "erdai",

  "age": 18,

  "email": "erdai666@qq.com",

  "course": ["math", "english", "computer"]

}

json 转 protobuf:

fun main(){

    //1、构建 protobuf 对象

    val student = studentouterclass.student.newbuilder()

        .setname("erdai")

        .setage(18)

        .setemail("erdai666@qq.com")

        .addallcourse(mutablelistof("math", "english", "computer"))

        .build()

    //2、protobuf 对象转 json

    val json = jsonformat.printer().print(student)

    //3、json 转 protobuf 对象

    val builder = studentouterclass.student.newbuilder()

    jsonformat.parser().ignoringunknownfields().merge(json,builder)

    //4、打印 protobuf 对象

    println(builder.build())

}

 

//打印结果

name: "erdai"

age: 18

email: "erdai666@qq.com"

course: "math"

course: "english"

course: "computer"

这个实则是在前述的基础上,当我们将 protobuf 转换为 json 后,在把 json 转换为对应的 java 对象。

这里我们先引入 gson库:

implementation 'com.google.code.gson:gson:2.8.9'

ps:类似于fastjson,是google出品的著名json解析库。

先定义一个接收当前 json 的数据模型:

data class mystudent(

    varname: string? = "",

    varage: int = 0,

    varemail: string? = "",

    varcourse: mutablelist? = null

)

最后进行转换:

fun main(){

    //1、构建 protobuf 对象

    val student = studentouterclass.student.newbuilder()

        .setname("erdai")

        .setage(18)

        .setemail("erdai666@qq.com")

        .addallcourse(mutablelistof("math", "english", "computer"))

        .build()

    //2、将 protobuf 转换为 json

    val json = jsonformat.printer().print(student)

 

    //3、将 json 转换为 java bean 对象

    val mystudent = gson().fromjson(json,mystudent::class.java)

    println(mystudent)

}

 

//打印结果

mystudent(name=erdai, age=18, email=erdai666@qq.com, course=[math, english, computer])

这里我小结一下关于 protobuf 的开发流程:

  • 1)一般都是后台定义好 protobuf;
  • 2)然后给到我们客户端;
  • 3)客户端在根据 protobuf 生成对应平台的代码进行使用即可。

因此,上面我讲的这些内容如果你都掌握了,基本上就能满足android端使用protobuff进行日常开发了。

好了,本篇文章到这里就结束了,如果有啥问题,欢迎评论区一起交流。

[1] 

[2] 

[3] 

[4] 

[5] 

[6] 

[7] 

[8] 

[9] 

[10] 

[11] 

[12] 

[13] 

(本文已同步发布于:)



作者: (点击作者姓名进入github)
出处:
交流:欢迎加入即时通讯开发交流群
讨论:
jack jiang同时是和的作者,可前往下载交流。
本博文 欢迎转载,转载请注明出处(也可前往 找到我)。


只有注册用户后才能发表评论。


网站导航:
              
 
jack jiang的 mail: jb2011@163.com, 联系qq: 413980957, 微信: hellojackjiang
网站地图