定义proto文件
首先看下这次用到的proto文件
syntax = "proto3";
option go_package="./;protofile";
package protofile;
message Req {
string message = 1;
}
message Res {
string message = 1;
}
service HelloGRPC {
rpc SayHi(Req) returns (Res);
}
syntax
文件的第一行指定当前proto文档使用的是proto3语法,这是必须的,因为当不指定版本时,默认使用的是proto2语法。
这也必须是文件的第一个非空、非注释行。
message
两个message均只指定了三个字段(名称/值对),这是内部在进行序列话与反序列化时的依赖。
字段编号:
消息定义中的每个字段应当在当前消息中有唯一的编号,而且一旦使用就不应该进行修改。
1到15范围内的字段编号占用一个字节进行编码,包括字段编号和字段类型。
16到2047范围内的字段编号占用两个字节,所以当某一些元素出现频率非常高时,应当分配到1到15之间,但是也应当适当保留一些1到15之间到字段出来,为将来可能出添加的频繁出现的元素预留空间。
service
如果想在 RPC(远程过程调用)系统中使用定义消息类型,可以在.proto文件中定义一个 RPC 服务接口,协议缓冲区编译器将以选择的语言生成服务接口代码和存根。因此,例如,如果想使用接受定义的Req并返回 a的方法定义 RPC 服务Res,可以在定义的.proto文件中按如下方式定义它:
service HelloGRPC {
rpc SayHi(Req) returns (Res);
}
可以发现,定义时还是非常简单的,通过rpc关键字,服务名(SayHi),请求(Req), returns关键字,响应(Res),就可以定义一个rpc服务。
package
同样的,在proto文件内也可以使用包说明符package定义包的名字
包说明符影响生成代码的方式取决于选择的语言:
- 在C++ 中,生成的类被包装在 C++ 命名空间中。例如,Open将在命名空间中foo::bar。
- 在Java和Kotlin 中,该包用作 Java 包,除非您option java_package在.proto文件中明确提供。
- 在Python 中, package 指令被忽略,因为 Python 模块是根据它们在文件系统中的位置组织的。
- 在Go 中,包用作 Go 包名称,除非您option go_package在.proto文件中明确提供。
- 在Ruby 中,生成的类被包裹在嵌套的 Ruby 命名空间中,转换为所需的 Ruby 大写样式(第一个字母大写;如果第一个字符不是字母,PB_则在前面)。例如,Open将在命名空间中Foo::Bar。
- 在C# 中,包在转换为 PascalCase 后用作命名空间,除非您option csharp_namespace在.proto文件中明确提供 an 。例如,Open将在命名空间中Foo.Bar。
option go_package
可以发现在上面的proto文件内有option go_package的出现,如果不正确设置这个选项,在生成go文件时,会报错提示。
Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.
这里可以如下设置:
option go_package="./;protofile";
这里的protofile是包名,与package关键字指定的包名保持一致就可以。前面的./
是指定当前路径。
自动编译脚本
由于我是mac环境,而且命令比较长容易打错,所以将命令放在了sh脚本内,这同样适用于linux,如果是windows平台,可以将命令放进bat脚本中,效果是一样的。
命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./test.proto
- protoc 是 protobuf 文件的编译器,可以将 .proto 文件转译成各编程语言对应的代码,需要安装 protobuf。
- go_out / go_opt 参数是转译成 go 代码需要的,但原生 protoc 不包含 go 的插件,需要安装 protoc-gen-go: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
这里的go_out是生成go代码,同样的也支持其他语言:
–cpp_out=OUT_DIR Generate C++ header and source.
–csharp_out=OUT_DIR Generate C# source file.
–java_out=OUT_DIR Generate Java source file.
–js_out=OUT_DIR Generate JavaScript source.
–kotlin_out=OUT_DIR Generate Kotlin file.
–objc_out=OUT_DIR Generate Objective-C header and source.
–php_out=OUT_DIR Generate PHP source file.
–python_out=OUT_DIR Generate Python source file.
–ruby_out=OUT_DIR Generate Ruby source file.
- go-grpc_out / go-grpc_opt 可以转译成 grpc 需要的 go 代码,需要 protoc-gen-go-grpc 编译插件:go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
这里以go_out / go_opt为例说一下这俩的使用方式
--go_out=.
指的是生成go文件,且生成后输出在本目录
–go_out 参数是用来指定 protoc-gen-go 插件的工作方式 和 go 代码目录架构的生成位置,可以向 --go_out 传递很多参数。
主要的两个参数为
plugins
和paths
,代表 生成 go 代码所使用的插件 和 生成的 go 代码的目录怎样架构。
paths 参数有两个选项,
import
和source_relative
。默认为 import ,代表按照生成的 go 代码的包的全路径去创建目录层级,source_relative 代表按照 proto 源文件的目录层级去创建 go 代码的目录层级,如果目录已存在则不用创建。
这里给出个实例:如何写一个 gRPC 服务端
protoc \
-I=. \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
*.proto
含义:
- protoc 是 protobuf 文件的编译器,可以将 .proto 文件转译成各编程语言对应的代码,需要安装 protobuf。
- go_out / go_opt 参数是转译成 go 代码需要的,但原生 protoc 不包含 go 的插件,需要安装 protoc-gen-go: go get github.com/golang/protobuf/protoc-gen-go
- go-grpc_out / go-grpc_opt 可以转译成 grpc 需要的 go 代码,需要 protoc-gen-go-grpc 编译插件:go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
- -I: 指定 .proto 文件路径。如果 .proto 中 import 了其他 .proto 文件,还需要追加 -I=xxx,关于 import 的注意,见后面。
- –go_out: .pb.go 文件生成后放在哪
- –go_opt=paths=source_relative: .pb.go 文件依赖相对路径,不加这个参数则会在当前目录下生成完整的 go_package 指定的路径
- –go-grpc_out: 同时生成 _grpc.pb.go 文件,里面是原本在 .pb.go 文件中的和 gRPC,client,server 相关的代码
- –go_grpc_opt=paths=source_relative: _grpc.pb.go 文件依赖相对路径,不加这个参数则会在当前目录下生成完整的 go_package 指定的路径
- *.proto: 需要生成 go 代码的 .proto 文件
此时我们的这个sh脚本运行就会自动执行命令生成test.pb.go和test_grpc.pd.go文件。
grpc使用
先来看下测试项目目录结构
.
├── go_server
│ └── main_server.go
├── go.mod
├── go.sum
├── go_client
│ └── main_client.go
└── protofile
├── build.sh
├── test.pb.go
├── test.proto
└── test_grpc.pb.go
go grpc服务端
在go_server/main_server.go中添加如下代码:
package main
import (
"GRPC/protofile"
"context"
"fmt"
"google.golang.org/grpc"
"net"
)
type server struct {
protofile.UnimplementedHelloGRPCServer
}
func (s *server) SayHi(ctx context.Context, req *protofile.Req) (res *protofile.Res, err error){
fmt.Println(req.GetMessage())
return &protofile.Res{Message: "服务端响应"}, nil
}
func main(){
listen, _ := net.Listen("tcp", ":8000")
s := grpc.NewServer()
protofile.RegisterHelloGRPCServer(s, &server{})
s.Serve(listen)
}
可以发现首先定义server struct,然后匿名内嵌rotofile.UnimplementedHelloGRPCServer
下面是其定义:
// UnimplementedHelloGRPCServer must be embedded to have forward compatible implementations.
type UnimplementedHelloGRPCServer struct {
}
顺着再往下看,发现这个结构上挂在了一个方法,即SayHi。
func (UnimplementedHelloGRPCServer) SayHi(context.Context, *Req) (*Res, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHi not implemented")
}
所以我们是通过匿名内嵌这个结构体,然后重写其拥有的方法,进而实现方法的重写。
在重写方法时要记得函数的输入输出与函数原型一致,当然了不一致也是无法运行的。
//原型
type UnimplementedHelloGRPCServer struct {
}
func (UnimplementedHelloGRPCServer) SayHi(context.Context, *Req) (*Res, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHi not implemented")
}
//重写方法
type server struct {
protofile.UnimplementedHelloGRPCServer
}
func (s *server) SayHi(ctx context.Context, req *protofile.Req) (res *protofile.Res, err error){
fmt.Println(req.GetMessage())
return &protofile.Res{Message: "服务端响应"}, nil
}
这里仅实现了一个小例子,当然后期是可以拓展更多功能的。
listen, _ := net.Listen("tcp", ":8000")
主函数内使用net包,进行端口监听,使用的是tcp协议。
grpc底层传输层是使用的tcp进行传输数据,grpc和protobuf实现上层应用,对传输的数据进行序列化以及反序列化,再实现具体功能。
s := grpc.NewServer()
新建一个grpc服务
protofile.RegisterHelloGRPCServer(s, &server{})
grpc服务初始化
s.Serve(listen)
运行服务
go grpc客户端
客户端明显更加简单
先来看下全部代码:
package main
import (
"GRPC/protofile"
"context"
"fmt"
"google.golang.org/grpc"
)
func main(){
conn,_ := grpc.Dial("localhost:8000", grpc.WithInsecure())
defer conn.Close() // 不这样做会一直无法关闭
client := protofile.NewHelloGRPCClient(conn)
req, _ := client.SayHi(context.Background(), &protofile.Req{Message: "客户端消息"})
fmt.Println(req.GetMessage())
}
conn,_ := grpc.Dial("localhost:8000", grpc.WithInsecure())
使用grpc拨号,第二个参数是可以设置一些安全信息的,但是现在只是测试,先以不安全模式运行就可以。
client := protofile.NewHelloGRPCClient(conn)
调用grpc服务连接
req, _ := client.SayHi(context.Background(), &protofile.Req{Message: "客户端消息"})
调用服务端的方法。
可以发现,服务端的protofile.NewHelloGRPCClient()也好,客户端的protofile.NewHelloGRPCClient()也好,都已将帮我们把底层的一些操作封装好了,我们只需要调用这些根据proto文件生成的服务就可以快速搭建起自己的GRPC服务,而且仔细观察可以发现这些封装好的函数命名与我们定义的服务名有着剪不断的联系。