Jack Jiang

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

          1、前言

          最近我負責的 LiveChat 客服聊天系統到了自研階段,任務類似于做一個騰訊云IM這樣的通信層SDK。在和后臺進行技術選型討論后,確定了數據傳輸層協議格式使用 Protobuf。

          本文基于我對Protobuf在Android端的實際使用心得,手把手教你如何在Android端IM產品中使用Protobuf,希望對你有幫助。

          學習交流:

          (本文已同步發布于:http://www.52im.net/thread-4135-1-1.html

          2、系列文章

          本文是系列文章中的第 篇,總目錄如下:

          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、參考資料

          [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



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 成武县| 武冈市| 禄劝| 唐河县| 罗源县| 桃江县| 山阳县| 洪泽县| 长垣县| 兰州市| 武义县| 东阳市| 韶山市| 桂林市| 新化县| 安丘市| 长春市| 宣恩县| 新泰市| 漳浦县| 塘沽区| 阿克苏市| 清远市| 宁波市| 定西市| 丽江市| 光泽县| 桦南县| 盐津县| 松江区| 红安县| 青铜峡市| 广东省| 永宁县| 沽源县| 宿松县| 宁国市| 安图县| 黑河市| 滨海县| 友谊县|