Protobuf學(xué)習(xí)


Protobuf是什么

Protobuf是一種平臺無關(guān)、語言無關(guān)、可擴展且輕便高效的序列化數(shù)據(jù)結(jié)構(gòu)的協(xié)議,可以用于網(wǎng)絡(luò)通信數(shù)據(jù)存儲。

為什么要使用Protobuf

protobuf特點.png

如何使用Protobuf

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

-I 編譯源文件的目錄

--java_out 編譯目錄文件

通過這個命令會自動編譯出java代碼,目前protobuf支持以下語言

Language Source
C++ src
Java java
Python python
Objective-C objectivec
C# csharp
JavaNano javanano
JavaScript js
Ruby ruby
Go golang/protobuf
PHP php
Dart dart-lang/protobuf

由于命令行的方式編譯代碼非常繁瑣,且效率極低。谷歌提供了開源的Protobuf Gradle插件

簡單說一下配置方式

在project.gradle中配置

buildscript {
  repositories {
    mavenLocal()
  }
  dependencies {
    classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6-SNAPSHOT'
  }
}

在modle.gradle中配置

apply plugin: 'com.google.protobuf'

dependencies {
  // You need to depend on the lite runtime library, not protobuf-java
  compile 'com.google.protobuf:protobuf-lite:3.0.0'
}

protobuf {
  protoc {
    // You still need protoc like in the non-Android case
    artifact = 'com.google.protobuf:protoc:3.0.0'
  }
  plugins {
    javalite {
      // The codegen for lite comes as a separate artifact
      artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
    }
  }
  generateProtoTasks {
    all().each { task ->
      task.builtins {
        // In most cases you don't need the full Java output
        // if you use the lite output.
        remove java
      }
      task.plugins {
        javalite { }
      }
    }
  }
}

目前有Protobuf2和Protobuf3,本文以Protobuf2為例,簡單介紹一下Protobuf2的語法,更多詳細內(nèi)容請參考官方文檔(需要翻墻)

先在Java的同級目錄下新建一個名為proto的文件夾專門用于存放proto文件,編寫proto文件后編譯模塊會根據(jù)proto文件內(nèi)容生成java文件。

image

來看一下名為Test.proto的文件內(nèi)容

//指定protobuf語法版本
syntax = "proto2";

//包名
option java_package = "com.lhc.protobuf";
//源文件類名
option java_outer_classname = "AddressBookProtos";

// class Person
message Person {
  //required 必須設(shè)置(不能為null)
  required string name = 1;
  //int32 對應(yīng)java中的int
  required int32 id = 2;
  //optional 可以為空
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
   //repeated 重復(fù)的 (集合)
  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

Protobuf應(yīng)用------網(wǎng)絡(luò)傳輸

http傳輸

通常在應(yīng)用層我們使用的都是Http協(xié)議,Http的本質(zhì)是一次socket請求的連接與斷開。傳輸數(shù)據(jù)時將protobuf對象轉(zhuǎn)換為byte[]傳輸即可

自定義TCP通信協(xié)議

當(dāng)我們自定義TCP通信協(xié)議的時候,將面臨粘包與分包的問題

分包:

  • 要發(fā)送的數(shù)據(jù)大于TCP緩沖剩余空間
  • 待發(fā)送數(shù)據(jù)大于MSS(最大報文長度)
image

粘包:

  • 要發(fā)送的數(shù)據(jù)小于TCP緩沖區(qū),將多次寫入緩沖區(qū)的數(shù)據(jù)一起發(fā)送
  • 接收端的應(yīng)用層沒有及時讀取緩沖區(qū)的數(shù)據(jù)

[站外圖片上傳中...(image-f9d2d3-1528012964593)]

自定義通信協(xié)議的兩種方式

  • 定義數(shù)據(jù)包包頭
image
  • 在數(shù)據(jù)包之間設(shè)置邊界
image

大家可以參考 JT808協(xié)議 ------交通部808協(xié)議(車聯(lián)網(wǎng)),也是采用類似的方式定義通信協(xié)議


手寫簡易Gradle Protobuf編譯插件

準(zhǔn)備proto編譯器工件,proto文件目錄,通過參數(shù)拼接出命令行編譯proto文件,將執(zhí)行結(jié)果注冊到編譯打包列表

定義兩個DSL命名空間

class ProtobufExt {
     /**
     * proto文件目錄
     */
    def srcDirs

    ProtobufExt() {
        srcDirs = []
    }

    def srcDir(String srcDir) {
        if (!srcDirs.contians(srcDir))
            srcDirs << srcDir
    }

    def srcDir(String... srcDirs) {
        srcDirs.each { srcDir(it) }
    }
}
class ProtoExt {
    def path
    def artifact
}

定義一個插件實現(xiàn)Plugin接口

class ProtobufPlugin implements Plugin<Project> {

    static final String PROTOBUF_EXTENSION_NAME = "protobuf"
    static final String PROTO_SUB_EXTENSION_NAME = "protoc"
    Project project

    @Override
    void apply(Project project) {
        this.project = project
        project.apply plugin: 'com.google.osdetector'
        project.extensions.create(PROTOBUF_EXTENSION_NAME, ProtobufExt)//創(chuàng)建命名空間
        project.protobuf.extensions.create(PROTO_SUB_EXTENSION_NAME, ProtoExt)
        //在gradle分析之后執(zhí)行
        project.afterEvaluate {
            if (!project.protobuf.protoc.path) {
                if (!project.protobuf.protoc.artifact) {
                    throw new GradleException("請配置protoc編譯器")
                }
                //創(chuàng)建依賴配置
                Configuration config = project.configurations.create("protobufConfig")
                def (group, name, version) = project.protobuf.protoc.artifact.split(":")
                def notation = [group: group, name: name, version: version, classifier: project.osdetector.classifier, ext: 'exe']
                //本地存在則返回工件,否則先下載
                Dependency dependency = project.dependencies.add(config.name, notation)
                //獲得對應(yīng)dependency的所有文件
                File file = config.fileCollection(dependency).singleFile
                println file
                if (!file.canExecute() && !file.setExecutable(true)) {
                    throw new GradleException("protoc編譯器無法執(zhí)行")
                }
                project.protobuf.protoc.path = file.path
            }

            Task task = project.tasks.create("compileProtobuf", CompileProtobufTask)
            task.inputs.files(project.protobuf.srcDirs)
            task.outputs.dir("${project.buildDir}/generated/source/proto")

            //將編譯生成的java文件假如到工程源代碼文件列表中
            linkProtoToJavaSource()
        }
    }

    /**
     * 判斷是否為安卓工程
     * @return
     */
    boolean isAndroidProject() {
        return project.plugins.hasPlugin(AppPlugin) || project.plugins.hasPlugin(LibraryPlugin)
    }

    def getAndroidVariants() {
        return project.plugins.hasPlugin(AppPlugin) ?
                project.android.applicationVariants + project.android.testVariants : project.android.libraryVariants + project.android.testVariants
    }

    def linkProtoToJavaSource() {
        if (isAndroidProject()) {
            androidVariants.each {
                BaseVariant variant ->
                    //將任務(wù)加入構(gòu)建過程,并將第二個參數(shù)的文件注冊到編譯列表當(dāng)中
                    variant.registerJavaGeneratingTask(project.tasks.compileProtobuf, project.tasks.compileProtobuf.outputs.files.files)
            }
        } else {
            project.sourceSets.each {
                SourceSet sourceSet ->
                    def compileName = sourceSet.getCompileTaskName('java')
                    JavaCompile javaCompile = project.tasks.getByName(compileName)
                    javaCompile.dependsOn project.tasks.compileProtobuf
                    sourceSet.java.srcDirs(project.tasks.compileProtobuf.outputs.files.files)
            }
        }
    }
}

實現(xiàn)一個DefaultTask子類,主要是通過輸入?yún)?shù)拼接出如下的編譯所需的命令行

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

class CompileProtobufTask extends DefaultTask {

    CompileProtobufTask() {
        group = 'Protobuf'
        outputs.upToDateWhen { false } //關(guān)閉增量構(gòu)建,否則輸入輸出不變時執(zhí)行增量構(gòu)建
    }
    
    @TaskAction
    def run() {
        def outDir = outputs.files.singleFile
        outDir.deleteDir()
        outDir.mkdirs()

        def cmd = [project.protobuf.protoc.path]

        cmd << "--java_out=$outDir"
        def source = []
        def inDirs = inputs.files.files
        inDirs.each {
            cmd << "-I=${it.path}"
        }

        getProtoFiles(inDirs, source)

        cmd.addAll(source)
        println "執(zhí)行:$cmd"

        Process process = cmd.execute()

        def stdout = new StringBuffer()
        def stdErr = new StringBuffer()

        process.waitForProcessOutput(stdout, stdErr)//輸出錯誤日志
        if (process.exitValue() == 0) {
            println "編譯protobuf文件成功"
        } else {
            throw new GradleException("編譯protobuf文件失敗" + " $stdout" + " $stdErr")
        }

    }

    /**
     * 將目錄下所有.proto文件添加到集合
     * @param dirs
     * @param source
     */
    def getProtoFiles(dirs, source) {
        dirs.each {
            File file ->
                if (file.isDirectory()) {
                    getProtoFiles(file.listFiles(), source)
                } else if (file.name.endsWith(".proto")) {
                    source << file
                }
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 簡介 protoBuf是google 的一種數(shù)據(jù)交換的格式,它獨立于語言,獨立于平臺。google 提供了多種語言...
    ssochi閱讀 3,056評論 0 2
  • 文章內(nèi)容源自Google官方文檔翻譯,詳見原文Language Guide。部分內(nèi)容可能重復(fù),望多見諒。 假設(shè)你想...
    Jancd閱讀 2,176評論 0 0
  • 1、下載編譯 git clone ...... sudo apt-get install autoconf aut...
    蘇恨閱讀 1,128評論 0 0
  • ProtoBuf 最近看書,看到了Protobuf概念,今天學(xué)習(xí)了解下 什么是Protobuf 官方給出的解釋:P...
    踐行者閱讀 929評論 0 2
  • protobuf 學(xué)習(xí) 前言 最近由于個人的興趣轉(zhuǎn)到了消息箱業(yè)務(wù)線,學(xué)習(xí)IM相關(guān)知識。說到IM,首先會講到使用TC...
    MikeZhangpy閱讀 5,622評論 0 6

友情鏈接更多精彩內(nèi)容