今天我们来了解一下 Go 语言是如何进行远程方法调用的,远程方法调用是服务间进行通信的基础方式之一,是 Go 语言实现微服务架构必须掌握的开发知识和原理。

gRPC

gRPC 是一个高性能、开源、通用的 RPC 框架,由 Google 推出,基于HTTP/2 协议标准设计开发,默认采用 Protocol Buffers 数据序列化协议,支持多种开发语言。gRPC 提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠代码的功能库。

我们来详细了解一下 gRPC 的众多特性:

  • g rpc 使用 ProtoBuf 来定义服务、接口和数据类型,ProtoBuf 是由 Google 开发的一种数据序列化协议(类似于XML、JSON和hessian)。ProtoBuf 能够将数据进行序列化,并广泛应用在数据存储和通信协议等方面。
  • gRPC 支持多种语言,并能够基于语言自动生成客户端和服务端代码。gRPC支持 C、C++、Node.js、Python、Ruby、Objective-C、PHP和C# 等语言,目前已提供了 C 语言版本的 gRPC、 Java 语言版本的grpc-java 和 Go 语言版本的 grpc-go,其他语言的版本正在积极开发中,其中,grpc-java 已经支持 Android 开发。

如上图所示为 gRPC 的调用示意图,我们可以看到,一个 C++ 语言的服务器可以通过 gRPC 分别与 Ruby 语言开发的桌面客户端和 Java 语言开发的 Android 客户端进行交互。

  • gRPC基于 HTTP/2 标准设计,所以相对于其他 RPC 框架,gRPC拥有更多强大功能,如双向流、头部压缩、多复用请求等。这些功能给移动设备带来重大益处,如节省带宽、降低 TCP 连接次数、提高CPU利用率和延长电池寿命等。同时,gRPC 还提高了云端服务和Web应用的性能。gRPC 既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现客户端和服务器端的通信和简化通信系统的构建。

gRPC 的安装

首先使用 go get 命令安装 grpc-go。

 go get -u google.golang.org/grpc
  

接着要安装插件,先使用 which protoc 命令检查是否安装了protoc;如果没有,则使用go install命令安装 proto 和 protoc-gen-go 两个库,最后可以使用 protoc 方法判断是否成功安装了。

 ----- 查看 protoc 是否安装,确保是3.0版本
$ which protoc
$ protoc --version
----- 安装插件
$ go install  github .com/golang/protobuf/proto
$ go install github.com/golang/protobuf/protoc-gen-go
----- 测试是否安装成功
$ protoc -I pb/string.proto--go_out=plugins=grpc:.pb/string.proto
  

gRPC 过程调用实践

gRPC 过程调用时,服务端和客户端需要依赖共同的 proto 文件。proto 文件可以定义远程调用的接口、方法名、参数和返回值等。通过 proto 文件可以自动生成客户端和客户端的相应 RPC 代码。借助这些代码,客户端可以十分方便地发送 RPC 请求,并且服务端也可以很简单地建立RPC服务器,处理RPC请求并且将返回值作为响应发送给客户端。

定义和编译 proto 文件

首先,我们要定义一个 proto 文件,其具体语法请查看 Protobuf3 语言指南。在该文件中,我们定义了两个参数结果,分别是 StringRequest 和 StringResponse,同时还有一个服务结构 StringService,代码如下:

 syntax = "proto3";
package pb;
service StringService{
    rpc Concat(StringRequest) returns (StringResponse) {}
    rpc  Diff (StringRequest) returns (StringResponse) {}
}
message StringRequest {
    string A = 1;
    string B = 2;
}
message StringResponse {
    string Ret = 1;
    string err = 2;
}
  

StrtingService 有两个方法,分别为 Concat 和 Diff,每个方法都有对应的输入参数和返回值,这些值也都定义在 proto 文件中。

gRPC 可以定义4种类型的服务接口,分别是一元 RPC、服务器流 RPC、客户端流式 RPC 和双向流 RPC。

(1)一元 RPC 是指客户端向服务器发送请求并获得响应,就像正常的函数调用一样。

 rpc Concat(StringRequest) returns (StringResponse) {}
  

(2)服务器流 RPC 是指客户端发送一个对象,服务器端返回一个Stream(流式消息)。

 rpc LotsOfServerStream(StringRequest) returns (stream StringResponse) {}
  

(3)客户端流式 RPC,客户端发送一个 Stream(流式消息)服务端返回一个对象。

 rpc LotsOfClientStream(stream StringRequest) returns (StringResponse) {}
  

