# protobuf-demo **Repository Path**: liuwen766/protobuf-demo ## Basic Information - **Project Name**: protobuf-demo - **Description**: 如何创建一个简单的基于proto协议的grpc项目。同时支持http接口协议。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-01-26 - **Last Updated**: 2024-01-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Protocol Buffer 从入门到入门 [protobuf官方文档](https://developers.google.com/protocol-buffers) [proto3语法指南](https://developers.google.com/protocol-buffers/docs/proto3#nested) [protoc下载地址](https://github.com/protocolbuffers/protobuf/) [grpc-gateway官方文档](https://grpc-ecosystem.github.io/grpc-gateway/) [grpc-gateway readme](https://github.com/grpc-ecosystem/grpc-gateway#readme) [swagger安装文档](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md) [【当前代码Demo】](https://gitee.com/liuwen766/protobuf-demo.git) ## Protocol Buffer简介 Protocol Buffer是一个由Google开发的协议,**是一个可以对结构化数据的序列化和反序列化协议**。谷歌开发它的目的是提供一种比XML更好的方式来使系统进行通信。因此,他们致力于使其比XML**更简单、更小、更快、更易于维护**。这个协议甚至超过了JSON,具有更好的性能、更好的可维护性和更小的体积。 > 另外protoc支持多语言,以及跨平台。 > > - 天然支持C、C++、Java、Python、PHP、Ruby、Kotlin等语言。 > - 不支持Go、Dart等语言。所以基于Go语言需要额外安装插件,下面会说到。 - Why Protocol Buffer?——简单来说它更小、因此也更快。举例如下: ```go // 当前代码位于 https://gitee.com/liuwen766/protobuf-demo.git func TestSerialize(t *testing.T) { // 这个是基于proto生成的 personFromProto := &pb.Person{ Name: "我是小明", Age: 18, PhoneNum: []string{"188", "120"}, Pets: &pb.Pets{ Type: "Cat", Name: "Tom", }, } marshal1, _ := proto.Marshal(personFromProto) create, _ := os.Create(fileName) defer create.Close() n, err := create.Write(marshal1) if err != nil { log.Fatal("create.Write(marshal) has err:", err) return } log.Println("proto.Marshal——Serialize Success:", n) // 这个是Go结构体 personFromStruct := &Person{ Name: "我是小明", Age: 18, PhoneNum: []string{"188", "120"}, Pets: &Pets{ Type: "Cat", Name: "Tom", }, } marshal2, _ := json.Marshal(personFromStruct) create, _ = os.Create(fileName) defer create.Close() n, err = create.Write(marshal2) if err != nil { log.Fatal("create.Write(marshal) has err:", err) return } log.Println("json.Marshal——Serialize Success:", n) // 日志输出如下:【传递的Person信息一模一样,但是基于proto的长度只有38,它是二进制数据】 //2024/01/22 18:13:16 proto.Marshal——Serialize Success: 38 //2024/01/22 18:13:16 json.Marshal——Serialize Success: 107 } ``` ## Protocol Buffer编译器安装 编译proto文件前的环境准备工作只有简单的两个步骤: - step1、下载protoc 。 [protoc下载地址](https://github.com/protocolbuffers/protobuf/) ```shell #将protoc.exe文件配置到环境变量,配置完之后验证 $ protoc --version libprotoc 3.19.4 ``` - step2、安装go插件。 > 前面提到过protoc支持多语言,包括C、C++、Java、Python、PHP、Ruby、Kotlin等语言。 > 但是不支持Go、Dart等语言。所以基于Go语言需要额外安装插件。只需要下面一个简单的命令。 ```shell go get google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/protobuf/cmd/protoc-gen-go ``` > go get 命令会获取依赖包到go env的 `GOMODCACHE` 目录下。 > go intall 命令会将对应的可执行文件安装到go env下的 `GOPATH/bin` 目录下。 > 因此需要配置`GOPATH/bin`为环境变量。否则会报错“'protoc-gen-go' 不是内部或外部命令,也不是可运行的程序或批处理文件。” 一个简单的proto3文件样例。 ```go // 指定proto语言版本 syntax = "proto3"; // 生成*.pb.go文件的包路径 option go_package = "/pb"; // proto包路径 package protobuf.demo; message Person{ string name = 1; int32 age = 2; } ``` 通过protoc生成 *.pb.go 文件: ```shell protoc -I . --go_out=./proto ./proto/person.proto # 这里的三个 . 都表示当前目录 ``` ## proto3语言指南 通过上面编写的简单proto文件,可以发现,proto文件中定义message与我们创建一个Go语言中的struct结构体类似。针对Go语言中的一些复杂类型,例如:数组[]int、集合map、枚举enum、嵌套等,proto自然也有相对应的定义。 - repeated repeated关键字的作用是用来定义数组,使用方式是`repeated 数组类型 属性名称 = 字段编号;` ```protobuf message Person { repeated string name = 1; } ``` - map map类型的定义方式是`map <键类型,值类型> 属性名称 = 字段编号;` ,这里需要注意对于map的键类型,只能定义为基本数据类型,但是值的类型可以是任何支持的类型。 ```protobuf message Person { map pets =1; } // 嵌套 message Pets{ string Type = 1; string name = 2; } ``` - enum 对于枚举的定义,需要用到enum关键字。 ```protobuf message Person{ Sex sex = 5; } enum Sex{ Sex_MAN = 0; SEX_WOMAN = 1; } ``` 一个完整示例。 ```go // 指定proto语言版本 syntax = "proto3"; // 生成*.pb.go文件的包路径 option go_package = "/pb"; // proto包路径 package protobuf.demo; message Person{ string name = 1; int32 age = 2; bool marry = 3; repeated string phoneNum = 4; map address = 5; Sex sex = 6; Pets pets = 7; } message Pets{ string Type = 1; string name = 2; } enum Sex{ Sex_MAN = 0; SEX_WOMAN = 1; } ``` 定义完proto文件后,通过protoc生成 *.pb.go 文件,执行如下命令: ```shell protoc -I . --go_out=./proto ./proto/person.proto # 这里的三个 . 都表示当前目录 ``` ## 序列化与反序列化 以序列化和反序列化为例,演示如何使用由proto编译生成的*.pb.go文件 ```go package proto import ( "encoding/json" "log" "os" "testing" "google.golang.org/protobuf/proto" "protobuf-demo/proto/pb" ) var fileName1 = "person-proto.txt" var fileName2 = "person-json.txt" type Pets struct { Type string Name string } type Person struct { Name string Age int32 PhoneNum []string Address map[string]string Pets *Pets } // 序列化 func TestSerialize(t *testing.T) { personFromProto := &pb.Person{ Name: "我是小明", Age: 18, PhoneNum: []string{"188", "120"}, //Sex: pb.Sex_Sex_MAN, Pets: &pb.Pets{ Type: "Cat", Name: "Tom", }, } marshal1, _ := proto.Marshal(personFromProto) create, _ := os.Create(fileName1) defer create.Close() n, err := create.Write(marshal1) if err != nil { log.Fatal("create.Write(marshal) has err:", err) return } log.Println("proto.Marshal——Serialize Success:", n) // why proto?let's look look json. personFromStruct := &Person{ Name: "我是小明", Age: 18, PhoneNum: []string{"188", "120"}, //Sex: pb.Sex_Sex_MAN, Pets: &Pets{ Type: "Cat", Name: "Tom", }, } marshal2, _ := json.Marshal(personFromStruct) create, _ = os.Create(fileName2) defer create.Close() n, err = create.Write(marshal2) if err != nil { log.Fatal("create.Write(marshal) has err:", err) return } log.Println("json.Marshal——Serialize Success:", n) // 日志输出如下:所以说proto更小 //2024/01/25 18:13:16 proto.Marshal——Serialize Success: 38 //2024/01/25 18:13:16 json.Marshal——Serialize Success: 107 } // 反序列化 func TestDeserialize(t *testing.T) { read, _ := os.ReadFile(fileName1) person := pb.Person{} err := proto.Unmarshal(read, &person) if err != nil { log.Fatal("proto.Unmarshal(read, &person) has err:", err) return } log.Printf("Deserialize Success: %+v", person) } ``` ## 引入grpc-gateway grpc-gateway是protoc的一个插件,它会读取proto文件中的grpc服务的定义并将其生成RestFul JSON API,并将其反向代理到我们的grpc服务中。 ![](image\grpc-gateway.png) `grpc-gatway`是在grpc上做的一个拓展。**但是grpc并不能很好的支持客户端,以及传统的RESTful API**。因此`grpc-gateway`诞生了,该项目可以为grpc服务提供HTTP+JSON接口。 ### 插件安装 首先,在项目中去创建一个tools的文件,然后执行`go mod tidy` ```go //go:build tools // +build tools package tools import ( _ "github.com/envoyproxy/protoc-gen-validate" _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway" _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" _ "google.golang.org/protobuf/cmd/protoc-gen-go" ) ``` 通过执行`go install`将这些可执行文件安装在`GOPATH/bin`目录下。 ```sh go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 go install google.golang.org/protobuf/cmd/protoc-gen-go go install google.golang.org/grpc/cmd/protoc-gen-go-grpc ``` ### 定义proto文件 ```go // 指定proto语言版本 syntax = "proto3"; // 生成*.pb.go文件的包路径 option go_package = "/pb"; package protobuf.demo; // 导入api注释依赖【注意将这里的注释依赖包放在当前项目的根目录下】 import "google/api/annotations.proto"; message Book { int32 id = 1; string name = 2; } message CreateBookRequest { string name = 1; } message CreateBookResponse{ string code = 1; string message = 2; Book data = 3; } // 定义服务 service BookService { // 创建Book rpc CreateBook (CreateBookRequest) returns (CreateBookResponse) { option (google.api.http) = { // POST /v1/books post: "/v1/books" body: "*" }; }; } ``` 注意这里需要导入api注释依赖。 由于项目依赖了google的proto文件,所以在使用protoc生成go文件的时候,需要将依赖的proto文件复制到项目中,依赖的proto文件仓库 [google/api](https://github.com/googleapis/googleapis/tree/master/google/api) 和 [google/protobuf](https://github.com/protocolbuffers/protobuf/tree/main/src/google/protobuf/compiler) 。下载下来,放在当前项目中的根目录下。注释依赖一般可以自动检测,不行就手动配置依赖。 ### 生成go文件 执行命令 ```shell protoc -I . --grpc-gateway_out=./proto/gen --grpc-gateway_opt logtostderr=true --grpc-gateway_opt paths=source_relative --go_out=./proto/gen --go_opt paths=source_relative --go-grpc_out=./proto/gen --go-grpc_opt paths=source_relative ./proto/book.proto # 如果报错没有目录,则手动创建目录 ``` > 不要被这么长的命令唬住了,记住 . 表示当前目录就行。 > 几个命令分别对应前面安装的三个插件:protoc-gen-go、protoc-gen-go-grpc和protoc-gen-grpc-gateway > protoc -I . > --grpc-gateway_out=./proto/gen > --grpc-gateway_opt logtostderr=true --grpc-gateway_opt paths=source_relative > --go_out=./proto/gen > --go_opt paths=source_relative > --go-grpc_out=./proto/gen > --go-grpc_opt paths=source_relative > ./proto/book.proto ### 实现Service服务 ```go package service import ( "context" "log" "protobuf-demo/db" pb "protobuf-demo/proto/gen/proto" ) type BookService struct { // 这里是要实现的服务 pb.UnimplementedBookServiceServer } // 这里是实现的方法 func (b *BookService) CreateBook(ctx context.Context, req *pb.CreateBookRequest) (*pb.CreateBookResponse, error) { resp := &pb.CreateBookResponse{} db.Db.Mux.Lock() defer db.Db.Mux.Unlock() id := db.Db.GetId() book := pb.Book{ Name: req.GetName(), Id: id, } err := db.Db.Save(&book) if err != nil { return resp, err } resp.Data = &book log.Printf("user %s create a book, %+v", db.GetUserId(ctx), &book) return resp, nil } ``` ### gRPC服务启动方法 ```go package grpc import ( "log" "net" "protobuf-demo/config" "protobuf-demo/grpc/middle" pb "protobuf-demo/proto/gen/proto" "protobuf-demo/service" "google.golang.org/grpc" ) // Run grpc的启动方法 func Run() error { //Step1:监听端口,用于提供grpc服务 grpcAddr := config.GetRpcAddr() listen, err := net.Listen("tcp", grpcAddr) if err != nil { log.Fatalf("tcp listen failed: %v", err) return err } defer listen.Close() // Step2:可以为这个grpc服务加一些定制化的特性 option := []grpc.ServerOption{ // 这里可以加一些middleware中间件 grpc.UnaryInterceptor(middle.AuthInterceptor), } // Step3:创建一个grpc服务,它是空的服务,还不能接收/处理任何请求 server := grpc.NewServer(option...) // Step4:进行服务注册 registerServer(server) log.Printf("Serving gRPC on %s", listen.Addr()) // Step5:grpc服务接收从监听端口过来的流量请求,对外提供服务 return server.Serve(listen) } func registerServer(server *grpc.Server) { // 注册 BookService 服务 pb.RegisterBookServiceServer(server, service.NewBookService()) } ``` ### gateway服务启动方法 ```go package gateway import ( "context" "log" "net/http" "protobuf-demo/config" "protobuf-demo/gateway/middle" "protobuf-demo/handler" pb "protobuf-demo/proto/gen/proto" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) // Run gateway的启动方法 func Run() error { // Step1:创建一个客户端,连接grpc服务 ctx := context.Background() option := []grpc.DialOption{ // 这里可以加一些middleware中间件 grpc.WithChainUnaryInterceptor(middle.AuthInterceptor), grpc.WithTransportCredentials(insecure.NewCredentials()), } // 创建grpc连接 连接到127.0.0.1:8001 conn, err := grpc.DialContext(ctx, config.GetRpcAddr(), option...) if err != nil { log.Fatalf("dial failed: %v", err) return err } // Step2:创建一个ServeMux,它是 grpc-gateway 的请求多路复用器。 serveMuxOption := []runtime.ServeMuxOption{ // 响应拦截 runtime.WithForwardResponseOption(middle.Interceptor), // 错误页自定义 runtime.WithRoutingErrorHandler(middle.RoutingErrorHandler), // 自定义保留哪些请求头信息到整个上下文中 runtime.WithIncomingHeaderMatcher(func(s string) (string, bool) { if s == "User-Id" { return s, true } return runtime.DefaultHeaderMatcher(s) }), } mux := runtime.NewServeMux(serveMuxOption...) // 自定义一些不好用proto编译的接口,比如这里的上传/下载接口 { if err = mux.HandlePath(http.MethodPost, "/v1/objects", handler.Upload); err != nil { return err } if err = mux.HandlePath(http.MethodGet, "/v1/objects/{name}", handler.Download); err != nil { return err } } // Step3:将http路由注册到前面创建的ServeMux,通过grpc-gateway反向代理,从而提供http服务 err = newGateway(ctx, conn, mux) if err != nil { log.Fatalf("register handler failed: %v", err) return err } // Step4:创建一个http服务 server := http.Server{ Addr: config.GetHttpAddr(), // 127.0.0.1:8002 // http服务需要处理的ServeMux Handler: mux, } log.Printf("Serving Http on %s", server.Addr) // Step5:进行监听并提供服务 err = server.ListenAndServe() if err != nil { return err } return nil } func newGateway(ctx context.Context, conn *grpc.ClientConn, mux *runtime.ServeMux) error { // 注册 BookService 服务,进行反向代理 err := pb.RegisterBookServiceHandler(ctx, mux, conn) if err != nil { return err } return nil } ``` ### main函数启动 ```go package main import ( "log" "os" "protobuf-demo/gateway" "protobuf-demo/grpc" ) func main() { // 启动grpc服务 go func() { err := grpc.Run() if err != nil { log.Fatal(os.Stderr, err) os.Exit(1) } }() // 启动gateway服务 err := gateway.Run() log.Fatal(os.Stderr, err) os.Exit(1) } ``` ### 验证 启动服务之后,进行接口验证。 ```shell # 新增接口 curl --request POST \ --url http://localhost:8002/v1/books \ --header 'X-User-Id: ' \ --header 'content-type: application/json' \ --data '{ "name": "三国演义" }' ``` 接下来,完成所有的增删改查接口。如下: ```shell # 查询接口 curl --request GET \ --url http://localhost:8002/v1/books/1 \ --header 'content-type: application/json' # 更新接口 curl --request POST \ --url http://localhost:8002/v1/books/1 \ --header 'content-type: application/json' \ --data '{ "id": 1, "name": "三国演义2" }' # 删除接口 curl --request DELETE \ --url http://localhost:8002/v1/books/1 \ --header 'content-type: application/json' ```