这是我参加「第四届青训营 」笔记创作活动的第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 页面 下载对应操作系统的版本

『Go』gRPC + Protocol Buffers 简易上手指南 | 青训营笔记

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

『Go』gRPC + Protocol Buffers 简易上手指南 | 青训营笔记

装置 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 发送一些恳求,咱们要先界说好传递的结构体和办法

『Go』gRPC + Protocol Buffers 简易上手指南 | 青训营笔记

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

界说类型的语法:

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

『Go』gRPC + Protocol Buffers 简易上手指南 | 青训营笔记

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()))
}

然后调用生成的代码,传入这个衔接和恳求体,就能够拿到呼应体

『Go』gRPC + Protocol Buffers 简易上手指南 | 青训营笔记

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 这边就拿到恳求,然后回来就好了

『Go』gRPC + Protocol Buffers 简易上手指南 | 青训营笔记

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
}