(4)双向流 RPC,两个流独立运行,客户端和服务器可以按照它们喜欢的顺序进行读取和写入;例如,服务器可以在写入响应之前等待接收所有客户端消息,也可以交替地进行消息的读取和写入,或读取和写入的其他组合。每个流中消息的顺序被保留。类似于 WebSocket(长连接),客户端可以向服务端请求消息,服务器端也可以向客户端请求消息。

 rpc LotsOfServerAndClientStream(stream StringRequest) returns (stream StringResponse) {}
  

接下来我们使用 protoc 编译工具编译这个protoc文件,生成服务端和客户端的代码,如下:

 protoc --go_out=plugins=grpc:. pb/string.proto
  

从 proto 文件中的服务定义开始,gRPC 提供了生成客户机和服务器端代码的 protocol buffer 编译器插件。gRPC 用户通常在客户端调用这些 API,并在服务器端实现相应的 API。

在服务器端,服务器实现服务声明的方法,并运行 gRPC 服务器来处理客户端调用。gRPC 框架会接受网络传入请求,解析请求数据,执行相应服务方法和将方法结果编码成响应通过网络传递给客户端。客户端的本地定义方法,其方法名、参数和返回值与服务端定义的方法相同。客户端可以直接在本地对象上调用这些方法,将调用的参数包含在对应的 protocol buffer 消息类型中,gRPC再将请求发送到服务端,服务端解析请求。

客户端发送 RPC 请求

我们先来看客户端代码,首先调用 grpc.Dial 建立网络连接,然后使用 protoc 编译生成的 pb.NewStringServiceClient 函数创建 gRPC 客户端,然后调用客户端的 Concat 函数,进行RPC调用,代码如下所示:

 package grpc
import (
    "context"
    "fmt"
    "github.com/keets2012/Micro-Go-Pracrise/ch9-rpc/pb"
    "google.golang.org/grpc"
)
func main() {
    serviceAddress := "127.0.0.1:1234"
    conn, err := grpc.Dial(serviceAddress, grpc.WithInsecure())
    if err != nil {
        panic("connect error")
    }
    defer conn.Close()
    stringClient := pb.NewStringServiceClient(conn)
    stringReq := &pb.StringRequest{A: "A", B: "B"}
    reply, _ := stringClient.Concat(context.Background(), stringReq)
    fmt.Printf("StringService Concat : %s concat %s = %s", 
    stringReq.A, stringReq.B, reply.Ret)
}
  

服务端建立 RPC 服务

再来看看服务器端的代码,它首先需要调用 grpc.NewServer() 来建立RPC的服务端,然后将 StringService 注册到RPC服务端上,其具有的两个函数分别处理 Concat 和 Diff 请求,代码如下:

 func main() {
    flag.Parse()
    lis, err := net.Listen("tcp","127.0.0.1:1234")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    grpcServer := grpc.NewServer()
    stringService := new(string_service.StringService)
    pb.RegisterStringServiceServer(grpcServer, stringService)
    grpcServer.Serve(lis)
}
  

最后我们来看 StringService 的具体代码实现,它首先定义了StringService 结构体,然后实现了它的 Concat 方法和 Diff 方法。

 type StringService struct{}
func (s * StringService) Concat(ctx context.Context, req *pb.StringRequest) (*pb.StringResponse, error) {
 if len(req.A)+len(req.B) > StrMaxSize {
  response := pb.StringResponse{Ret: ""}
  return &response, nil
 }
 response := pb.StringResponse{Ret: req.A + req.B}
 return &response, nil
}
func (s * StringService) Diff(ctx context.Context, req *pb.StringRequest) (*pb.StringResponse, error) {
 if len(req.A) < 1 || len(req.B) < 1 {
  response := pb.StringResponse{Ret: ""}
  return &response, nil
 }
 res := ""
 if len(req.A) >= len(req.B) {
  for _, char := range req.B {
   if strings.Contains(req.A, string(char)) {
    res = res + string(char)
   }
  }
 } else {
  for _, char := range req.A {
   if strings.Contains(req.B, string(char)) {
    res = res + string(char)
   }
  }
 }
 response := pb.StringResponse{Ret: res}
 return &response, nil
}
  

如上代码所示,StringService 的 Concat 方法和 Diff 方法实现起来都很简单,Concat 方法就是将 StringRequest 中的 A 和 B 字符拼接在一起;而 Diff 方法则是通过循环遍历,将 A 和 B 字符的差异部分计算出来。

从上面的讲述可以看出,客户端发送一个请求后,必须等待服务器发回响应才能继续发送下一个请求,这种交互模式具有一定局限性,它无法更好地利用网络带宽,传递更多的请求或响应。而 gRPC 支持流式的请求响应模式来优化解决这一问题。