gRPC介绍及grpc-go入门实战

miloyang
0 评论
/ /
653 阅读
/
8337 字
28 2023-07

RPC

什么是RCP

早在上个世纪70年代,位于加利福尼亚帕洛阿尔托研究中心的施乐公司(Xerox Corporation),就提出RPC的概念,该公司开发了许多重要的计算机科学技术,包括图形用户界面(GUI)、以太网局域网、鼠标等。在这一创新浪潮中,他们提出了远程过程调用(RPC)的概念,以促进分布式系统中的通信和协作。虽然最初的想法是在 Xerox 内部使用的,但随后 RPC 的概念被广泛采纳,并在计算机科学和软件工程中得到了广泛的应用和发展。
RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议。
用于使一个计算机程序可以调用位于另一个地址空间(通常是不同机器上)的子程序或函数,就像调用本地函数一样。它允许开发者在分布式系统中建立客户端和服务器之间的通信,使得远程的计算能够像本地调用一样简单。 过程是什么?过程就是程序和业务处理、计算任务,更为直白的说,就是程序,像调用本地方法一样调用远程的过程。 一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机的某个对象,就像调用本地应用应用程序中的对象一样。
那么,我们至少挖掘出如下几个要点:

  • RPC是协议
    既然是协议,就只有一套规范,那么就需要有人遵循这套规范来进行实现,如接下来介绍的grpc。
  • 网络协议透明
    RPC的客户端认为自己是在调用本地对象一样调用远程方法,那么传输层使用的是TCP/UDP或者是HTTP协议,又或者是其他协议,则无需关系。
  • 信息格式透明
    传统的http请求,会定义一些参数格式,返回调用结果比如xml、json等等,那么对于远程调用,这些参数会以某种信息格式传递给网络上的另一个计算机,这些信息格式具体如何构成,则无需关系。
  • 具有跨语言的能力
    如何理解?假设我们某个服务的提供方,那我们并不知道调用方实际运行的应用程序是什么语言,java/php/c++或者什么,那么对于调用方来说,只要遵循了一套约定好的规范(框架),则无论使用什么语言,本次都应该调用成功,且返回值也不应该随着语言变化而变化。
    如下图所示 RPC调用涉及到哪些流程细节(图片来源:腾讯云开发者社区)

rpc调用过程图示

1:服务消费方(client)调用(caller)以本地调用方式(rpc call)调用服务;
2:client stub接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体;
3:client stub找到服务器地址,并将消息发送到服务端;
4:server stub收到消息后进行反序列化;
5:server stub根据解码结果调用本地的服务(local call);
6:本地服务执行并将结果返回给server stub(local return);
7:server stub将结果序列化并发送至消费方(send); 8:client stub接收到消息,并进行解码(receive);
9:服务消息方,得到最终的返回结果(rpc return);
那么,RPC的框架目的,就是把2-8步骤封装起来,让用户对这些细节透明

为什么需要用到RCP

这应该是应用开发到一定的阶段的强烈需求驱动。最早期我们开发简单的单一应用,逻辑简单、用户流量不大,我们单体运行即可承受。后续随着互联网直线崛起,系统访问量增大、业务增多,发现单体服务无法承受的时候,也可以将业务拆分几个互不关联的应用,分别部署在各自机器,也是不需要RPC,因为应用之间是互不关联的。
但是,我们业务越来越多,应用越来越复杂,有些功能已经不能简单划分或者根本划分不出来。此时,可以将公共业务逻辑抽离出来,组成独立的服务应用,而原有的、新增的应用都可以与那些独立的service应用交互,以此来完成完整的业务功能,有这种思想的时候,RPC也就来了。

