gRPC

RPC算是近些年比较火的概念,随着微服务架构的兴起,RPC的应用越来越广泛。本文介绍了RPC和gRPC的相关概念,并通过详细的代码示例介绍了gRPC的基本使用。

1. RPC

在分布式计算,远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

1.1 RPC入门

首先我们编写一个RPC版本的HelloWorld

Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。
我们先构造一个HelloService类型,其中的Hello方法用于实现打印功能:

type HelloService struct {}

func (p *HelloService) Hello(request string, reply *string) error {
    *reply = "hello:" + request
    return nil
}

其中Hello方法必须满足Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。

然后就可以将HelloService类型的对象注册为一个RPC服务:

func main() {
    rpc.RegisterName("HelloService", new(HelloService))
    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }
    conn, err := listener.Accept()
    if err != nil {
        log.Fatal("Accept error:", err)
    }
    rpc.ServeConn(conn)
}

其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。

下面是客户端请求HelloService服务的代码:
新建client.go文件

func main() {
    client, err := rpc.Dial("tcp", "localhost:1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    var reply string
    err = client.Call("HelloService.Hello", "hello", &reply)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(reply)
}

首选是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定义RPC方法的两个参数。

运行:
分别运行:go run server.go go run client.go
输出:hello:hello

2. gRPC

2.1 gRPC简介

gRPC是一种现代化开源的高性能RPC框架,能够运行于任意环境之中。最初由谷歌进行开发。它使用HTTP/2作为传输协议。

在gRPC里,客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法,帮助你更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。在服务端程序中实现这个接口并且运行gRPC服务处理客户端调用。在客户端,有一个stub提供和服务端相同的方法。

image20210727092041052.png

# 沟通
	程序可以用不同的语言来编写,后端可以用go,也可以用java,在微服务架构盛行的今天,即使是后端我们也可能使用不同语言来编写程序。根据业务和技术的限制,为了相互交流,他们必须有一套API达成协议。例如沟通渠道,验证机制,有效负载,验证模型以及如何处理错误。
	微服务之间交换消息数量巨大,所以沟通越快越好,在某些带宽受限制的地方,拥有更轻量级的通信协议更为重要。

image-20210727092041052

    1、客户端(gRPC Sub)调用 A 方法,发起 RPC 调用

    2、对请求信息使用 Protobuf 进行对象序列化压缩(IDL)

    3、服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回

    4、对响应结果使用 Protobuf 进行对象序列化压缩(IDL)

    5、客户端接受到服务端响应,解码请求体。回调被调用的 A 方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果

为什么要使用gRPC

使用gRPC, 我们可以一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端,反过来,它们可以应用在各种场景中,从Google的服务器到你自己的平板电脑—— gRPC帮你解决了不同语言及环境间通信的复杂性。使用protocol buffers还能获得其他好处,包括高效的序列号,简单的IDL以及容易进行接口更新。总之一句话,使用gRPC能让我们更容易编写跨语言的分布式代码。

2.2 proto buffer

proto buffers 是什么?

proto buffer 称为协议缓冲区,用于定义结构化数据,并使用编译器,将数据生成指定语言的接口方法,其中包括客户端和服务器代码,还有其他序列化代码;

# 优势:
	- 容易阅读理解
	- 语言可以互通,自动生成多种语言代码
	- 以二进制格式表示数据,使其占用内存更小,传输速度更快,提供了客户端与服务器之间强类型的API合同。
	- 具有API演化的各种规则,保证了向前和向后的兼容性

语法:

syntax = "proto3";

service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse);
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
    ...
}

1、第一行(非空的非注释行)声明使用 proto3 语法。如果不声明,将默认使用 proto2 语法。同时我建议用 v2 还是 v3,都应当声明其使用的版本

2、定义 SearchService RPC 服务,其包含 RPC 方法 Search,入参为 SearchRequest 消息,出参为 SearchResponse 消息

3、定义 SearchRequest、SearchResponse 消息,前者定义了三个字段,每一个字段包含三个属性:类型、字段名称、字段编号

4、Protobuf 编译器会根据选择的语言不同,生成相应语言的 Service Interface Code 和 Stubs

相较 Protobuf,为什么不使用 XML?

  • 更简单
  • 数据描述文件只需原来的 1/10 至 1/3
  • 解析速度是原来的 20 倍至 100 倍
  • 减少了二义性
  • 生成了更易使用的数据访问类
2.2.1 protoc安装

release 版本下载地址

https://github.com/protocolbuffers/protobuf/releases

建议使用百度网盘下载

查看是否安装成功

$ protoc --version
libprotoc 3.6.0

除了安装 protoc 之外还需要安装各个语言对应的编译插件,我用的 Go 语言,所以还需要安装一个 Go 语言的编译插件

go get google.golang.org/protobuf/cmd/protoc-gen-go
2.2.1 protoc使用

创建.proto文件

//声明proto的版本 只有 proto3 才支持 gRPC
syntax = "proto3";
// 将编译后文件输出在 github.com/lixd/grpc-go-example/helloworld/helloworld 目录
option go_package = "/helloworld";
// 指定当前proto文件属于helloworld包
package helloworld;

