go GRPC

go GRPC

下载依赖

protobuf

protobuf 是核心可执行文件

  1. 安装protobuf,这里使用scoop安装: scoop install protobuf
  2. 确认安装: 执行 protoc --version

语言相关

要根据不同的语言安装对应的插件, 以生成对应的代码.

其中: protoc-gen-go 用于生成用于生成消息结构体, 生成的代码通常放在 xxx.pb.go 文件中. ``protoc-gen-go-grpc用于生成服务接口和客户端, 生成的代码通常放在xxx_grpc.pb.go` 文件中

  1. 项目目录下安装库 go get google.golang.org/grpc
  2. 安装插件 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 以及 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest, 这是下载到 $GOPATH/bin 下的全局命令

proto文件的编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 表明生成的go文件 "目录 ; 包名"
option go_package = ".;server";

// 定义服务, 可以接收参数,也可以返回字段
service SayHello {
// 定义一个RPC方法
rpc Hello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
string name = 1; // 请求消息包含一个字符串字段 name
// int age = 2; // 根据编号确定字段
}

message HelloResponse {
string message = 1; // 响应消息包含一个字符串字段 message
}

.proto 文件目录下执行

1
2
protoc --go_out=. hello.proto
protoc --go-groc_out=. hello.proto

proto文件介绍

  • message

    消息就是需要传输的数据格式的定义, 类似于结构体, 每个字段都有一个名字和类型

  • 字段规则

    required: 必填,不设置会导致异常,protobuf2中使用,protobuf3中删去

    optional: 可选,protobuf3中没有required和optional, 默认都是optional

    repeated: 可重复字段, 重复的值的顺序会被保留, 在go中重复的会被定义为切片

  • 消息号

    在消息体定义中,每个字段都必须有一个唯一的标识号,从 $[1, 2^{29}-1]$ 范围内的一个整数

  • 嵌套消息

    可以在其他消息中定义,使用消息类型

    1
    2
    3
    4
    5
    6
    7
    8
    message PersonInfo {
    message Person {
    string name = 1;
    int32 height = 2;
    repeated int32 weight = 3;
    }
    repeated Person info = 1;
    }

    或者将 Person 定义在外部, 直接使用即可

    1
    2
    3
    4
    5
    6
    7
    8
    message Person {
    string name = 1;
    int32 height = 2;
    repeated int32 weight = 3;
    }
    message PersonInfo {
    repeated Person info = 1;
    }
  • service

    如果要将消息类型用在RPC系统中, 可以在 .proto 文件中定义一个RPC服务接口, protobuf buffer 编译器将会根据所选的不同语言生成服务接口的代码及存根

    1
    2
    3
    4
    service SearchService{
    # rpc 服务函数名(参数) 返回 (返回参数)
    rpc Search(SearchRequest) returns (SearchResponse)
    }

服务端编写

  1. 创建gRPC Server对象
  2. 将server(其包含需要被调用的服务端接口)注册到gRPC Server的内部注册中心. 这样可以在接收到请求时, 通过内部的服务发现, 发现该服务端接口并转接进行逻辑处理
  3. 创建 Listen, 监听 TCP 端口
  4. gRPC Server 开始 lis.Accept, 直到 Stop

客户端编写

  1. 创建与给定目标(服务端)的连接交互
  2. 创建server的客户端对象
  3. 发送 RPC 请求, 等待同步响应, 得到回调后的响应结果
  4. 输出响应结果

证书安装和使用

证书安装参考mk文档

服务端配置:

1
2
3
4
5
6
7
8
// 使用证书,第一个是证书路径,第二个是私钥路径
creds, err := credentials.NewServerTLSFromFile("C:\\Users\\king\\Documents\\localhost.pem", "C:\\Users\\king\\Documents\\localhost-key.pem")
if err != nil {
fmt.Println("failed to load credentials:", err)
return
}
···
grpcServer := grpc.NewServer(grpc.Creds(creds)) // 使用TLS

客户端配置:

1
2
3
4
5
6
7
8
// 带上TLS连接, 第一个是证书路径,第二个是服务器名称(避免服务器名称不匹配报错)
creds, err := credentials.NewClientTLSFromFile("C:\\Users\\king\\Documents\\localhost.pem", "localhost")
if err != nil {
fmt.Println("failed to load credentials:", err)
return
}
···
conn, err := grpc.NewClient("localhost:9090", grpc.WithTransportCredentials(creds)) // 使用TLS

Token

客户端定义token

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
// 实现PerRPCCredentials接口
//type PerRPCCredentials interface {
// GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// RequireTransportSecurity() bool
//}

type ClientTokenAuth struct {
}

func (c ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
// 注意这里的字段id等就算为ID,传到server也会自动转小写
"id": "APPID",
"key": "1231231",
}, nil
}
func (c ClientTokenAuth) RequireTransportSecurity() bool {
return true // 如果是true, 则必须使用TLS连接,也就是基于TLS再进行token验证
}

···
// 可以使用一组认证方式
var opts []grpc.DialOption
opts = append(opts, grpc.WithTransportCredentials(creds)) // 使用TLS
opts = append(opts, grpc.WithPerRPCCredentials(ClientTokenAuth{})) // 使用自定义的认证

服务端获取token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这是在对应的远程调用函数中实现
// 从上下文中获取认证信息
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("missing metadata")
}
var appId, appKey string
if val, exists := md["id"]; exists {
appId = val[0]
}
if val, exists := md["key"]; exists {
appKey = val[0]
}
// 验证
if appId != "APPID" || appKey != "123123" {
return nil, errors.New("token auth failed")
}