4

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf

 1 year ago
source link: http://www.52im.net/thread-4135-1-1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

微信扫一扫关注!

本文由sweetying分享,为了更好的阅读体验,即时通讯网有较多的内容修订和排版优化。

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

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

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_cover-opti.png

2、系列文章

本文是系列文章中的第 6 篇,总目录如下:

3、Protobuf 介绍

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_0.png



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

一个疑问:既然有了 XML 和 JSON,Google 为啥还要推出 Protobuf 呢?

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

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

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_1.png



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

4、Protobuf 环境配置

4.1概述

介绍两种安装 Protobuf 的方式:

  • 1)Github 下载 Protobuf 并安装;
  • 2)brew 安装。

4.2Github 下载 Protobuf 并安装

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

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

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_2.png



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

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_3.png



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

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

# protobuf 环境变量
export PROTOBUF_HOME=/Users/zhouying/Downloads/protoc-3.19.2-osx-x86_64
export PATH=${PATH}:${PROTOBUF_HOME}/bin

配置好后,使用 protoc --version 命令验证:
IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_4.png

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

4.3brew 安装

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

//一键安装 protobuf
brew install protobuf
//一键卸载 protobuf
brew uninstall protobuf

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

5、使用 IDE 创建 Protobuf 文件

这里我使用的 IDE 是 AndroidStudio。如果你还在用Eclipse+ADT开发Android程序,那我也不知道说啥好。

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

如下图

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_5.png



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

5.2集成 Protocol Buffers 插件

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_6.png



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

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_7.png



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

6、Protobuf 语法基础

关于 Protobuf 的语法有很多,限于篇幅,我们这里只讲一些常用的(如果有兴趣进一步学习的,可以详读本系列的前几篇,比如《Protobuf从入门到精通,一篇就够!》、《快速理解Protobuf的背景、原理、使用、优缺点》)。

6.1Protobuf 常用关键字介绍

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_8.png



PS:在 Proto3 中不支持 required (必须)字段。

6.2Protobuf 基本数据类型

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_9.png



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

//例如说我在 Java 里面进行如下定义:
int a = 1;
//因为 int 类型占 4 个字节,1个字节占 8 位,我们把 1 的字节占位给列出来:
00000000 00000000 00000000 00000001
//可以看到 1 的前面 3 个字节占位都是 0,在 Protobuf 里面是可以去掉的,于是就变成了:
00000001
//因此 1 在 Protobuf 里面就只占用了一个字节,节省了空间

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

基本数据类型默认值:

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_10.png

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

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_11.png



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/ 目录下生成如下文件:

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_12-opti.png



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

7.2使用 protoc 命令行转换

执行如下命令:

protoc --java_out=${"你要生成的 Java 文件目录"} ${"Protobuf 文件位置"}
//以我的为例:
protoc --java_out=./app/src/main/java ./app/src/main/proto/student.proto

生成的文件:

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_13.png



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

7.3生成的 Java 文件介绍

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

IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf_14.png



接下来简单进行说明。

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("[email protected]")
.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
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("[email protected]")
.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("[email protected]")
.addAllCourse(mutableListOf("Math", "English", "Computer"))
.build()
//2、Protobuf 转 Json
val json = JsonFormat.printer().print(student)
//3、打印 json
println(json)
}
//打印结果
{
"name": "erdai",
"age": 18,
"email": "[email protected]",
"course": ["Math", "English", "Computer"]
}

JSON 转 Protobuf:

fun main(){
//1、构建 Protobuf 对象
val student = StudentOuterClass.Student.newBuilder()
.setName("erdai")
.setAge(18)
.setEmail("[email protected]")
.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
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(
var name: String? = "",
var age: Int = 0,
var email: String? = "",
var course: MutableList<String>? = null
)

最后进行转换:

fun main(){
//1、构建 Protobuf 对象
val student = StudentOuterClass.Student.newBuilder()
.setName("erdai")
.setAge(18)
.setEmail("[email protected]")
.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 protected], 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK