IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf
本文由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_64
exportPATH=${PATH}:${PROTOBUF_HOME}/bin
配置好后,使用 protoc --version 命令验证:

可以看到打印出了版本,证明我们安装成功了。
4.3brew 安装
直接使用以下命令就可以一键安装或卸载:
//一键安装 protobuf
brew installprotobuf
//一键卸载 protobuf
brew uninstall protobuf
PS:这种方式只适用于 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: 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)
}
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: 18
email: "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、参考资料
[2] Protobuf官方手册
[5] 强列建议将Protobuf作为你的即时通讯应用数据传输格式
[6] APP与后台通信数据格式的演进:从文本协议到二进制协议
[9] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
[12] 金蝶随手记团队的Protobuf应用实践(原理篇)
[13] 新手入门一篇就够:从零开发移动端IM
(本文已同步发布于:http://www.52im.net/thread-4135-1-1.html)
IM通讯协议专题学习(六):手把手教你如何在Android上从零使用Protobuf的更多相关文章
- 2020年,手把手教你如何在CentOS7上一步一步搭建LDAP服务器的最新教程
同步滚动:关 什么是LDAP 什么是LDAP? 要想知道一个概念,最简单的办法就是wikipedia,当然也可以百科. LDAP全称是轻型目录访问协议(Lightweight Directory Ac ...
- 教你如何在 Android 使用多线程下载文件
# 教你如何在 Android 使用多线程下载文件 前言 在 Android 日常开发中,我们会经常遇到下载文件需求,这里我们也可以用系统自带的 api DownloadManager 来解决这个问题 ...
- XMPP即时通讯协议使用(六)——开发Openfire聊天记录插件
转载地址:http://www.cnblogs.com/hoojo/archive/2013/03/29/openfire_plugin_chatlogs_plugin_.html 开发环境: Sys ...
- SpringCloud学习之手把手教你用IDEA搭建入门项目(三)
本篇博客是承接上一篇<手把手教你用IDEA搭建SpringCloud入门项目(二)>,不清楚的请到我的博客空间查看后再看本篇博客,上面两篇博客成功创建了一个简单的SpringCloud项目 ...
- SpringCloud学习之手把手教你用IDEA搭建入门项目(二)
本篇博客是承接上一篇<手把手教你用IDEA搭建SpringCloud入门项目(一)>,不清楚的请到我的博客空间查看后再看本篇博客 1)先创建一个Eureka服务注册中心模块,用来作为服务的 ...
- SpringCloud学习之手把手教你用IDEA搭建入门项目(一)
SpringCloud简单搭建 jdk:1.8开发工具:IDEA注:需要了解springcloud 1.创建最简单的Maven项目 1)开始创建一个新的项目 2)创建一个空模板的maven项目,用 ...
- 手把手教你 在Pytorch框架上部署和测试 关键点人脸检测项目DBFace,成功实现人脸检测效果
这期教向大家介绍仅仅 1.3M 的轻量级高精度的关键点人脸检测模型DBFace,并手把手教你如何在自己的电脑端进行部署和测试运行,运行时bug解决. 01. 前言 前段时间DBFace人脸检测库横空出 ...
- 手把手教你如何在Ubuntu系统中安装Pycharm
前几天带大家一起安装了Ubuntu14.04系统,没来得及上车的伙伴可以戳这篇文章:手把手教你在VMware虚拟机中安装Ubuntu14.04系统.今天小编带大家一起在Ubuntu14.04中安装Py ...
- 教你如何在Android 6.0上创建系统悬浮窗
郭霖大神的文章:http://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650235949&idx=1&sn=0f7eded ...
- TensorFlow-谷歌深度学习库 手把手教你如何使用谷歌深度学习云平台
自己的电脑跑cnn, rnn太慢? 还在为自己电脑没有好的gpu而苦恼? 程序一跑一俩天连睡觉也要开着电脑训练? 如果你有这些烦恼何不考虑考虑使用谷歌的云平台呢?注册之后即送300美元噢-下面我就来介 ...
随机推荐
- linux 基础(3)基本文件操作
目录的基本操作 在 linux 文件系统里,以斜杠 / 开头的路径是绝对路径,从根目录开始寻找:其他的路径则都是相对路径,从当前目录(working directory)开始寻找. 相对目录中常用的符 ...
- 从 Git 提交历史生成 Release Note
发布软件时写 Release Note 算是常规操作,但每次从头手打也有点累,可以考虑从 Git 的提交历史中自动生成. Git 提交信息一般是三段式结构,段落之间使用空行隔开: <subjec ...
- 电脑端 itunes 备份保存路径修改方法
默认在c盘,重做系统就会丢失. 1.先删除C:\Users\你的用户名\AppData\Roaming\Apple Computer里的 MobileSync文件夹(首次安装iTunes没有,要先运行 ...
- Chapter12 动态内存与智能指针
目录 12.1.0 介绍 12.1.1 shared_ptr类 make_shared函数 shared_ptr的拷贝和引用 shared_ptr自动销毁所管理的对象- -shared_ptr还会自动 ...
- 惊爆!72.1K star 的 Netdata:实时监控与可视化的超炫神器!
在当今复杂的 IT 环境中,实时监控与可视化对于保障系统的稳定运行和性能优化至关重要. 无论是服务器.应用程序,还是网络设备,及时获取性能数据能够帮助我们快速定位问题.优化资源配置. Netdata, ...
- x-easypdf 初始
一.概述 一个 java 语言简化处理 pdf 的框架 项目主页 https://x-easypdf.cn 项目概述 x-easypdf是一个java语言简化处理pdf的框架,包含fop模块与pdfb ...
- 2024-11-27:字符串的分数。用go语言,给定一个字符串 s,我们可以定义其“分数”为相邻字符的 ASCII 码差值绝对值的总和。 请计算并返回字符串 s 的分数。 输入:s = “hello“
2024-11-27:字符串的分数.用go语言,给定一个字符串 s,我们可以定义其"分数"为相邻字符的 ASCII 码差值绝对值的总和. 请计算并返回字符串 s 的分数. 输入:s ...
- Lambda【1】-- List相关Lambda表达式使用(上篇)
Lambda在jdk1.8里面已经很好用了,在这里不讲底层的实现,只有简单的用法,会继续补全. 首先一个list我们要使用lambda的话,需要使用它的stream()方法,获取流,才能使用后续的方法 ...
- LeetCode题集-5 - 最长回文子串(一)
题目:给你一个字符串 s,找到 s 中最长的回文子串. 这一题作为中等难度,常规解法对于大多数人应该都没有难度.但是其中也有超难的解决办法,下面我们就一起由易到难,循序渐进地来解这道题. 01.暴力破 ...
- Swagger 调试,我不想再复制粘贴token啦~
作为后端开发,进行Web Api 调试,除了使用 Postman, Apifox 等 Web Api 调试工具之外,我想使用Swagger进行调试应该是更方便,更常用的方式了吧. 那么在需要 toke ...