proto是在当今使用最广泛的IDL之一,起因是dubbo3的Triple 协议需要用到proto文件来生成统一规范的跨语言代码,Grpc也有类似的问题,想想一个团队有很多的业务模块,涉及到一些相互调用依赖的问题,如 A模块需要用到B模块的接口,就需要找到B模块开发者,请告知一下 B模块相关的proto文件是哪些,我需要copy到A模块来生成客户端调用代码,虽说这个场景单看起来条理是清晰的,后续如果越来越多的模块需要相互引用依赖,版本变更 ,昨天提供给你的proto文件 今天已经被提供者加了字段 或者删减了字段,需要一一通知到位,并需要重新copy最新的proto文件给使用者,如果B模块又依赖了C模块,这个时候如果使用者要用到B模块的时候 需要把B、C相关的proto都要提供,处理这类繁琐易错的问题还是相当复杂的。所以让buf来帮你解决这个问题。

buf 可以解决

  • 统一管理proto文件 类似git一样 区分仓库 Buf Schema Registry(BSR)支持远程拉取、推送、提交。
  • 检查proto依赖以及语法问题
  • 检查兼容性问题
  • 生成多语言代码 根据buf.gen.yaml 自定义配置非常很方便

buf安装

以下软件环境均在linux x64,需要安装go环境。官方提供源码、二进制包、Tarball 3种安装方式 为了方便这里使用官方二进制包方式。

BIN="/usr/local/bin" && \
VERSION="1.10.0" && \
curl -sSL \
"https://github.com/bufbuild/buf/releases/download/v${VERSION}/buf-$(uname -s)-$(uname -m)" \
-o "${BIN}/buf" && \
chmod +x "${BIN}/buf"

验证buf命令是否安装成功 能输出一系类的命令使用帮助表示安装就没问题。

buf --help

Available Commands:
beta Beta commands. Unstable and likely to change.
breaking Verify that the input location has no breaking changes compared to the against location.
build Build all Protobuf files from the specified input and output a Buf image.
completion Generate auto-completion scripts for commonly used shells.
convert Convert a message from binary to JSON or vice versa
export Export the files from the input location to an output location.
format Format all Protobuf files from the specified input and output the result.
generate Generate stubs for protoc plugins using a template.
help Help about any command
lint Verify that the input location passes lint checks.
ls-files List all Protobuf files for the input.
mod Manage Buf modules.
push Push a module to a registry.
registry Manage assets on the Buf Schema Registry.

再安装一个protoc 针对后面java 、c++语言的生成代码演示 如果只是为了生成grpc go的话可以不安装

wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip

#把protoc可执行文件放再 /usr/local/bin 方便直接执行
cp bin/protoc /usr/local/bin/
#验证
protoc --version

输出libprotoc 3.21.12 表示安装成功。

安装protoc go grpc代码生成工具 用于后续生成go代码

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$PATH:$(go env GOPATH)/bin"

build ls-files

clone示例proto文件仓库用于下面演示

git clone https://github.com/DemoHubs/bufbuilddemo.git

该演示项目大概是一个宠物服务的API接口定义。目录中有2个文件夹 start目录表示为你准备演示工作的proto文件, finish目录表示 start相关演示执行后的目录的样子 这里先不关注finish目录 。

ls-files 用于查询目录下的proto文件列表
cd bufbuilddemo 
buf ls-files
-----------输出---------
finish/paymentapis/payment/v1alpha1/payment.proto
finish/petapis/pet/v1/pet.proto
start/petapis/google/type/datetime.proto
start/petapis/pet/v1/pet.proto

你也可以用仓库clone到本地 用远程输入仓库的方式查询 还是非常方便的

buf ls-files "https://github.com/DemoHubs/bufbuilddemo.git#branch=main,subdir=start/petapis"
------------输出---------
start/petapis/google/type/datetime.proto
start/petapis/pet/v1/pet.proto

其中远程输入的地址加了参数

branch 表示查看的是哪个分支 这里是main分支

