鄙人最近在参加青训营的项目,要完成一个分布式存储系统,里面就用到了 gRPC 框架,学习之后有所收获,所以特此记录


理论知识

什么是 RPC

要知道什么是 gRPC ,先要了解 RPC(Remote Procedure Call,远程过程调用)

什么叫做远程过程调用捏?比如说,你在写程序的时候,可以很方便地调用你本地写的函数,但是,如果你想调用其他程序的函数,那该怎么办呢?

答案是使用 RPC ,它做到这一点,即使目标函数的程序跑在地球的另一边,都没有问题

什么是 gRPC

gRPC 是一个出名的 RPC 框架,它速度很快,而且支持多种语言,它允许你可以在 Go 中调用 Java 乃至 Python 中的函数

多语言支持是怎么做到的呢?那中间必然是要借助某种通用介质,在这里就是 Protocol Buffers

什么是 Protocol Buffers

Protocol Buffers 是谷歌搞的一种数据交换格式(就类似于 JSON ,XML 之类的),常被简写成 protobuf

但是与 JSON 之类不同的是,Protocol Buffers 不是明文存储的,而是压缩打包成二进制的,这也就是 gRPC 选择 Protocol Buffers 的原因,毕竟传输起来方便

你要先通过 .proto 文件定义好你的数据结构和调用函数,然后用编译器编译出 xxxxx.pb.go 文件(里边有一堆打包和解包相关的函数方法)和 xxxxx_grpc.pb.go (里边是关于 RPC 的函数方法),之后在你的项目里调用就好了


上手实践

准备环境

根据官网上的教程,你有两件事要做:安装 Protocol Buffers 编译器 protoc 和相关的 go 插件

安装 protoc

前往 Github 页面 下载对应操作系统的版本

image-20220820231848200

解压后把 bin 目录添加到 PATH 里,保证命令行里面可以运行 protoc

image-20220820232058298

安装 go 插件

1
2
go install google.golang.org/protobuf/cmd/[email protected]
go install google.golang.org/grpc/cmd/[email protected]

然后把这两个插件的目录也丢到 PATH

1
export PATH="$PATH:$(go env GOPATH)/bin"

编写 .proto 文件

这里我就不写了,下面都拿我项目里面的代码来演示

项目地址:https://github.com/tiktok-dfs/dfs (等公开后即可访问)

在我的这个项目里, client 会向 namenode 发送一些请求,我们要先定义好传递的结构体和方法

image-20220821000121232

首先在文件开头先交代好语法版本、包名、生成路径,下面就写你要传递的那些类型,还要注册方法

定义类型的语法:

1
2
3
4
5
6
7
message 结构体名(请求体或者响应体){
string 参数1 = 1; // 后面的 = 1 这些一定要加上
bool 参数2 = 2;
int64 参数3 = 3;
// 如果是可选的,那就在前面加上 optional
// 如果是可重复的(数组切片),就在前面加上 repeated
}

而这里使用的类型,可以参考下表

.proto Type Notes C++ Type Python Type Go Type
double double float float64
float float float float32
int32 使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代 int32 int int32
uint32 使用变长编码 uint32 int/long uint32
uint64 使用变长编码 uint64 int/long uint64
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int32
sint64 使用变长编码,有符号的整型值。编码时比通常的 int64高效。 int64 int/long int64
fixed32 总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。 uint32 int uint32
fixed64 总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。 uint64 int/long uint64
sfixed32 总是4个字节 int32 int int32
sfixed32 总是4个字节 int32 int int32
sfixed64 总是8个字节 int64 int/long int64
bool bool bool bool
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。 string str/unicode string
bytes 可能包含任意顺序的字节数据。 string str []byte

例如客户端要查看一个目录下的文件和其他目录,那么请求体就是这样的

1
2
3
message ListReq {
string ParentPath = 1;
}

响应体里边文件和目录分开返回,我就这样写

1
2
3
4
message ListResp {
repeated string DirName = 1;
repeated string FileName = 2;
}

最后在 service 里注册这个方法,语法如下

