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

如何使用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文件。

來看一下名為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(最大報文長度)

粘包:
- 要發(fā)送的數(shù)據(jù)小于TCP緩沖區(qū),將多次寫入緩沖區(qū)的數(shù)據(jù)一起發(fā)送
- 接收端的應(yīng)用層沒有及時讀取緩沖區(qū)的數(shù)據(jù)
[站外圖片上傳中...(image-f9d2d3-1528012964593)]
自定義通信協(xié)議的兩種方式
- 定義數(shù)據(jù)包包頭

- 在數(shù)據(jù)包之間設(shè)置邊界

大家可以參考 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
}
}
}
}