这是我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的服务。
有几个细节需要注意,
- 有一个 option 字段
go_package
,这将告诉 protoc 将这个代码到golang 的时候的package name. - 引入了 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
文件即可。