1
rpc 方法名(请求体) returns (响应体){}

这样一来,service 里就是这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
service NameNodeService {
rpc GetBlockSize(GetBlockSizeRequest) returns (GetBlockSizeResponse);
rpc ReadData(ReadRequst) returns (ReadResponse);
rpc WriteData(WriteRequest) returns (WriteResponse);
rpc DeleteData(DeleteDataReq) returns (DeleteDataResp);
rpc StatData(StatDataReq) returns (StatDataResp);
rpc GetDataNodes(GetDataNodesReq) returns (GetDataNodesResp);
rpc IsDir(IsDirReq) returns (IsDirResp);
rpc Rename(RenameReq) returns (RenameResp);
rpc Mkdir(MkdirReq) returns (MkdirResp);
rpc List(ListReq) returns (ListResp);
rpc ReDirTree(ReDirTreeReq) returns (ReDirTreeResp);
}

然后通过下面的命令编译

1
2
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths
=source_relative .\proto\namenode\namenode.proto

通过编译器编译之后,你就能在生成的代码里找到这些方法

客户端发起连接与请求

在客户端,你先需要使用 grpc.Dial() 发起连接,获取一个 *grpc.ClientConn

image-20220821105041287

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
package client

import (
"go-fs/client"
"go-fs/pkg/util"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"net"
)

// ...

func ListHandler(nameNodeAddress string, parentPath string) (*client.ListResp, error) {
rpcClient, err := initializeClientUtil(nameNodeAddress)
util.Check(err)
defer rpcClient.Close()
return client.List(rpcClient, parentPath)
}

func initializeClientUtil(nameNodeAddress string) (*grpc.ClientConn, error) {
host, port, err := net.SplitHostPort(nameNodeAddress)
util.Check(err)

return grpc.Dial(host+":"+port, grpc.WithTransportCredentials(insecure.NewCredentials()))
}

然后调用生成的代码,传入这个连接和请求体,就可以拿到响应体

image-20220821105522867

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
package client

import (
dn "go-fs/proto/datanode"
namenode_pb "go-fs/proto/namenode"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
// ...
)

// ...

func List(nameNodeConn *grpc.ClientConn, parentPath string) (*ListResp, error) {
resp, err := namenode_pb.NewNameNodeServiceClient(nameNodeConn).List(context.Background(), &namenode_pb.ListReq{
ParentPath: parentPath,
})
if err != nil {
log.Println("NameNode List Error:", err)
return nil, err
}
return &ListResp{
FileName: resp.FileName,
DirName: resp.DirName,
}, nil
}

NameNode 响应请求

NameNode 这边就拿到请求,然后返回就好了

image-20220821110335050

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
48
49
50
51
package namenode

import (
dn "go-fs/proto/datanode"
namenode_pb "go-fs/proto/namenode"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
// ...
)

// ..

type Service struct {
namenode_pb.UnimplementedNameNodeServiceServer

Port uint16
BlockSize uint64
ReplicationFactor uint64
IdToDataNodes map[uint64]util.DataNodeInstance
FileNameToBlocks map[string][]string
BlockToDataNodeIds map[string][]uint64
DataNodeMessageMap map[string]DataNodeMessage
DirTree *tree.DirTree
}

func (s *Service) List(c context.Context, req *namenode_pb.ListReq) (*namenode_pb.ListResp, error) {
path := util.ModPath(req.ParentPath)
dir := s.DirTree.FindSubDir(path)
var dirNameList []string
var fileNameList []string
for _, str := range dir {
resp, err := s.IsDir(context.Background(), &namenode_pb.IsDirReq{
Filename: path + str + "/",
})
if err != nil {
log.Println("NameNode IsDir Error:", err)
return &namenode_pb.ListResp{}, err
}
if resp.Ok {
//是目录
dirNameList = append(dirNameList, str)
} else {
fileNameList = append(fileNameList, str)
}
}
return &namenode_pb.ListResp{
FileName: fileNameList,
DirName: dirNameList,
}, nil

}