RPC核心概念术语

  • 客户端代理(client proxy) :在客户端中生成的代理代码,用于将本地函数调用转换为网络请求,并将请求发送到远程服务器。
  • 服务端代理(server stub) :在服务器端中生成的代理代码,用于接收来自客户端的网络请求,将请求解码为函数调用,并调用实际的函数来处理请求。
  • 调用请求(Call Request): 客户端通过客户端代理发送的请求,包含要调用的远程函数以及传递的参数。
  • 调用响应(Call Response): 服务器通过服务器代理返回的响应,包含远程函数执行后的结果或错误信息。
  • 远程对象引用(Remote Object Reference): 在分布式系统中,为了唯一标识远程服务对象,需要使用远程对象引用。客户端通过远程对象引用访问远程对象的方法。
  • 协议: 指定了客户端和服务器之间如何进行通信、交换数据,以及处理错误和异常情况的规则和约定。
  • 版本(version) : 为兼容程序协议变更,一个服务端可能支持多个版本的远程程序

gRPC

本文主要针对于grpc-go进行介绍
github地址 grpc-go
gRPC ,他的官方介绍是:是开源的远程过程调用(RPC)框架,可在任何环境运行。使用 gRPC 可以有效地连接数据中心内和跨数据中心的服务,gRPC 具有可插拔的负载均衡、追踪、健康检查和身份验证支持。它也适用于将设备、移动应用程序和浏览器连接到后端服务的分布式计算的最后一英里。它基于 HTTP/2 协议,使用 Protocol Buffers(ProtoBuf)作为默认的序列化协议,提供了快速、高效和跨平台的远程通信能力。他做的事情,就是帮我们 2-8 的步骤封装。
在gRPC,我们称调用方为client,被调用方为server,基于"服务定义"的思想,就是我们通过某种方式来描述一个服务,这种描述方式是无关语言的。在这个"服务定义"的过程中,只需要描述我们提供的服务名是什么,有哪些方法可以被调用,这些方法有什么样的入参,有什么样的回参。其余的gRPC回屏蔽底层的细节,client只需要直接调用定义好的方法,就能拿到预期的返回结果,对与server端来说,还需要实现我们定义的方法。 rpc调用过程图示
由上图所示,gRPC使用了Protocol Buffss,这是谷歌开源的一套成熟的数据结构序列化机制。可以理解为是一个工具,这个工具可以把我们定义的方法,转成特定语言的代码,也就完成了RPC具有跨语言跨平台的能力。其中protobuf是一种二进制的数据格式,需要编码和解码,数据本身不具有可读性,只有反序列化后才能真正得到可读的数据,因此,具有一下优势:

  • 序列化后体积相比json和xml较小,网络传输快。
  • 支持跨平台多语言
  • 消息格式升级和兼容以及安全性都很强
  • 序列化和反序列化速度很快

grpc安装

protobuf的下载安装
  • 直接去 github protobuf 中去下载对应环境的应用包。如我的是Windows电脑,就下载 protoc-24.2-win64.zip
  • 下载解压后,需要配置环境变量 配置完成后,在控制台执行 protoc命令,成功输出信息后则表示安装成功
C:\Users\44312>protoc --version
libprotoc 24.2
go语言下的核心库安装

上面安装的是protocol编译器,它可以生成各种不同语言的代码,因此,除了这个编译器,我们还需要配合各个语言的代码生成工具,对于golang来说,称为 protoc-gen-go。 这里需要注意的是,github.com/golang/protobuf/protoc-gen-go 和 google.golang.org/protobuf/cmd/protoc-gen-go 是不同的,前者是旧版本,后面是google自己接管的新版本,他们之间的api不兼容,也就是生成的命令以及生成的文件都不一样,目前gRPC源码中的示例文件使用的是google版本,我们建议也采取最新的方式,需要安装两个库,这两个库就是把我们定义好的协议,自动生成对应的代码,如下:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go get google.golang.org/grpc # 导入我们grpc的库

如果install成功后,会在我们gopath目录下会有两个可执行文件:
rpc调用过程图示

gRPC入门demo

