这是我gRPC系列的第二篇,
上一篇是 gRPC系列:(1)Protobuf 简要介绍 | Finley’s Blog

前言

这篇文章将开发一个简单的测试项目,使用 Golang 作为服务器,
前端将使用三种不同的技术:Golang 的 cli 项目、Flutter 的 Desktop 项目、Vue (gRPC-Web)

这篇文章将在简要介绍gRPC的基础上,开发一个 Golang 的gRPC 服务器,并且使用 gRPCurl 和 Apifox 进行测试。

Proto 设计

编写 proto 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
syntax = "proto3";
package user;
option go_package = "/api";
import "google/protobuf/empty.proto";
message User {
  uint64 id = 1;
  string name = 2;
  string email = 3;
  string password = 4;
}

message RegisterRequest {
  string name = 1;
  string email = 2;
  string password = 3;
}

message RegisterResponse { string message = 1; }

message LoginRequest {
  string email = 1;
  string password = 2;
}

message LoginResponse {
  string message = 1;
  string token = 2;
}

message GetUserRequest { uint64 id = 1; }

message GetUserResponse { User user = 1; }

message GetAllUsersResponse { repeated User users = 1; }

service UserService {
  rpc Register(RegisterRequest) returns (RegisterResponse);
  rpc Login(LoginRequest) returns (LoginResponse);
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc GetAllUsers(google.protobuf.Empty) returns (GetAllUsersResponse);
}

通过上述的 proto 我们定义了一个具有4个rpc的服务。
有几个细节需要注意,

  1. 有一个 option 字段go_package,这将告诉 protoc 将这个代码到golang 的时候的package name.
  2. 引入了 google 提供的一个 empty 的 message, 需要在使用之前导入。

编译到目标语言 golang

在这之前你应该有一个golang 的项目 (go mod init [name] 之后)

通过 protoc 可以将之编译到目标语言,我们的目标语言是 golang,所以可以执行:

1
2
3
protoc --go_out=../server --go_opt=paths=import \
--go-grpc_out=../server --go-grpc_opt=paths=import \
user.proto

server是我们后端服务器的源码目录,
为什么有一个是go_out,一个是go-grpc_out呢,是因为 protobuf 和 gRPC 是两个东西,前一个只是编译 message 的部分,而后者会将 service 部分编译为 golang 的 grpc 代码。

两个opt则是指定import模式,也就是将编译后的代码作为一个 package 可以引入。

建议将上述代码写成makefile,以便后续执行。

构建 golang server

首先需要写一个 struct 来实现给出的api(否则编译失败)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type UserSeviceServer struct {
	api.UnimplementedUserServiceServer
}

func (s UserSeviceServer) Register(ctx context.Context, req *api.RegisterRequest) (*api.RegisterResponse, error) {
	return &api.RegisterResponse{
		Message: "Hello " + req.Name,
	}, nil
}

func (s UserSeviceServer) Login(ctx context.Context, req *api.LoginRequest) (*api.LoginResponse, error) {
	return &api.LoginResponse{
		Message: "Hello " + req.Email + " " + req.Password,
		Token:   "token",
	}, nil
}

func (s UserSeviceServer) GetUser(ctx context.Context, req *api.GetUserRequest) (*api.GetUserResponse, error) {
	var user *api.User = &api.User{
		Id:    req.Id,
		Name:  "User " + strconv.Itoa(int(req.Id)),
		Email: "user" + strconv.Itoa(int(req.Id)) + "@gmail.com",
	}

	return &api.GetUserResponse{
		User: user,
	}, nil
}

func (s UserSeviceServer) GetAllUsers(ctx context.Context, req *emptypb.Empty) (*api.GetAllUsersResponse, error) {
	var users []*api.User
	for i := 1; i <= 10; i++ {
		users = append(users, &api.User{
			Id:    uint64(i),
			Name:  "User " + strconv.Itoa(i),
			Email: "user" + strconv.Itoa(i) + "@gmail.com",
		})
	}
	return &api.GetAllUsersResponse{
		Users: users,
	}, nil
}

注意,此处省略了业务逻辑。

接下来写监听端口的部分代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var (
	port = flag.Int("port", 50051, "The server port")
)

func RunGRPCServer() {
	// creds, err := credentials.NewServerTLSFromFile("./cert/server.pem", "./cert/server.key")
	// if err != nil {
	// 	log.Fatalf("could not load TLS keys: %s", err)
	// }

	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	defer lis.Close()
	s := grpc.NewServer(
		// grpc.Creds(creds),
		grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
			grpc_recovery.UnaryServerInterceptor(),
			grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
		)),
	)

	api.RegisterUserServiceServer(s, &UserSeviceServer{})
	reflection.Register(s)

	log.Printf("gRPC server listening at %v", lis.Addr())
	go func() {
		if err := s.Serve(lis); err != nil {
			log.Fatalf("failed to serve: %v", err)
		}
	}()
	defer s.GracefulStop()

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt, os.Kill)
	<-ch
	log.Println("Stopping the server")
	s.Stop()
	lis.Close()
	log.Println("Server stopped")
}

func main() {
	RunGRPCServer()
}

注意,被注释的部分是开启 gRPCs 的,需要提供两个证书文件,
可以参考
TLS SSL HTTPS SSH GPG 这些都是什么鬼? | Finley’s Blog

上述代码中:

1
2
3
4
5
6
7
s := grpc.NewServer(
		// grpc.Creds(creds),
		grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
			grpc_recovery.UnaryServerInterceptor(),
			grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
		)),
	)

这一部分加入了两个中间件,一个是recovery(错误后不kill),一个是ctxtags模式的日志输出。

reflection.Register(s) 则是启用了grpc-reflection,这将允许请求这个grpc server的所有api(用于 gRPCurl )

代码调试

运行代码

1
GRPC_GO_LOG_VERBOSITY_LEVEL=99 GRPC_GO_LOG_SEVERITY_LEVEL=info go run main.go

这里临时提供了两个环境变量,设置了日志输出模式,以查看调试信息。

通过 gRPCurl 测试

1
grpcurl --plaintext localhost:50051 describe

这将打印该 server 中的 gRPC 服务

例如本例的输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
grpc.reflection.v1.ServerReflection is a service:
service ServerReflection {
  rpc ServerReflectionInfo ( stream .grpc.reflection.v1.ServerReflectionRequest ) returns ( stream .grpc.reflection.v1.ServerReflectionResponse );
}
grpc.reflection.v1alpha.ServerReflection is a service:
service ServerReflection {
  rpc ServerReflectionInfo ( stream .grpc.reflection.v1alpha.ServerReflectionRequest ) returns ( stream .grpc.reflection.v1alpha.ServerReflectionResponse );
}
user.UserService is a service:
service UserService {
  rpc GetAllUsers ( .google.protobuf.Empty ) returns ( .user.GetAllUsersResponse );
  rpc GetUser ( .user.GetUserRequest ) returns ( .user.GetUserResponse );
  rpc Login ( .user.LoginRequest ) returns ( .user.LoginResponse );
  rpc Register ( .user.RegisterRequest ) returns ( .user.RegisterResponse );
}

带数据测试

1
grpcurl -d '{"email":"test@example.com", "password":"test"}' --plaintext localhost:50051 user.UserService/Login

将输出:

1
2
3
4
{
  "message": "Hello test@example.com test",
  "token": "token"
}

使用 Apifox 进行测试

使用 Apifox 进行测试更为简单,只需要导入.proto 文件即可。

Apifox