上一篇gRPC-shop:什么是 gRPC(一)主要介绍了 gRPC 基础概念,gRPC 采用 Protobuf 作为它的序列化协议格式,那么这篇文章我们简单介绍一下 Protobuf。
Protobuf 是什么
Protobuf
全称是Protocal buffers
,平时我们都会简称为pb
,下文就使用pb
代称了。
官网中对pb
的完整定义如下:
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
简单的说, pb
是一种与语言、平台无关,可扩展的序列化数据格式。它的定位类似于JSON
、XML
,但是比他们更小、更快、更简单。
对于开发者来说,我们只需要定义proto
文件。然后使用对应的IDL
编译器把.proto文件
编译成对应语言的代码即可。
从某个角度来看,.proto
文件就是接口。里面包含了接口方法、入参、出参。
如何使用
环境安装可以看这篇[1]。
来看一个简单的示例,我们创建一个order.proto
文件,内容如下:
逐行解释。
pb
有两个版本,默认版本是proto2
,如果要使用proto3
,我们就得在非空非注释的第一行代码中使用syntax = "proto3";
声明版本。
option
表示可选项配置。有些选项是文件级别的,需要在一个文件的顶级作用域定义,比如上面的option go_package = "order";
。
go_package
选项,定义的值,就是把pb
文件编译生成Go
代码后文件的package
名。这样其他生成的Go代码就可以通过关键字import
使用到这个包。
你可能还会在.proto
文件中看到类似package xxx
这样的, 比如,
package
主要用来解决不同的pb
文件存在相同消息类型名称之间的冲突。比如a.proto
有一个Common message
,b.proto
也有一个Common message
,此时需要通过package
来做区分。
那package
和go_package
有什么关系吗?
There is no correlation between the Go import path and the
package
specifier in the.proto
file. The latter is only relevant to the protobuf namespace, while the former is only relevant to the Go namespace. Also, there is no correlation between the Go import path and the.proto
import path.
没有关系。
package
只和pb
空间有关,而go_package
前面解释过了,和Go
的import
包有关,他们两者,本质上不是一个层面的。
接着我们定义了一个Order
的消息message
类型,定义消息类型是使用关键字message
定义的。
Order
定义了四个字段,对应string
、uint32
以及bool
字段类型。
消息类型中字段定义的格式为,
默认情况下,每个字段最前面的修饰符为singular
,一般省略不写。
假设一个主订单下有多个子订单,我们可以如何定义?
我们新定义了一个OrderItem
子订单的消息类型。然后在Order
中,我们使用了OrderItem
作为items
字段的类型值。
repeated
是另一种修饰符,表示允许字段重复。上面的场景即一个订单有多个子订单的概念。
编译成Go
代码,items
会变成slice
类型。
其他类型就不一一介绍,更多的类型可以查看这:Protocol Buffers v3.0.0[2]
接着我们通过命令编译(确保工具已装)生成Go相关的代码,执行如下命令:
--proto_path
指定要编译的源码的搜索路径,上面的.
表示当前目录。如果不指定--proto_path
的话,默认为pwd
。所以上面我们也可以省略成这样,
--go_out
参数告知protoc
编译器去加载对应的protoc-gen-go
工具且指定编译生成Go
代码后保存位置。当然如果是生成php
代码那就是--php_out
。对应的语言,对应--xx_out
。
最后的*.proto
表示搜索当前目录下所有.proto
文件。
那么整句命令行意思就是:编译当前目录下所有.proto
文件, 把生成的Go
代码存放到当前目录。
上面的命令执行后,当前目录下多了一个order.pb.go
文件。生成的数据结构:
可以看到生成的主订单结构中,Items
是一个切片类型。同时生成的还有一些Getxxx
方法,方便读取字段值。
等等,接口呢?
message
有了,现在我们可以在.proto
文件定义RPC
服务接口了。
我们定义了一个 OrderService
的服务,提供了一个GetOrderList
的接口,它的入参是OrderListReq
,返回类型是OrderListResp
,是不是很像我们平常定义的函数。
和平常自定义函数的不同在于,可以自定义无入参出参的函数,而在pb中接口必须携带参数,否则编译的时候会报,
Expected type name
针对不需要参数的情况,我们一般会定义一个空message
类型或者传入google.protobuf.Empty
,传入google.protobuf.Empty
不好点在于不易扩展,万一这个接口要参数了呢。
service
定义完毕,再次执行上述命令,你会发现,重新生成的order.pb.go
文件并没有变化。这是因为RPC
框架很多,protoc
编译器并不知道给我们生成哪个RPC框架的代码。
protoc-gen-go
内部已经集成了一个名为gRPC
的插件,所以我们可以通过命令行参数--go-grpc_out
生成gRPC
代码:
然后你就发现这个错误,
我们需要改动option go_package = ".;order"
为option go_package = "./;order"
。
再次执行,多出了一个order_grpc.pb.go
文件。里面就能看到grpc
客户端和服务端的api
服务。
总结
这篇主要介绍了pb
一些基础的知识,包括定义message
、service
以及通过protoc-gen-go
生成对应的Go
代码,gRPC
代码。关于 protoc
命令,后面有必要再单独写一篇文章。
下一篇,通过生成的Go
代码,完成gRPC
通信,然后正式步入主题。
参考
- https:/github.com/protocolbuffers/protobuf/releases/tag/v3.0.0[1]
- https://go.aabbccm.com/docs/main/env[2]
- https://grpc.io/docs/languages/go/quickstart/
- https://developers.google.com/protocol-buffers
- https://colobu.com/2019/10/03/protobuf-ultimate-tutorial-in-go/#option
- https://chai2010.cn/advanced-go-programming-book/ch4-rpc/ch4-02-pb-intro.html