github地址 grpc-study 我们来写个入门demo,思路如下:

  • 服务端提供proto文件(给谁提供方法,谁就是服务端,它就要编写proto文件),里面有一个方法名为SayHello的方法
  • 该方法入参为一个字符串,回参也是一个字符串,如客户端调用 SayHello 传miloyang,服务端接收后处理传给客户端为 hello miloyang。
    第一步,定义proto文件
// 这是说明我们使用的是proto3来实现的,之前是2,不过现在都是proto3了
syntax = "proto3";

// 这部分的内容是关于最后生成的go文件是哪个目录下,也就是哪个包下面,.表示当前目录生成
// 如.;services就表示在当前目录下的service生成go文件,包名也就是service了
option go_package = ".;service";

//这里就是定义一个服务,这个服务需要有一个方法,这个方法接收客户端的参数(HelloRequest),然后返回对应的响应(HelloResponse)。
// 显而易见,定义了一个SayHello的service,有个rpc的方法,名为SayHello,发送一个HelloRequest,返回一个HelloResponse。
// 可以类比java中 SayHello的class,然后有一个SayHello的方法,接收一个参数,返回一个对象
service SayHello{
  rpc SayHello(HelloRequest)returns (HelloResponse){}
}

// message关键字 理解为Golang的struct
// message中,每个字段都必须要有一个唯一的整型识别号,范围是[1,2^29-1](根本用不完)。
message HelloRequest{
  string requestName = 1; // 这里可不是赋值哦,只是定义这个变量在message中的index 从1开始
  // int reqNum = 2; // 表示第二个字段为int类型的
  // repeated string friends = 3 // repeated,类似定义集合。在go语言中表示定义了一个[]string的切片,在第三个位置
}

// 同理定义一个返回的结构体
message HelloResponse{
  string responseMsg = 1; // 同理返回的第一个参数为string类型
  // 结构体也可以嵌套
  //message Info{
     // string name = 1;
  //}
}
第二步,生成对应的文件

有了如上的proto文件,就可以把这个文件给调用方(客户端了),且服务端和客户端都需要通过我们刚刚安装的工具生成对应的go语言代码,不同的是服务端还需要针对SayHello方法做具体的实现。

  • 生成go语言方法,服务端和客户端都需要,cd到proto具体的文件路径。
 protoc --go_out=. --go-grpc_out=. hello.proto

rpc调用过程图示

  • 服务端实现具体的业务逻辑
func main() {
    listen, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    //创建grpc
    grpcServer := grpc.NewServer()
    //注册grpc
    pb.RegisterSayHelloServer(grpcServer, &server{}) //通过引用注册我们的对象
    //启动
    err = grpcServer.Serve(listen)
    if err != nil {
        log.Fatal(err)
    }
}

// hello server
type server struct {
    pb.UnimplementedSayHelloServer
}

// SayHello 实现具体的方法
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
    return &pb.HelloResponse{
        ResponseMsg: "hello " + req.RequestName,
    }, nil
}
  • 客户端实现通过gRPC调用服务端SayHello
func main() {
    //连接到server端
    var opts []grpc.DialOption
    opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) // 使用不加密的方式
    conn, err := grpc.Dial("127.0.0.1:8080", opts...)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
        
    client := pb.NewSayHelloClient(conn)
    resp, err := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "miloyang"}) // 远程调用,传入参数
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(resp.GetResponseMsg())
}
  • 结果,先起来服务端,在起来客户端,输出:
GoLand2023.2\tmp\GoLand\___1go_build_main_go__1_.exe
hello miloyang

后续

我们发现,在客户端或者服务端启动rpc的时候,并未做任何的安全校验,这就表示任何人都可以连接RPC的服务,这是极端的不安全的,下一章节,介绍gRPC的校验机制。

文献参考

RPC简介 https://cloud.tencent.com/developer/article/2017599
RPC发展史 https://cloud.tencent.com/developer/article/1864288

人未眠
工作数十年
脚步未曾歇,学习未曾停
乍回首
路程虽丰富,知识未记录
   借此博客,与之共进步