Golang gRPC快速入门
本文参照如下官网链接:
gRPC简介:https://grpc.io/docs/what-is-grpc/introduction/
proto语法:https://developers.google.com/protocol-buffers/docs/proto3
如何使用proroc工具和相关插件将proto文件编译为go代码? Go Generated Code
grpc helloword示例:https://github.com/grpc/grpc-go/tree/master/examples/helloworld
一、概念
1. gRPC
gRPC是Google开源的一个RPC框架,同大多数RPC系统一样,gRPC也是通过service,method等概念来定义一个可被远程调用的服务,在服务端我们会启动一个gRPC server来处理客户端请求(以预定义好的格式),在客户端我们直接调用相应method即可访问对应客户端的服务来获取数据。
这其实与http使用json格式来获取数据相似,新手可先照此理解。
目前grpc支持的语言有:C++,C#,Java,Kotlin,PHP,Python,Go等等。同普通的http接口一样,grpc的客户端和服务端可以使用不同的编程语言实现,gRPC有统一的proto文件来约束格式及类型,交互起来更加规范一些。
2. Protobuf
Protocol Buffers(Protobuf)是Google开源的一种组织结构化数据的结构,gRPC默认使用protobuf来组织,protobuf跨平台、跨语言、且向前后兼容,其物理存在是以.proto后缀的文件。
简单理解,把gRPC看做数据交互协议,把proto看做是gRPC的配置文件。
二、.proto文件编写
我们需要先定好义一个.proto文件,然后才能据此生成对应的golang代码。
message: user.proto
// proto的注释使用C风格的//和/*...*/,同Go. // 协议版本,必须是proto文件中的首行(注释和空行不算),如不写默认使用proto2版本 // 由于proto3中删减了一些字段修饰词,因此看起来更简洁些,本文不讲proto2 syntax = "proto3"; // 冒号左侧表示protoc生成的.go文件相对于指定的--go_out的位置(go_out我一般指定./user),冒号右侧表示生成的go文件包名 // 我这边执行protoc都是在windows开发环境,osx和linux未实测,灵活变通即可,有时候可能不需要添加option go_package option go_package = "./;user"; // 这里你还可以import其他自定义的proto文件,就像上边import官方proto文件一样 import "google/protobuf/timestamp.proto"; // 消息结构体 message User { string name = 1; int32 id = 2; string email = 3; google.protobuf.Timestamp last_updated = 4; }
一些解释:
- message: 结构定义的key word,后接结构体名,本例即定义里一个User的消息结构
- repeated: Field Rules即字段规则or字段修饰词,表示该字段可以出现多次,用于表达变长数组,如java.arraylist或go.slice, python.list,其他修饰词还有例如enum,reserved等。
- string, int32等都是proto的数据类型, 常用proto基本数据类型及各语言对应关系参考:proto标量数据类型列表,除基本数据类型外,还可以是enum或其他内置的message等自定义类型,如官网示例,这里不再详述。
- name,age,email,last_updated 自定义的字段名
- 1, 2, 3是字段唯一识别码,一经设定不可修改,取值范围为1~(2^29-1),但是不能使用19000到19999之间的数字,其实一般100以内就够了,很少有超过100个字段的结构体。此外1~15的数字使用1字节编码,大于15使用2字节编码,显然使用1-15之间的字段较高效。
通用格式表达为:[Field Rules] Type FieldName = 唯一不可变字段识别码
service: user.proto
message用于定义消息结构体,那么我们还需要使用service来定义如何操作结构体(或者叫如何交互数据)。如下例我们定义一个service: 传入一个User对象,得到一个User对象的详细内容。
// 在之前的user.proto文件中添加如下部分: message Users { repeated User users = 1; } service UserService { rpc getUsersInfo (User) returns (Users) { }
显然,为了遵守gRPC的规则,这里只能传入一个User,或者再额外定义一个只包含ID字段的UserID结构体,但这并无必要,因为这个User不必指定所有字段的值,我们可以只传入User的ID字段来查询,但要注意此时其他字段会取默认值,需注意默认值可能带来业务上的歧义。
这里的getUsersInfo我们先将其想象为传入一个Name,然后得到一些同名的Users,我们额外定义一个Users对象来存储结果,这个结果显然需要是一个变长数组,因此加上repeated前缀。
三、自动生成go代码
为了将定义的user.proto文件转化(学名叫编译)为go代码,我们需要下载protoc工具及其go插件。
- 至 https://github.com/protocolbuffers/protobuf/releases 下载对应的protoc文件包。
- 解压后将protoc可执行文件的路径加入操作系统PATH目录中。
- 执行
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
以及go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
安装代码生成插件。 - 将上述2个插件的地址也加入PATH,确保protoc可以找到:
$ export PATH="$PATH:$(go env GOPATH)/bin"
,如果windows的go安装时就用的默认位置那一般不用加,因为GOPATH会被自动加入系统PATH。 - 在编写好的.proto文件目录里,创建user文件夹,然后执行
protoc --go_out=./user user.proto
,这会在user目录下生成一个user.pb.go文件,里边包含user定义。 - 在编写好的.proto文件目录里,执行
protoc --go-grpc_out=./user user.proto
,这会在user目录下生成一个user_grpc.pb文件,里边包含client和server需要的相关代码。 - 再创建client和server目录用于存放后边的示例代码,此时目录结构如下:
注意:
无论是客户端还是服务端,都需要完整的这两个.go文件(虽然各自只会用到其中一部分对象),这两个生成的文件不要去edit,业务代码应在外部编写。
如果server端是已经存在的服务,那我们只需要从server项目的源代码拷贝proto文件,然后在客户端使用你自己的编程语言的相关grpc插件生成对应代码即可。
四、解析生成的go代码
user.pb.go文件没什么可说的,里边就是User和Users对象,大致内容如下:
type User struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` LastUpdated *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"` } ... type Users struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` }
再看user_grpc.pb.go文件,其中最重要的就是UserServiceClient和UserServiceServer这两个接口以及一个服务注册函数。
type UserServiceClient interface { GetUserByID(ctx context.Context, in *User, opts ...grpc.CallOption) (*User, error) } type userServiceClient struct { cc grpc.ClientConnInterface } ... type UserServiceServer interface { GetUsersInfo(context.Context, *User) (*User, error) mustEmbedUnimplementedUserServiceServer() } ... func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { s.RegisterService(&UserService_ServiceDesc, srv) }
最下面这个函数是将UserServiceServer注册到gRPC中,我们会在服务端调用。
五、业务代码编写
上述gRPC和protobuf相关的工作基本上不需要写代码,只需要编写proto文件然后通过相关插件生成对应语言的代码。
接下来我们看看如何通过golang使用上边生成的各种东西。
服务端: server.go
首先参照官方的helloword示例编写server.go
package main import ( "context" "flag" "fmt" "google.golang.org/grpc" "log" "net" pb "test/protos/user" ) var ( port = flag.Int("port", 10086, "Server Port") ) // 封装pb.UnimplementedUserServiceServer type server struct { pb.UnimplementedUserServiceServer } // 实现GetUsersInfo方法 func (s *server) GetUsersInfo(ctx context.Context, in *pb.User) (*pb.Users, error) { allUsers := make([]*pb.User, 0) allUsers = append(allUsers, &pb.User{ Name: "zhangshan", Id: 1, Email: "zhangshan1@qq.com", LastUpdated: nil, }) allUsers = append(allUsers, &pb.User{ Name: "zhangshan", Id: 2, Email: "zhangshan2@qq.com", LastUpdated: nil, }) allUsers = append(allUsers, &pb.User{ Name: "lisi", Id: 3, Email: "lisi1@qq.com", LastUpdated: nil, }) allUsers = append(allUsers, &pb.User{ Name: "lisi", Id: 4, Email: "lisi2@qq.com", LastUpdated: nil, }) queryUsers := new(pb.Users) log.Printf("Received User Name: %v", in.Name) if in.GetName() == "" { return nil, fmt.Errorf("empty user name") } for _, u := range allUsers { if u.Name == in.Name { queryUsers.Users = append(queryUsers.Users, u) } } return queryUsers, nil } func main() { flag.Parse() // 创建监听在10086端口的Listener lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } // 启动grpc server并注册我们的proto中的service s := grpc.NewServer() pb.RegisterUserServiceServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) // 启动服务,对外提供proto中定义的service if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
客户端: client.go
package main import ( "context" "flag" "log" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "test/protos/user" ) // 定义要连接的server地址 和 命令行中指定要查询的User name var ( addr = flag.String("addr", "localhost:10086", "the grpc address connect to") name = flag.String("name", "", "User name") ) func main() { flag.Parse() // Set up a connection to the server. conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect, err: %v", err) } defer conn.Close() c := pb.NewUserServiceClient(conn) // Contact the server and invoke it's service method. ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 使用传入的name构造User消息结构体 in := &pb.User{Name: *name} us, err := c.GetUsersInfo(ctx, in) if err != nil || len(us.Users) == 0 { log.Fatalf("could not get users by name [%s], err: %v", *name, err) } // 遍历得到的Users for _, u := range us.Users { log.Printf("User Info: %v", u) } }
执行过程:
开两个cmd终端:一个启动server,一个执行client访问,这样就模拟了客户端远程访问server端的流程。
D:\GoLand\workspace\test>tree /F protos 卷 NewDisk 的文件夹 PATH 列表 卷序列号为 xxx D:\GOLAND\WORKSPACE\TEST\PROTOS │ user.proto │ ├─client │ client.go │ ├─server │ server.go │ └─user user.pb.go user_grpc.pb.go
服务端启动:
客户端访问: