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插件。

  1. 至 https://github.com/protocolbuffers/protobuf/releases 下载对应的protoc文件包。
  2. 解压后将protoc可执行文件的路径加入操作系统PATH目录中。
  3. 执行go install google.golang.org/protobuf/cmd/protoc-gen-go@latest以及go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest安装代码生成插件。
  4. 将上述2个插件的地址也加入PATH,确保protoc可以找到:$ export PATH="$PATH:$(go env GOPATH)/bin",如果windows的go安装时就用的默认位置那一般不用加,因为GOPATH会被自动加入系统PATH。
  5. 在编写好的.proto文件目录里,创建user文件夹,然后执行protoc --go_out=./user user.proto,这会在user目录下生成一个user.pb.go文件,里边包含user定义。
  6. 在编写好的.proto文件目录里,执行protoc --go-grpc_out=./user user.proto,这会在user目录下生成一个user_grpc.pb文件,里边包含client和server需要的相关代码。
  7. 再创建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

服务端启动:

客户端访问:

posted @ 2022-09-14 14:38  realcp1018  阅读(339)  评论(0编辑  收藏  举报