Protobuf学习


Protobuf是什么

Protobuf是一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议,可以用于网络通信数据存储

为什么要使用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的语法,更多详细内容请参考官方文档(需要翻墙)

先在Java的同级目录下新建一个名为proto的文件夹专门用于存放proto文件,编写proto文件后编译模块会根据proto文件内容生成java文件。

image

来看一下名为Test.proto的文件内容

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

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

// class Person
message Person {
  //required 必须设置(不能为null)
  required string name = 1;
  //int32 对应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 重复的 (集合)
  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

Protobuf应用------网络传输

http传输

通常在应用层我们使用的都是Http协议,Http的本质是一次socket请求的连接与断开。传输数据时将protobuf对象转换为byte[]传输即可

自定义TCP通信协议

当我们自定义TCP通信协议的时候,将面临粘包与分包的问题

分包:

  • 要发送的数据大于TCP缓冲剩余空间
  • 待发送数据大于MSS(最大报文长度)
image

粘包:

  • 要发送的数据小于TCP缓冲区,将多次写入缓冲区的数据一起发送
  • 接收端的应用层没有及时读取缓冲区的数据

[站外图片上传中...(image-f9d2d3-1528012964593)]

自定义通信协议的两种方式

  • 定义数据包包头
image
  • 在数据包之间设置边界
image

大家可以参考 JT808协议 ------交通部808协议(车联网),也是采用类似的方式定义通信协议


手写简易Gradle Protobuf编译插件

准备proto编译器工件,proto文件目录,通过参数拼接出命令行编译proto文件,将执行结果注册到编译打包列表

定义两个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
}

定义一个插件实现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)//创建命名空间
        project.protobuf.extensions.create(PROTO_SUB_EXTENSION_NAME, ProtoExt)
        //在gradle分析之后执行
        project.afterEvaluate {
            if (!project.protobuf.protoc.path) {
                if (!project.protobuf.protoc.artifact) {
                    throw new GradleException("请配置protoc编译器")
                }
                //创建依赖配置
                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)
                //获得对应dependency的所有文件
                File file = config.fileCollection(dependency).singleFile
                println file
                if (!file.canExecute() && !file.setExecutable(true)) {
                    throw new GradleException("protoc编译器无法执行")
                }
                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 ->
                    //将任务加入构建过程,并将第二个参数的文件注册到编译列表当中
                    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)
            }
        }
    }
}

实现一个DefaultTask子类,主要是通过输入参数拼接出如下的编译所需的命令行

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

class CompileProtobufTask extends DefaultTask {

    CompileProtobufTask() {
        group = 'Protobuf'
        outputs.upToDateWhen { false } //关闭增量构建,否则输入输出不变时执行增量构建
    }
    
    @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 "执行:$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
                }
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 简介 protoBuf是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言...
    ssochi阅读 2,962评论 0 2
  • 文章内容源自Google官方文档翻译,详见原文Language Guide。部分内容可能重复,望多见谅。 假设你想...
    Jancd阅读 2,092评论 0 0
  • 1、下载编译 git clone ...... sudo apt-get install autoconf aut...
    苏恨阅读 1,019评论 0 0
  • ProtoBuf 最近看书,看到了Protobuf概念,今天学习了解下 什么是Protobuf 官方给出的解释:P...
    践行者阅读 908评论 0 2
  • protobuf 学习 前言 最近由于个人的兴趣转到了消息箱业务线,学习IM相关知识。说到IM,首先会讲到使用TC...
    MikeZhangpy阅读 5,379评论 0 6