文章目录
-
- [一、Protocol Buffer简介](#一、Protocol Buffer简介)
- [二、Protocol Buffer编译器安装](#二、Protocol Buffer编译器安装)
- 三、proto3语言指南
- 四、序列化与反序列化
- 五、引入grpc-gateway
相关参考链接:
一、Protocol Buffer简介
作为一个后端开发,我们用的最多的传递信息的协议是http协议。性能需求的提高,要求我们转向性能更高的协议,这就不得不提到一个由Google公司推广的一个小而快的rpc协议------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下载地址
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 <string, Pets> 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<string, string> 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服务中。
grpc-gatway
是在grpc上做的一个拓展。但是grpc并不能很好的支持客户端,以及传统的RESTful API 。因此grpc-gateway
诞生了,该项目可以为grpc服务提供HTTP+JSON接口。
1、插件安装
首先,在项目中去创建一个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
2、定义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 和 google/protobuf 。下载下来,放在当前项目中的根目录下。注释依赖一般可以自动检测,不行就手动配置依赖。
3、生成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
4、实现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
}
5、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())
}
6、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
}
7、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)
}
8、验证
启动服务之后,进行接口验证。
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'