鄙人最近在参加青训营的项目,要完成一个分布式存储系统,里面就用到了 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 页面 下载对应操作系统的版本
解压后把 bin
目录添加到 PATH
里,保证命令行里面可以运行 protoc
安装 go 插件
然后把这两个插件的目录也丢到 PATH
里
1
| export PATH="$PATH:$(go env GOPATH)/bin"
|
编写 .proto
文件
这里我就不写了,下面都拿我项目里面的代码来演示
项目地址:https://github.com/tiktok-dfs/dfs (等公开后即可访问)
在我的这个项目里, client 会向 namenode 发送一些请求,我们要先定义好传递的结构体和方法
首先在文件开头先交代好语法版本、包名、生成路径,下面就写你要传递的那些类型,还要注册方法
定义类型的语法:
1 2 3 4 5 6 7
| message 结构体名(请求体或者响应体){ string 参数1 = 1; bool 参数2 = 2; int64 参数3 = 3; }
|
而这里使用的类型,可以参考下表
.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
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())) }
|
然后调用生成的代码,传入这个连接和请求体,就可以拿到响应体
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 这边就拿到请求,然后返回就好了
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
}
|