分布式系统的特点是不同的功能模块会以独立服务器程序的方式运行在不同主机上。当服务A想请求位于另一台机器的服务B完成特定请求时,就必须将要处理的数据提交给B。这个过程就涉及到一系列问题,首先A需要把数据进行序列化然后通过网络连接发送给B,B接收到数据后需要进行反序列化得到数据原型,进行相应处理得到结果,接着把结果序列化后再传递给A,A收到结果后进行反序列化,得到处理结果的数据结构。
这一系列过程涉及到数据序列化,网络连接的建立,数据的传输效率,同时还涉及到身份认证,数据加密等,这些步骤繁琐麻烦,而且跟业务逻辑无关,如果都要由开发者来处理,那么很多宝贵的时间和精力就得浪费在这些不产生效益的工作上,而且这些工作处理起来还很复杂。因此要想高效的搭建基于微服务的分布式系统,我们需要高效的工具来处理服务之间的跨进程通讯,gRPC就是满足于该目的的有效框架。
gRPC的目的就是让位于不同主机的进程在相互调用特定接口时尽可能的省却不必要的操作,让接口调用变得像处于同一进程间的模块相互调用那么简单。gRPC的运行有四种模式,第一种是客户端向服务端发出一个请求,服务端处理后给客户端返回一个结果;第二种是客户端向服务端发起一个请求,然后服务端向客户端返回一系列结果;第三种是客户端向服务端发送一系列请求,服务端返回一个结果;第4种是客户端向服务端发送一系列请求,服务端给客户端返回一系列结果,本节我们先看前两种情况。
假设我们现在要开发一个电商后台系统,系统有一个订单存储查询服务,客户端向服务发送订单ID,服务接收到ID后将相应订单的详细信息返回。如果服务端模块跟客户端模块属于同一个进程的话,那么它们之间就会存在调用关系,服务端模块会导出一个接口,该接口接收的参数就是订单ID,调用返回的结果就是订单的具体数据,由于服务端和客户端处于不同进程,甚至位于不同主机,但我们希望能实现两者之间的交互就像同一进程内不同模块之间相互调用那么简单。
为了实现gRPC,我们首先要安装相应环境,gRPC使用protobuf来作为进程之间交互数据的结构,因此需要先安装好protocolBuf,然后还需要安装gRPC组件,组件的下载可以点击这里:https://github.com/grpc/grpc-go/releases。根据不同系统下载相应可执行文件,然后将它放置到PATH环境变量对应的路径下即可。
接下来我们需要通过proto文件来定义订单存储查询服务导出的接口,以及订单信息的数据结构。先在根目录下创建一个GRPC文件夹,然后创建子目录proto用来存放proto文件,proto文件的内容如下:
syntax="proto3";
import "google/protobuf/wrappers.proto";
option go_package="service/order_management";
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns(Order);
}
message Order {
string id = 1;
repeated string items = 2;
string description = 3;
float price = 4;
string destination = 5;
}
其中getOrder就是订单存储查询模块向外导出接口的定义,它接收一个StringValue类型作为订单ID,然后以Order描述的数据结构作为订单具体信息返回,注意看这里我们使用关键字service来定义服务导出的接口,服务的名称为OrderManagement,导出接口名称为getOrder,在proto目录里面我们执行命令:
protoc -I=. --go_out=plugins=grpc:. *.proto
其中grpc:. 表示调用我们前面路径下载的应用程序编译proto文件中定义的service信息,如果没有下载前面路径所给组件的话,上面命令执行就会有问题。完成编译后,在当前路径下就会生成子目录service/order_management,下面有一个名为order_management.pb.go的文件,我们需要看一下他的内容。
这个文件里面代码很复杂,我们只需要关注几部分,使用关键字getOrder进行搜索,我们可以发现如下定义:
// OrderManagementServer is the server API for OrderManagement service.
type OrderManagementServer interface {
GetOrder(context.Context, *wrappers.StringValue) (*Order, error)
}
这是一个接口定义,它主要用于初始化服务器程序,我们的工作就是要实现接口函数GetOrder,然后将实现的实例传给服务器对象,到时候客户端请求过来时,服务器会自动调用我们实现的接口。
第二个需要关注的是:
func RegisterOrderManagementServer(s *grpc.Server, srv OrderManagementServer) {
s.RegisterService(&_OrderManagement_serviceDesc, srv)
}
我们将调用这个接口来生成一个服务器对象,它将负责建立tcp连接等工作,注意看这个函数需要传入OrderManagementServer接口,所以当我们实现了GetOrder的逻辑后,调用该函数就可以完成服务端的工作。
为了能调用order_management.pb.go的代码,我们需要进入到service/order_management目录,然后执行如下命令:
sudo go mod init order_management
接下来我们看看服务端的实现,回到GRPC根目录,新建目录server,在该目录下创建文件main.go,首先我们添加依赖包和初始化一下服务端数据:
package main
import (
"context"
"github.com/golang/protobuf/ptypes/wrappers"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "order_management"
wrapper "github.com/golang/protobuf/ptypes/wrappers"
"net"
"strings"
)
const (
port = ":50051" //服务端将在50051端口监听客户端请求
)
func initSampleData() {
//虚拟一些数据以便客户端进行请求
orderMap["102"] = pb.Order{Id: "102", Items: []string{"Google Pixel 3A", "Mac Book Pro"}, Destination: "Mountain View, CA", Price: 1800.00}
orderMap["103"] = pb.Order{Id: "103", Items: []string{"Apple Watch S4"}, Destination: "San Jose, CA", Price: 400.00}
orderMap["104"] = pb.Order{Id: "104", Items: []string{"Google Home Mini", "Google Nest Hub"}, Destination: "Mountain View, CA", Price: 400.00}
orderMap["105"] = pb.Order{Id: "105", Items: []string{"Amazon Echo"}, Destination: "San Jose, CA", Price: 30.00}
orderMap["106"] = pb.Order{Id: "106", Items: []string{"Amazon Echo", "Apple iPhone XS"}, Destination: "Mountain View, CA", Price: 300.00}
}
var orderMap = make(map[string]pb.Order)
type server struct { //server就是要实现接口getOrder的实例
orderMap map[string]*pb.Order
}
接下来一个重要的工作就是完成getOrder接口的实现:
func (s *server) GetOrder(ctx context.Context,
orderId *wrapper.StringValue) (*pb.Order, error) {
//根据客户端发来ID查找订单数据
ord, exists := orderMap[orderId.Value]
if exists {
return &ord, status.New(codes.OK, "").Err()
}
return nil, status.Errorf(codes.NotFound, "Order does not exist. : ", orderId)
}
接口的实现中有些东西需要强调,首先是codes,它用来标志接口处理结构,它跟http协议的返回状态类似,如果处理成功,那么返回标志就是codes.OK,如果有错误那么就设置相应标志,例如客户端发送的订单号没有对应数据时,错误就是codes.NotFound,这点给http的404类似。
如果有内容,那么我们就以Order数据结构的形式将数据返回,数据的序列化和发送等工作用gRPC框架来负责。
接下来我们要做的就是建立tcp连接,然后使用实现了getOrder接口的server实例来初始化服务端,代码如下:
func main() {
initSampleData()
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterOrderManagementServer(s, &server{})
if err = s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
上面代码中,我们先建立一个监听在端口50051的tcp连接,然后使用grpc.NewServer生成一个服务器对象,RegisterOrderManagmentServer将我们实现的业务逻辑注入到服务器,当客户端将订单ID传过来时,服务器对象就会调用我们实现的接口,以上就是服务端的gRPC实现。完成上面代码后,我们需要执行如下命令:
sudo go mod init server
sudo go mod tidy
go build
如果命令执行没有问题的话,在本地目录就会创建服务端的可执行文件名为server,然后我们先将服务器程序运行起来:
./server
接下来我们看看客户端的实现,回到GRPC根目录,创建目录client,在下面生成文件main.go,首先我们要做的是引入依赖项:
package main
import (
"context"
"log"
pb "order_management"
"time"
"google.golang.org/grpc"
wrapper "github.com/golang/protobuf/ptypes/wrappers"
"io"
)
const (
address = "localhost:50051" //向服务器端口发出请求
)
接下来的工作就是向服务端那样,调用gprc生成的代码创建客户端对象:
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure()) //创建与服务端的连接
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewOrderManagementClient(conn) //生成客户端对象
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5)
defer cancel()
//调用getOrder接口向服务端提供订单ID,发起服务请求
retriveOrder, err := client.GetOrder(ctx, &wrapper.StringValue{Value : "106"})
log.Print("GetOrder Response -> :", retriveOrder)
}
以上的代码就是创建客户端实例,然后调用getOrder接口向服务端发出请求。完成上面代码后,我们执行如下命令:
sudo go mod init client
sudo go mod tidy
go build
./client
客户端运行后就会向服务端发出请求,然后将返回的订单数据打印出来,客户端运行后输出结果如下:
我们可以看到,使用gRPC实现跨进程调用,在服务端需要实现定义的接口逻辑,然后就调用生成的接口创建服务器实例,将我们实现的接口传入服务器即可。客户端处理创立tcp连接,调用生成的代码获得客户端实例,接下来就可以直接调用定义的接口向服务端发起请求,gRPC框架让能让不同服务直接的调用尽可能像位于同一进程的模块直接发送调用那么简单,当然它也提供了更加复杂的调用功能,后面我们再一一解析,代码可以在该链接获取:https://github.com/wycl16514/go_distributed_system_grpc.git