subdir 表示查询的文件加是哪一个 支持多层级查找 这里查询的是 仓库种start/petapis目录下包含的proto文件列表

输入文档类型 支持 git dir mod tar zip 等主流的方式。

配置一个新的buf空间

进入bufbuilddemo/start/petapis,执行 buf mod init 会在当前目录中生成默认的buf.yaml文件,如果是一个新模块 就从这里开始让buf来管理你的proto文件。

cd bufbuilddemo/start/petapis
buf mod init
----- 生成的buf.yaml文件--------------
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT

lint 检查你的proto规范

回到示例仓库 。进入bufbuilddemo/start/petapis 。

buf lint 
------输出--------------
google/type/datetime.proto:17:1:Package name "google.type" should be suffixed with a correctly formed version, such as "google.type.v1".
pet/v1/pet.proto:44:10:Field name "petID" should be lower_snake_case, such as "pet_id".
pet/v1/pet.proto:49:9:Service name "PetStore" should be suffixed with "Service".

上面输入的可读性很差 可以指定以json的方式输出就好看多了。

buf lint --error-format=json
------输出----------
{"path":"google/type/datetime.proto","start_line":17,"start_column":1,"end_line":17,"end_column":21,"type":"PACKAGE_VERSION_SUFFIX","message":"Package name \"google.type\" should be suffixed with a correctly formed version, such as \"google.type.v1\"."}
{"path":"pet/v1/pet.proto","start_line":44,"start_column":10,"end_line":44,"end_column":15,"type":"FIELD_LOWER_SNAKE_CASE","message":"Field name \"petID\" should be lower_snake_case, such as \"pet_id\"."}
{"path":"pet/v1/pet.proto","start_line":49,"start_column":9,"end_line":49,"end_column":17,"type":"SERVICE_SUFFIX","message":"Service name \"PetStore\" should be suffixed with \"Service\"."}

可以看到规范检查输出了错误,这些buf默认的规范检查定义 在buf.yaml中可以看到 lint.use=DEFAULT。当然也是可以去掉某些规则

第1个错误 google/type/datetime.proto 第17行后缀不对。规则 PACKAGE_VERSION_SUFFIX

第2个错误 pet/v1/pet.proto 文件44行petID命名不对。规则 FIELD_LOWER_SNAKE_CASE

第3个错误 pet/v1/pet.proto 文件49行PetStore命名不对 必须要以Service结尾。规则 SERVICE_SUFFIX

添加except节点 人为的排除掉一些lint规则 如下 把这3个规则都排除了。重新执行buf lint就没有看到任何输出就证明 通过了lint检查。

还有更暴力的忽略检查ignore 指定Proto文件配置 在一些特殊场景可以使用

version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
ignore:
- google/type/datetime.proto
except:
# - PACKAGE_VERSION_SUFFIX
- FIELD_LOWER_SNAKE_CASE
- SERVICE_SUFFIX

为了验证修复 先把节点去掉 except 。不排除默认lint规则。