// 定义一个名叫 greeting 的服务
service Greeter {
  // 该服务包含一个 SayHello 方法 HelloRequest、HelloReply分别为该方法的输入与输出
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 具体的参数定义
message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

protoc编译

# Syntax: protoc [OPTION] PROTO_FILES
$ protoc --proto_path=IMPORT_PATH  --go_out=OUT_DIR  --go_opt=paths=source_relative path/to/file.proto

这里简单介绍一下 golang 的编译姿势:

  • –proto_path或者-I

    :指定 import 路径,可以指定多个参数,编译时按顺序查找,不指定时默认查找当前目录。

    • .proto 文件中也可以引入其他 .proto 文件,这里主要用于指定被引入文件的位置
  • –go_out

    :golang编译支持,指定输出文件路径

    • 其他语言则替换即可,比如 --java_out 等等
  • –go_opt:指定参数,比如--go_opt=paths=source_relative就是表明生成文件输出使用相对路径。

  • path/to/file.proto :被编译的 .proto 文件放在最后面

$ protoc --go_out=. hello_word.proto

编译后会生成一个hello_word.pb.go文件。

到此为止就ok了。

2.3 gRPC使用

然后是安装 gRPC 。

$ go get -u google.golang.org/grpc

gRPC plugins

接着是下载 gRPC Plugins,用于生成 gRPC 相关源代码。

$ go get google.golang.org/grpc/cmd/protoc-gen-go-grpc

使用 protoc 编译生成对应源文件,具体命令如下:

protoc --go_out=. --go_opt=paths=source_relative ./hello_world.proto
protoc --go-grpc_out=. --go-grpc_opt=paths=source_relative  ./hello_world.proto

image20210727141151492.png

编写服务端

package main

import (
   pb "awesomeProject1/helloworld/helloworld"
   "context"
   "google.golang.org/grpc"
   "log"
   "net"
)

const port = ":50051"
//greeterServer定义一个结构体用于实现.proto文件中定义的方法
//新版gRPC要求必须嵌入pb.UnimplementedGreetingServer结构体
type greeterServer struct {
   pb.UnimplementedGreetingServer
}

//SayHello 简单实现了.proto中定义的SayHello方法
func (g *greeterServer) SayHello (ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error)  {
   log.Printf("Received: %v", in.GetName())
   return &pb.HelloReply{Message: "Hello"+in.GetName()},nil
}

func main()  {
   listen, err := net.Listen("tcp", port)
   if err != nil {
      log.Fatalf("failed to listen: %v", err)
   }

   server:= grpc.NewServer()

   //将服务描述(server)及其具体实现(greeterServer)注册到gRPC中去。
   //内部使用的是一个map数据结构,类似于Http server
   pb.RegisterGreetingServer(server,&greeterServer{})
   if err := server.Serve(listen); err != nil {
      log.Fatalf("failed to serve: %v", err)
   }
}

具体步骤如下:

  • 1)定义一个结构体,必须包含pb.UnimplementedGreeterServer 对象;
  • 2)实现 .proto文件中定义的API;
  • 3)将服务描述及其具体实现注册到 gRPC 中;
  • 4)启动服务。

编写客户端

package main

import (
   pb "awesomeProject1/helloworld/helloworld"
   "context"
   "google.golang.org/grpc"
   "log"
   "os"
   "time"
)

const address = "localhost:50051"
const defaultName = "world"

func main() {
   conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
   if err != nil {
      log.Fatalf("did not connect: %v", err)
   }
   defer conn.Close()
   client := pb.NewGreetingClient(conn)

   name := defaultName
   //通过命令行参数指定name
   if len(os.Args) > 1 {
      name = os.Args[1]
   }
   ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second)
   defer cancelFunc()
   r, err := client.SayHello(ctx, &pb.HelloRequest{Name: name})
   if err != nil {
      log.Fatalf("could not greet: %v", err)
   }
   log.Printf("Greeting: %s", r.GetMessage())
}

具体步骤如下:

  • 1)首先使用 grpc.Dial() 与 gRPC 服务器建立连接;
  • 2)使用pb.NewGreeterClient(conn)获取客户端;
  • 3)通过客户端调用ServiceAPI方法client.SayHello

这边我们可以使用flag工具包对其进行优化

package main

import (
   pb "awesomeProject1/helloworld/helloworld"
   "context"
   "flag"
   "google.golang.org/grpc"
   "log"
   "time"
)

const address = "localhost:50051"
 var defaultName = flag.String("default", "world", "UserName")

func main() {
   conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
   if err != nil {
      log.Fatalf("did not connect: %v", err)
   }
   defer conn.Close()
   client := pb.NewGreetingClient(conn)

   flag.Parse()
   name := *defaultName
   //通过命令行参数指定name

   ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second)
   defer cancelFunc()
   r, err := client.SayHello(ctx, &pb.HelloRequest{Name: name})
   if err != nil {
      log.Fatalf("could not greet: %v", err)
   }
   log.Printf("Greeting: %s", r.GetMessage())
}

参考文章 https://www.lixueduan.com/post/grpc/03-stream

Q.E.D.


勤俭节约,艰苦奋斗。