本文由 sweetying 分享,为了更好的浏览体验,有较多的内容订正和排版优化。
1、前言
最近我负责的 LiveChat 客服聊天零碎到了自研阶段,工作相似于做一个腾讯云 IM 这样的通信层 SDK。在和后盾进行技术选型探讨后,确定了数据传输层协定格局应用 Protobuf。
本文基于我对 Protobuf 在 Android 端的理论应用心得,手把手教你如何在 Android 端 IM 产品中应用 Protobuf,心愿对你有帮忙。
学习交换:
- 挪动端 IM 开发入门文章:《新手入门一篇就够:从零开发挪动端 IM》
- 开源 IM 框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步公布于:http://www.52im.net/thread-4135-1-1.html)
2、系列文章
本文是系列文章中的第 6 篇,总目录如下:
《IM 通信协定专题学习 (一):Protobuf 从入门到精通,一篇就够!》
《IM 通信协定专题学习 (二):疾速了解 Protobuf 的背景、原理、应用、优缺点》
《IM 通信协定专题学习 (三):由浅入深,从根上了解 Protobuf 的编解码原理》
《IM 通信协定专题学习 (四):从 Base64 到 Protobuf,详解 Protobuf 的数据编码原理》
《IM 通信协定专题学习 (五):Protobuf 到底比 JSON 快几倍?全方位实测!》
《IM 通信协定专题学习 (六):手把手教你如何在 Android 上从零应用 Protobuf》(* 本文)
《IM 通信协定专题学习 (七):手把手教你如何在 NodeJS 中从零应用 Protobuf》
《IM 通信协定专题学习 (八):金蝶顺手记团队的 Protobuf 利用实际 (原理篇)》
《IM 通信协定专题学习 (九):手把手教你如何在 iOS 上从零应用 Protobuf》
3、Protobuf 介绍
Protobuf 的全称是 Protocol Buffers,它是 Google 推出的一种与平台无关、语言无关、可扩大的轻便高效的序列化数据存储格局,相似于咱们罕用的 xml 和 json。
一个疑难:既然有了 XML 和 JSON,Google 为啥还要推出 Protobuf 呢?存在即是正当,Protobuf 的劣势用两个字总结就是:小、快。雷同的数据内容,用 Protobuf 序列化后的大小是 JSON 的十分之一,是 XML 格局的二十分之一,而且性能是他们的 5~100 倍。
通常状况下,咱们应用 XML 或者 JSON 进行数据通信是没什么问题的,然而在性能优化的场景下,如果有方法压缩数据量、进步传输效率,显然会给用户带来更快更晦涩的体验。因而我在做 LiveChat 自研技术选型时,Protobuf 成为了咱们进行数据传输协定格局的第一抉择。
4、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_64exportPATH=${PATH}:${PROTOBUF_HOME}/bin 配置好后,应用 protoc –version 命令验证:
能够看到打印出了版本,证实咱们装置胜利了。
4.3brew 装置
间接应用以下命令就能够一键装置或卸载:// 一键装置 protobufbrew installprotobuf // 一键卸载 protobufbrew uninstall protobufPS:这种形式只实用于 Mac 零碎,而且装置的 protobuf 为最新版本,因而如果想要应用指定的版本,倡议应用上大节里的 Github 下载安装这种形式。
5、应用 IDE 创立 Protobuf 文件
这里我应用的 IDE 是 AndroidStudio。如果你还在用 Eclipse+ADT 开发 Android 程序,那我也不晓得说啥好。
5.1 创立一个以 .proto 为后缀的文件如下
图:
能够看到这个文件后面的 icon 是个文本类型的,而且你在编写代码时,没有任何提醒,关键字也没高亮显示,此时咱们须要集成一个插件来反对 Protobuf 的编写。
5.2 集成 Protocol Buffers 插件
当咱们集成这个插件后,能够看到文件 icon 和代码高亮的变动,而且还会有语法提醒(如下图所示)。
ok,接下来咱们就能够欢快的编写 Protobuf 了,然而要怎么写呢?你是不是还不会 Protobuf 的语法,接着往下看。
6、Protobuf 语法根底
对于 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<String>}
有几个中央须要留神:
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<String>map<string,string> = 2; // 相似 Java 的 Map<String,String>
有几个中央须要留神:
1)Protobuf 中定义汇合,就是在你定义好的属性后面加 repeated 关键字;
2)Protobuf 中定义 map 和 Java 相似,只不过 map 是小写的。
6.6reserved 保留字段
当咱们应用 reserved 关键字指定一个字段编号或字段名为保留字段后,咱们就不能去应用它,否则编译器会报错(如下图所示)。
ok,简略的介绍了 Protobuf 语法后,接下来咱们应用 AndroidStudio 将 Protobuf 文件转换为对应的 Java 文件来进行开发。
7、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’ 最新版本可查看此链接:https://github.com/protocolbuffers/protobuf/tree/main/java
7.1 集成 Protobuf 插件转换
插件链接是:https://github.com/google/protobuf-gradle-plugin
第一步:在我的项目的根 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。
8、Protobuf 的应用接下来咱们对方才生成的 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: 18email: “erdai666@qq.com”course: “Math”course: “English”course: “Computer” 0123
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)}
9、Protobuf 和 JSON 互相转换
首先咱们须要增加一个第三方依赖: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: 18email: “erdai666@qq.com”course: “Math”course: “English”course: “Computer”
10、Protobuf 和 Java 对象互相转换
这个实则是在前述的根底上,当咱们将 Protobuf 转换为 JSON 后,在把 JSON 转换为对应的 Java 对象。这里咱们先引入 Gson 库:implementation ‘com.google.code.gson:gson:2.8.9’PS:相似于 fastjson,Gson 是 Google 出品的驰名 JSON 解析库。先定义一个接管以后 Json 的数据模型:data class MyStudent(varname: String? = “”, varage: Int = 0, varemail: String? = “”, varcourse: MutableList<String>? = 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])
11、本文小结
这里我小结一下对于 Protobuf 的开发流程:
1)个别都是后盾定义好 Protobuf;
2)而后给到咱们客户端;
3)客户端在依据 Protobuf 生成对应平台的代码进行应用即可。
因而,下面我讲的这些内容如果你都把握了,基本上就能满足 Android 端应用 Protobuff 进行日常开发了。好了,本篇文章到这里就完结了,如果有啥问题,欢送评论区一起交换。
12、参考资料
[1] Protobuf 官网开发者指南(中文译版)
[2] Protobuf 官网手册
[3] Protobuf 从入门到精通,一篇就够!
[4] 如何抉择即时通讯利用的数据传输格局
[5] 强列倡议将 Protobuf 作为你的即时通讯利用数据传输格局
[6] APP 与后盾通信数据格式的演进:从文本协定到二进制协定
[7] 面试必考,史上最艰深大小端字节序详解
[8] 挪动端 IM 开发须要面对的技术问题(含通信协议抉择)
[9] 简述挪动端 IM 开发的那些坑:架构设计、通信协议和客户端
[10] 实践联系实际:一套典型的 IM 通信协议设计详解
[11] 58 到家实时音讯零碎的协定设计等技术实际分享
[12] 金蝶顺手记团队的 Protobuf 利用实际 (原理篇)
[13] 新手入门一篇就够:从零开发挪动端 IM(本文已同步公布于:http://www.52im.net/thread-4135-1-1.html)