43 message DeletePetRequest {
44 string pet_id = 1; //petID 改为 pet_id
45 }
49 service PetStoreService {//PetStore改为PetStoreService
50 rpc GetPet(GetPetRequest) returns (GetPetResponse) {}

改了之后 在执行buf lint 就没有错误了。

breaking 兼容性中断检查

上面lint主要是检查proto语法相关的规范,但是如果 Pet 中的属性 pet_type 是PetType 类型 已经提供给使用者在用了,这个时候如果改变了数据类型 就兼容不正在使用的客户端,所以用breaking来解决对历史版本的兼容性问题筛查。

为了演示 把Pet的pet_type类型 从PerType变更为string。

message Pet {
string pet_type = 1;//PetType pet_type = 1;
string pet_id = 2;
string name = 3;
google.type.DateTime created_at = 4;
}

执行中段检查命令 需要传入一个against参数 用于本地和主干文件做对比 同样也支持像上面的设置分支和目录

  buf breaking --against "https://github.com/DemoHubs/bufbuilddemo.git#branch=main,subdir=start/petapis" --error-format=json
-----------输出------------
{"path":"pet/v1/pet.proto","type":"SERVICE_NO_DELETE","message":"Previously present service \"PetStore\" was deleted from file."}
{"path":"pet/v1/pet.proto","start_line":20,"start_column":3,"end_line":20,"end_column":9,"type":"FIELD_SAME_TYPE","message":"Field \"1\" on message \"Pet\" changed type from \"enum\" to \"string\"."}
{"path":"pet/v1/pet.proto","start_line":44,"start_column":3,"end_line":44,"end_column":21,"type":"FIELD_SAME_JSON_NAME","message":"Field \"1\" with name \"pet_id\" on message \"DeletePetRequest\" changed option \"json_name\" from \"petID\" to \"petId\"."}
{"path":"pet/v1/pet.proto","start_line":44,"start_column":10,"end_line":44,"end_column":16,"type":"FIELD_SAME_NAME","message":"Field \"1\" on message \"DeletePetRequest\" changed name from \"petID\" to \"pet_id\"."}

这里主要关注第2个错误\”Pet\” changed type from \”enum\” to \”string\”。就明确提示了 由enum变更为string。其它的错误是因为我们修复了默认的lint的几个规则。

generate code 生成代码

生成代码也是我们用这个工具的核心功能。回到start目录中 配置生成代码的模板规则文件buf.gen.yaml

代码生成
cd ../
vim buf.gen.yaml
-------- 输入模板内容------
version: v1
plugins:
- plugin: cpp
out: gen/proto/cpp
- plugin: java
out: gen/proto/java
- plugin: go
out: gen/proto/go
opt: paths=source_relative
- plugin: go-grpc
out: gen/proto/go
opt: paths=source_relative

这里配置了4个插件 分别的cpp java go go-grpc 用于生成c++ java go grpc代码

c++ 生成的源码放到gen/proto/cpp中

java 生成的源码放到 gen/proto/java中

go生成的源码放到 gen/proto/go中

go-grpc生成的源码放到 gen/proto/go中

执行生成代码命令 –template参数是指定模板的路径 不指定默认会在当前执行目录查找buf.gen.yaml

# buf generate perapis --template buf.gen.yaml
buf generate petapis

如果没有报错 当前目录中 gen代码生成成功。使用 tree gen 查看gen目录以下文件树形的方式展开。可以看到c++ java go代码均生成成功

tree gen
gen
└── proto
├── cpp
│   ├── google
│   │   └── type
│   │   ├── datetime.pb.cc
│   │   └── datetime.pb.h
│   └── pet
│   └── v1
│   ├── pet.pb.cc
│   └── pet.pb.h
├── go
│   └── pet
│   └── v1
│   ├── pet_grpc.pb.go
│   └── pet.pb.go
└── java
├── com
│   └── google
│   └── type
│   ├── DateTime.java
│   ├── DateTimeOrBuilder.java
│   ├── DateTimeProto.java
│   ├── TimeZone.java
│   └── TimeZoneOrBuilder.java
└── pet
└── v1
└── PetOuterClass.java
使用远程插件生成代码

上面在生成代码的时候配置了plugin插件,比如go 我需要先安装好,这里可以直接使用远程插件。像这样 在buf.gen.yaml中把plugin的地址写为远程的地址即可。

version: v1
plugins:
- plugin: buf.build/protocolbuffers/go:v1.28.1
out: gen/proto/go
opt: paths=source_relative
- plugin: buf.build/grpc/go:v1.2.0
out: gen/proto/go
opt: paths=source_relative
代码托管配置模式

代码托管配置模式就是对特定语言代码的参数做一个自定义的控制 不需要写死的proto文件中。让buf自己去管理特有的特性。同样是在buf.gen.yaml文件中配置

managed:
enabled: true
cc_enable_arenas: false
java_multiple_files: true

enable 启用配置托管模式

java_multiple_files 生成的java代码 多文件模式 就是不用内部类的方式了

关于更多语言的配置 参考 https://docs.buf.build/generate/managed-mode

远程仓库 BSR

BSR全称 Buf Schema Registry。可以理解为和maven的中央仓库、github这种仓库有点类似。只是bsr是用于buf远程管理仓库。要使用远程仓库功能 需要先注册https://buf.build/login 注册之后需要在 设置中添加API TOKEN https://buf.build/settings/user 用于buf cli连接到BSR的凭证。

buf cli 登录到BSR
buf registry login

按提示 输入注册的用户名 和 设置的api token。输出 Credentials saved to /root/.netrc. 后 表示登录成功。身份信息存储在/root/.netrc文件中。

buf cli 退出登录

退出登录后 自动删除了/root/.netrc文件

buf registry logout
------------ 输出-----------
All existing BSR credentials removed from /root/.netrc.
推送到远程BSR

目前 buf cli 好像还不能搭建自己的私有仓库服务。所以默认的仓库服务由buf提供。统一域名为buf.build。

仓库地址组成由 buf.build/$buf_username/$rep_name

buf_username 为上面注册buf的用户名

rep_name 为仓库的名称。

把演示的proto项目推送到BSR 这里 我的用户名为peachyy 仓库名为 bufbuilddemo –visibility 表示仓库的权限为公开。

 buf beta registry repository create buf.build/peachyy/bufbuilddemo --visibility public
------------- 输出-----------
WARN This command is in beta. It is unstable and likely to change. To suppress this warning, set BUF_BETA_SUPPRESS_WARNINGS=1
Full name Created
buf.build/peachyy/bufbuilddemo 2022-12-19T10:12:01Z

仓库创建成功了,访问buf.build/peachyy/bufbuilddemo 能看到仓库文件信息。当前还是个空仓库。

准备推送proto文件到远程

进入 petapis 目录 为buf.yaml 新增name属性 值为上面创建的远程仓库全路径

version: v1
name: buf.build/peachyy/bufbuilddemo
breaking:
use:
- FILE
.....

执行推送到主干 输出的字符串则为版本号。现在刷新在网页上刷新仓库能看到proto文件了。

buf push
-----------输出--------
63458f6007e64ac0936d88c9caeae212

如果你推送不想影响到主干 可能你的proto文件还有变更 不是很完善的时候 可以先提交草稿。同样的也会返回一个版本号

buf push --draft draft001
---------------- 输出---------------
8745a3039aa7403c8f1420a68a4b5f3e

BSR还有像github中readme文件一样 用来描述这个Proto项目。它叫 buf.md

依赖管理

可以看到 pet/v1/pet.proto 文件依赖了一个DateTime类型, DateTime类型的源文件位于 google/type/datetime.proto 。

19 message Pet {
20 string pet_type = 1;//PetType pet_type = 1;
21 string pet_id = 2;
22 string name = 3;
23 google.type.DateTime created_at = 4; //这里依赖了 google/type/datetime.proto 源文件。DateTime类型
24 }

尝试把datetime.proto文件删除后 执行buf build。可以看到报错了 因为 datetime.proto不存在

buf build
------------输出--------------
pet/v1/pet.proto:5:8:google/type/datetime.proto: does not exist
添加远程依赖到本地

新增依赖配置 deps 值是一个仓库列表

version: v1
name: buf.build/peachyy/bufbuilddemo
deps:
- buf.build/googleapis/googleapis #指定了远程依赖仓库地址 这个是官方提供的示例代码 直接拿来引用一下
breaking:
use:
- FILE
lint:
use:
- DEFAULT
except:
# - PACKAGE_VERSION_SUFFIX
# - FIELD_LOWER_SNAKE_CASE
# - SERVICE_SUFFIX
ignore:
- google/type/datetime.proto

再次执行buf build。同样是错误输出 但是多了一个警告 意思是你deps依赖库不在buf.lock中。运行 buf mod update 生成buf.lock文件 这个文件保存了deps依赖信息。

buf build 
-------------输出----------
WARN Specified deps are not covered in your buf.lock, run "buf mod update":
- buf.build/googleapis/googleapis
pet/v1/pet.proto:5:8:google/type/datetime.proto: does not exist

执行buf mod update 执行后生成的buf.lock文件记录了依赖的 远程服务地址、用户名、仓库名称、提交版本号。

buf mod update 
----------buf.lock文件内容--------
version: v1
deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: 75b4300737fb4efca0831636be94e517

如果想指定依赖具体的版本号的仓库可以指定提交id 格式 仓库地址:提交版本ID

deps:
- buf.build/peachyy/bufbuilddemo:63458f6007e64ac0936d88c9caeae212

再次执行bud build 就不报错了。说明成功引用到了远程仓库,以后不需要copy到本地后再生成代码。

buf build

buf的编译构建的时候 会生成缓存在$HOME/.cache目录中。

工作空间

buf.work.yaml使用

新创建一个支付api模块定义 用于购买宠物的时候支付使用。

mkdir -p paymentapis/payment/v1alpha1
vim paymentapis/payment/v1alpha1/payment.proto

paymentapis/payment/v1alpha1/payment.proto 定义了一个枚举PaymentProvider 和 order结构对象。

syntax = "proto3";

package payment.v1alpha1;

option go_package = "github.com/bufbuild/buf-tour/petstore/gen/proto/go/payment/v1alpha1;paymentv1alpha1";

//import "google/type/money.proto";

// PaymentProvider represents the supported set
// of payment providers.
enum PaymentProvider {
PAYMENT_PROVIDER_UNSPECIFIED = 0;
PAYMENT_PROVIDER_STRIPE = 1;
PAYMENT_PROVIDER_PAYPAL = 2;
PAYMENT_PROVIDER_APPLE = 3;
}
// Order represents a monetary order.
message Order {
string order_id = 1;
string recipient_id = 2;
//google.type.Money amount = 3;
PaymentProvider payment_provider = 4;
}

回到petapis模块 ,修改文件petapis/pet/v1/pet.proto ,新增一个支付rpc PurchasePet方法。支付请求参数引用了paymentapis模块的实体。

import "payment/v1alpha1/payment.proto";
.....
.....
service PetStoreService {
rpc GetPet(GetPetRequest) returns (GetPetResponse) {}
rpc PutPet(PutPetRequest) returns (PutPetResponse) {}
rpc DeletePet(DeletePetRequest) returns (DeletePetResponse) {}
rpc PurchasePet(PurchasePetRequest) returns (PurchasePetResponse) {} //新增支付方法
}

message PurchasePetRequest { //支付请求参数
string pet_id = 1;
payment.v1alpha1.Order order = 2; //该实体在paymentapis模块定义
}

message PurchasePetResponse {//支付响应值
}

工作空间配置文件是在buf.work.yaml中进行配置, 用directories属性列表来引入目录。这里指定了petapis paymentapis。在刚刚修改的代码中 依赖关系是petapis 依赖了paymentapis。

vim buf.work.yaml
-------buf.work.yaml内容----------
version: v1
directories:
- paymentapis
- petapis

执行buf build ,并没有报错 这种工作空间的方式让petapis 成员引用到了paymentapis模块。 如果把buf.work.yaml中 - paymentapis注释后 执行buf build就提示找不到petapis/pet/v1/pet.proto:6:8:payment/v1alpha1/payment.proto: does not exist 错误

buf build

同样的道理工作空间的方式,其他buf操作命令也是如此,包括buf {breaking,build,generate,ls-files}

工作空间关于Push远程的问题

尝试操作推送 可是报错了。

cd petapis && buf push
-----------输出-----------
Failure: pet/v1/pet.proto:6:8:payment/v1alpha1/payment.proto: does not exist

解决这个问题需要先把paymentapis模块推送到远程后 在petapis 模块中使用deps的方法加入paymentapis地址才能push。

但是buf build命令却没有问题 因为工作区就在你本地能找到,推送远程的时候 paymentapis 模块远程找不到 所以才有这个问题。

参考 https://docs.buf.build/introduction

示例仓库

https://buf.build/peachyy/bufbuilddemo

https://buf.build/peachyy/bufbuilddemo-paymentapis