这是我参加「第四届青训营 」笔记创作活动的第1天
鄙人最近在参加青训营的项目,要完结一个分布式存储系统,里面就用到了 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 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
然后把这两个插件的目录也丢到 PATH
里
export PATH="$PATH:$(go env GOPATH)/bin"
编写 .proto
文件
这里我就不写了,下面都拿我项目里面的代码来演示
项目地址:github.com/tiktok-dfs/… (等揭露后即可访问)
在我的这个项目里, client 会向 namenode 发送一些恳求,咱们要先界说好传递的结构体和办法
首先在文件最初先交代好语法版本、包名、生成路径,下面就写你要传递的那些类型,还要注册办法
界说类型的语法:
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 |
例如客户端要检查一个目录下的文件和其他目录,那么恳求体便是这样的
message ListReq {
string ParentPath = 1;
}
呼应体里面文件和目录分开回来,我就这样写
message ListResp {
repeated string DirName = 1;
repeated string FileName = 2;
}
最终在 service 里注册这个办法,语法如下
rpc 办法名(恳求体) returns (呼应体){}
这样一来,service 里便是这个样子
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);
}
然后经过下面的命令编译
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
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()))
}
然后调用生成的代码,传入这个衔接和恳求体,就能够拿到呼应体
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 这边就拿到恳求,然后回来就好了
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
}