Protocol Buffers初探
ProtoBuf是Google提出的语言中立、平台无关、可扩展的数据序列化协议。比起XML、JSON等格式它更小、更简单、更快。
使用ProtoBuf,你只需要定义消息结构一次,就可以自动生成各种语言的代码来读写消息结构。
你需要ProtoBuf编译器,才能把消息定义文件编译为各种语言的源代码。执行下面的命令安装编译器:
1 2 3 4 5 6 |
git clone https://github.com/google/protobuf.git cd protobuf/ ./autogen.sh ./configure --prefix=/usr make && sudo make install sudo ldconfig |
按照上述命令安装ProtoBuf后,路径/usr/include/google/protobuf下会包含一些proto文件。
上述构建步骤完成后,你可以使用protoc命令来编译.proto文件,产生特定编程语言的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
protoc --proto_path=IMPORT_PATH # .proto文件中的import指令的搜索路径,默认当前目录 # 不同语言代码的输出目录 --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR # 待编译的proto文件 path/to/file.proto |
ProtoBuf在.proto文件中定义消息结构或者RPC服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
/* ProtoBuf版本 */ syntax = "proto3"; // 导入其它消息定义文件 import "google/protobuf/timestamp.proto"; // 包名,默认对应Go的包名 package gmem; // 定义消息,消息是一系列字段的聚合 message User { // 保留标签和字段名。这些是以前曾经使用,但是废弃的字段。为了保证新旧定义的兼容性,废弃的字段应该不再使用 reserved 15, 9 to 11; reserved "foo", "bar"; // 支持的数据类型: // bool float double string bytes // 变长整数:int32 int64 uint32 uint64 sint32 sint64 // 固定长度整数:fixed32 fixed64 sfixed32 sfixed64 // 等于号的后面的数字必须是唯一的,作为串行化后字段的标签。1-15号字段比起16+字段串行化后少一个字节 string stringType = 1; // A string must always contain UTF-8 encoded or 7-bit ASCII text. Default value = "" // Number Types, Default Value = 0 int32 int32Type = 2; // Uses Variable Length Encoding. Inefficient For Negative Numbers, Instead Use sint32. int64 int64Type = 3; // Uses Variable Length Encoding. Inefficient For Negative Numbers, Instead Use sint64. uint32 uInt32Type = 4; // Uses Variable Length Encoding uint64 uInt64Type = 5; // Uses Variable Length Encoding sint32 sInt32Type = 6; // Uses Variable Length Encoding. They are efficient in encoding for negative numbers. // Use this instead of int32 for negative numbers sint64 sInt64Type = 7; // Uses Variable Length Encoding. They are efficient in encoding for negative numbers. // Use this instead of int64 for negative numbers. fixed32 fixed32Type = 8; // Always four bytes. More efficient than uint32 if values are often greater than 2^28. fixed64 fixed64Type = 9; // Always eight bytes. More efficient than uint64 if values are often greater than 2^56 sfixed32 sfixed32Type = 10; // Always four bytes. sfixed64 sfixed64Type = 11; // Always Eight bytes. bool boolType = 12; // Boolean Type. Default Value = false bytes bytesType = 13; // May contain any arbitrary sequence of bytes. Default Value = Empty Bytes double doubleType = 14; float floatType = 15; // 消息内部可以定义枚举,枚举本身不是字段 enum TelType { MOBILE = 0; // Tag 0 is always used as default in case of enum HOME = 1; WORK = 2; OTHER = 3; } // 可以内嵌消息定义,消息定义本身不是字段 message TelNum { string number = 1; TelType type = 2; } // 如果声明repeated则字段可以重复0-N次。字段的顺序在串行时被保留 repeated TelNum phones = 3; repeated string aliases = 4; // 多个字段,但是每个消息只会出现其中一个 oneof version { string version = 5; int32 apiversion = 6; } // 映射格式支持,这类字段不能是repeated map<string, User> relatedUsers = 7; // ProtoBuf 3提供的字段类型 google.protobuf.Timestamp dob = 8; } // 在Message 中可以引用其它消息类型 message Person { string fname = 1; string sname = 2; } message City { Person p = 1; } // Message定义可以是内嵌的 message NestedMessages { message FirstLevelNestedMessage { string firstString = 1; message SecondLevelNestedMessage { string secondString = 2; } } FirstLevelNestedMessage msg = 1; FirstLevelNestedMessage.SecondLevelNestedMessage msg2 = 2; } // Any类型:允许你使用消息作为嵌入类型,且不需要它们的proto定义,Any持有任意的串行化为字节数组的数字+一个代表 // 其全局唯一类型标识符的URL import "google/protobuf/any.proto"; message AnySampleMessage { repeated google.protobuf.Any.details = 1; } // 任意值类型的映射 map<string, google.protobuf.Any> // 类似于结构 google.protobuf.Struct |
你可以在.proto中定义一个RPC服务:
1 2 3 |
service UserService { rpc Query (string) returns (User); } |
ProtoBuf编译器可以生成服务的(指定语言的)接口代码和桩代码。
你可以在.proto文件中声明一系列的选项,这些选项不会从根本上改变消息结构或者服务语义,但是会在特定上下文中影响编译器的行为:
1 2 3 4 5 6 |
// 生成的Java包名 option java_package = "cc.gmem"; // 在不同的Java文件中生成顶级消息、枚举、服务的定义 option java_multiple_files = true; // 影响Java和C++代码生成器的行为 option optimize_for = SPEED | CODE_SIZE | LITE_RUNTIME; |
对于.proto文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
syntax = "proto3"; import "google/protobuf/timestamp.proto"; package digital; option java_package = "com.dangdang.digital"; message Media { uint64 mediaId = 1; uint64 saleId = 2; string title = 3; uint32 chapterCnt = 4; uint32 wordCnt = 5; bool isFull = 6; string authorName = 7; string publisher = 8; google.protobuf.Timestamp publishDate = 9; uint32 price = 10; uint32 paperBookPrice = 11; } |
需要安装protoc-gen-go:
1 |
go get -u github.com/golang/protobuf/protoc-gen-go |
注意$GOPATH/bin需要加入到PATH环境变量。
执行下面的命令将上节的.proto编译为Go代码:
1 2 3 4 |
cd /home/alex/Go/workspaces/default/digital-api mkdir digital protoc -I=proto --go_out=./digital Media.proto |
生成的文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
package digital import proto "github.com/golang/protobuf/proto" import fmt "fmt" import math "math" import google_protobuf "github.com/golang/protobuf/ptypes/timestamp" // 下面的语句用于抑制unused import报错 var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // 生成的Go结构体,注意字段名被自动大写 type Media struct { MediaId uint64 `protobuf:"varint,1,opt,name=mediaId" json:"mediaId,omitempty"` SaleId uint64 `protobuf:"varint,2,opt,name=saleId" json:"saleId,omitempty"` Title string `protobuf:"bytes,3,opt,name=title" json:"title,omitempty"` ChapterCnt uint32 `protobuf:"varint,4,opt,name=chapterCnt" json:"chapterCnt,omitempty"` WordCnt uint32 `protobuf:"varint,5,opt,name=wordCnt" json:"wordCnt,omitempty"` IsFull bool `protobuf:"varint,6,opt,name=isFull" json:"isFull,omitempty"` AuthorName string `protobuf:"bytes,7,opt,name=authorName" json:"authorName,omitempty"` Publisher string `protobuf:"bytes,8,opt,name=publisher" json:"publisher,omitempty"` PublishDate *google_protobuf.Timestamp `protobuf:"bytes,9,opt,name=publishDate" json:"publishDate,omitempty"` Price uint32 `protobuf:"varint,10,opt,name=price" json:"price,omitempty"` PaperBookPrice uint32 `protobuf:"varint,11,opt,name=paperBookPrice" json:"paperBookPrice,omitempty"` } // 为结构定义了一些方法 func (m *Media) Reset() { *m = Media{} } func (m *Media) String() string { return proto.CompactTextString(m) } func (*Media) ProtoMessage() {} func (*Media) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } // 为所有字段定义了Get func (m *Media) GetMediaId() uint64 { if m != nil { return m.MediaId } return 0 } // ... |
执行下面的命令将上节的.proto编译为Java代码:
1 2 |
# Java包所需的目录会自动生成 protoc -I=proto --java_out=/home/alex/JavaEE/projects/idea/digital-client/src/main/java Media.proto |
需要添加Maven依赖:
1 2 3 4 5 |
<dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.5.1</version> </dependency> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
media := &digital.Media{ AuthorName: "汪震", ChapterCnt: 8, IsFull: true, MediaId: 1000000, PaperBookPrice: 6400, Price: 640, PublishDate: &google_protobuf.Timestamp{Seconds: int64(time.Now().Second())}, } // 序列化为字节 out, err := proto.Marshal(media) if err != nil { log.Fatal(err) } log.Printf("marshalled size: %v", len(out)) fname := "/tmp/media.data" ioutil.WriteFile(fname, out, 0644) // 反序列化 in, err := ioutil.ReadFile(fname) media = new(digital.Media) proto.Unmarshal(in, media) log.Println(media.AuthorName) |
使用Struct类型,可以方便的映射map[string]any:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var vars map[string]any var variables *structpb.Struct variables, err = structpb.NewStruct(vars) if err != nil { return nil, err } return &GetInstanceVariablesResponse{ // *_struct.Struct 对应"google/protobuf/struct.proto"的google.protobuf.Struct Variables: variables, }, nil // 要将Struct转换为map,只需要 variables.AsMap() |
使用Struct类型,可以方便的映射字典:
1 2 3 4 5 6 7 8 9 10 |
from google.protobuf.struct_pb2 import Struct from google.protobuf import json_format data = { "id": 123, "name": "example" } struct_obj = Struct() json_format.ParseDict(data, struct_obj) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package com.dangdang.digital; import com.google.protobuf.Timestamp; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.Test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class ProtoBufTest { private static final Logger LOGGER = LogManager.getLogger( ProtoBufTest.class ); public static final String FNAME = "/tmp/media.data"; @Test public void testWrite() throws IOException { MediaOuterClass.Media.Builder builder = MediaOuterClass.Media.newBuilder() .setAuthorName( "汪震" ) .setChapterCnt( 8 ) .setIsFull( true ) .setMediaId( 1000000 ) .setPaperBookPrice( 6400 ) .setPrice( 640 ) .setPublishDate( Timestamp.newBuilder().setSeconds( System.currentTimeMillis() / 1000 ).build() ); // 生成Java对象 MediaOuterClass.Media media = builder.build(); FileOutputStream fos = new FileOutputStream( FNAME ); // 写入到文件 media.writeTo( fos ); } @Test public void testRead() throws IOException { FileInputStream fis = new FileInputStream( FNAME ); MediaOuterClass.Media media = MediaOuterClass.Media.parseFrom( fis ); LOGGER.debug( media.getAuthorName() ); } } |
开源项目protoc-go-inject-tag支持为Protobuf生成的结构体添加自定义标签:
1 |
go get github.com/gmemcc/protoc-go-inject-tag |
1 2 3 4 |
message Media { // @inject_tag: db:"media_id" uint64 mediaId = 1; } |
对于上述proto,执行命令:
1 2 3 |
protoc -I=proto --go_out=. Media.proto # 生成Tag: protoc-go-inject-tag -input=Media.pb.go -remove=true |
最终的结构如下:
1 2 3 4 |
type Media struct { // @inject_tag: db:"media_id" MediaId uint64 `protobuf:"varint,1,opt,name=mediaId" json:"mediaId,omitempty" db:"media_id"` } |
此项目Fork了golang/protobuf,提供了额外的代码生成器。支持特性:
- 更快的序列化/反序列化
- 更加规范的Go结构
- 和goprotobuf兼容
- 支持生成测试、Benchmark代码
要生成具有更快序列化/反序列化能力的代码,安装:
1 |
go get github.com/gogo/protobuf/protoc-gen-gofast |
示例:
1 |
protoc --gofast_out=. myproto.proto |
使用此特性,就不能使用任何其它的gogoprotobuf扩展。
调用形式和golang/protobuf一样:
1 |
protoc --gofast_out=plugins=grpc:. my.proto |
安装protoc-gen-gogo:
1 2 3 4 |
go get github.com/gogo/protobuf/proto go get github.com/gogo/protobuf/jsonpb go get github.com/gogo/protobuf/protoc-gen-gogo go get github.com/gogo/protobuf/gogoproto |
此扩展允许定制生成结构的JSON Tag,示例:
1 |
bool include_public = 2 [(gogoproto.jsontag) = "includePublic,omitempty"]; |
此命令能够解析Proto文件,然后根据你指定的选项来生成代码。
1 |
protoc [OPTION] PROTO_FILES |
选项 | 说明 | ||
-IPATH -I PATH --proto_path=PATH |
指定从中搜索Imports的目录,也可以指定目标Proto文件,此选项可以指定多次。如果不指定则使用当前目录 如果在Proto文件中引入:
假设上述Proto文件在你的GOPATH下,则你需要指定 -I $GOPATH/src才能生成代码 |
||
--plugin=EXECUTABLE | 指定需要使用的插件,默认情况下protoc在$PATH下寻找插件 | ||
--cpp_out=OUT_DIR | 生成C++头和源码 | ||
--java_out=OUT_DIR | 生成Java源码 | ||
--js_out=OUT_DIR | 生成JavaScript源码 | ||
--python_out=OUT_DIR | 生成Python源码 | ||
@<filename> | 从文件读取选项 | ||
--go_out | 调用protoc-gen-go来生成Go代码 | ||
--gogo_out | 调用protoc-gen-gogo来生成Go代码 |
1 2 3 4 5 6 7 |
protoc \ # 从这里寻找impirt -I$GOPATH/src \ # 直接指定Proto源文件 -Ipkg/apis/chart/v1 pkg/apis/chart/v1/chart_service.proto \ # 基于protoc-gen-gogo来生成Go代码,启用grpc支持,输出的根目录为当前目录,具体路径取决于go_package --gogo_out=plugins=grpc:. |
插件Protobuf Support为IntelliJ平台提供了proto编辑器,完整支持Protocol Buffer 3。安装后,需要注意设置Include Path:
如果需要使用google.protobuf.Timestamp等类型,则需要将ProtoBuf安装前缀/include目录包含在列表中。
很强大。
:D