go开发中经常使用gRPC,每次都需要重新熟悉一下,这里做个入门记录;对应代码在这里.

gRPC顾名思义是一种RPC协议。它采用Protocol Buffers来序列化数据。RPC调用一般包含两个部分,调用函数签名和请求响应格式。gRPC通过.proto文件来定义这两个部分,proto文件中定义传输结构和结构中的字段类型;protobuf支持的字段类型以及结构定义写法看这里

proto文件

例如一个简单的请求响应格式定义 hello.proto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
syntax = "proto3"; // 版本申明,放最上面

option go_package = "github.com/hulb/grpc-test/pb"; // 申明包含这个文件生成的go代码所在package的import路径

message Request{ // 结构定义
    string msg = 1; // 字段定义,前面是类型,后面是字段名
}

message Response{
    string back = 1;
}

定义有了,如何来使用呢?

gRPC提供了protoc这个编译器将定义文件编译为不同语言的代码,不同语言代码的生成通过protoc的不同插件来实现。比如go语言需要使用的protoc插件是

1
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

安装这个插件后就可以通过protoc将上述.proto文件编译生成.go代码文件。通过如下命令:

1
protoc --go_out . --go_opt=module=github.com/hulb/grpc-test hello.proto

这里--go_out参数告诉protoc要调用go语言插件生成go代码,且生成的代码文件路径; --go_opt参数设置protoc-gen-go生成代码时的输出模式,可以看这里

生成的go代码中是.proto文件定义的数据结构在go语言中的定义。

上述这个过程是将数据结构的定义编译为go代码,接下来我们增加函数签名:

1
2
3
service Greeter{ // rpc 服务定义
    rpc SayHi(Request) returns(Response){};
}

service这部分定义RPC服务,需要通过另一个protoc插件来生成对应的go代码:

go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

安装好后生成命令上要增加--go-grpc_out以及--go-grpc_opt两个参数:

1
2
3
4
5
6
protoc \
--go-rpc_out= . \
--go-rpc_out=module=github.com/hulb/grpc-test \
--go_out . \
--go_opt=module=github.com/hulb/grpc-test \
hello.proto

实现gRPC服务端和客户端

grpc代码生成后,接下来就是如何使用了。代码分为客户端和服务端。客户端很简单,去连接服务端调用接口即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
conn, err := grpc.Dial(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
    return err
}
client := pb.NewGreeterClient(conn)
resp, err := client.SayHi(cmd.Context(), &pb.Request{
    Msg:  "hi",
})
if err != nil {
    return err
}

服务端稍微麻烦点,.proto中定义了service,go代码中会对应生成interface,服务端需要实现这个接口;除此之外实现这个接口的结构需要嵌入生成的go代码中的UnimplementedGreeterServer结构:

1
2
3
4
5
6
7
8
9
type service struct {
	pb.UnimplementedGreeterServer
}

func (s *service) SayHi(ctx context.Context, req *pb.Request) (*pb.Response, error) {
	return &pb.Response{
		Back: "back,hi",
	}, nil
}

这是对应.protoservice的go里面我们实现的服务, 然后新建一个grpc server实例,将这个service注册进去。有了这个以后再就是监听端口,接受请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
ls, err := net.Listen("tcp", listen)
if err != nil {
    return err
}
defer ls.Close()

s := grpc.NewServer()
pb.RegisterGreeterServer(s, &service{})

if err := s.Serve(ls);err!=nil{
    return err
}

通过grpc.NewServer新建一个gRPC server实例,将我们实现的service通过生成代码中的RegisterGreeterServer注册到gRPC server实例,然后启动实例提供服务。

proto定义import

有时候我们需要在.proto定义中使用timestamp类型的字段来代表时间,或是struct字段来代表动态结构。google 提供了这两种以及其他类型的proto定义,下载protoc时会附带在里面,我们可以通过import的方式来使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import "timestamp.proto";
import "struct.proto";

message Request{
    string msg = 1;
    google.protobuf.Timestamp time = 2;
}

message Response{
    string back = 1;
    google.protobuf.Struct value = 2;
}

这里有两个问题,一是在生成代码是protoc如何找到通过import引入的proto定义; 二是引入的定义在使用时要带上包名,如:google.protobuf.Timestamp

先看第一个问题。在执行代码生成时可以指定--proto_path来指定要引入的proto文件路径,比如我们将google的proto文件放在google/proto下,我们可以将上面的命令改为:

1
2
3
4
5
6
7
8
protoc \
--proto_path=./protoc/include/google/protobuf/
--proto_path= ./proto
--go-rpc_out= . \
--go-rpc_out=module=github.com/hulb/grpc-test \
--go_out . \
--go_opt=module=github.com/hulb/grpc-test \
hello.proto

注意当使用了--proto_path后,除了指定引入的proto文件所在路径外,还需要执行要编译的proto文件的路径,即hello.proto的路径。

第二个问题说的是每个proto文件都可以定义一个package名字,而在使用引入的proto定义时要带上这个package名字。比如google的Timstamp类型定义文件如下(截取开头片段):

1
2
3
4
5
syntax = "proto3";

package google.protobuf;

...

所以我们在上面使用时应该用google.protobuf.Timestamp

至此我们完成了go开发中gRPC的简单使用。