gRPC框架详解

gRpc简介

  gRPC 是Google公司开发的一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。

       gRpc官网地址:https://www.grpc.io

  gRpc中文文档地址:http://doc.oschina.net/grpc

  

  gRPC是一款RPC框架,那么先了解Rpc是什么。

Rpc基本概念

   RPC(Remote Procedure Call)远程过程调用,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,简单的理解是一个节点请求另一个节点提供的服务。RPC只是一套协议,基于这套协议规范来实现的框架都可以称为 RPC 框架,比较典型的有 Dubbo、Thrift 和 gRPC。

  RPC 机制和实现过程
    RPC 是远程过程调用的方式之一,涉及调用方和被调用方两个进程的交互。因为 RPC 提供类似于本地方法调用的形式,所以对于调用方来说,调用 RPC 方法和调用本地方法并没有明显区别。

     RPC的机制的诞生和基础概念

    1984 年,Birrell 和 Nelson 在 ACM Transactions on Computer Systems 期刊上发表了名为“Implementing remote procedure calls”的论文,该文对 RPC 的机制做了经典的诠释:

    RPC 远程过程调用是指计算机 A 上的进程,调用另外一台计算机 B 上的进程的方法。其中A 上面的调用进程被挂起,而 B 上面的被调用进程开始执行对应方法,并将结果返回给 A,计算机 A 接收到返回值后,调用进程继续执行。

    发起 RPC 的进程通过参数等方式将信息传送给被调用方,然后被调用方处理结束后,再通过返回值将信息传递给调用方。这一过程对于开发人员来说是透明的,开发人员一般也无须知道双方底层是如何进行消息通信和信息传递的,这样可以让业务开发人员更专注于业务开发,而非底层细节。

    RPC 让程序之间的远程过程调用具有与本地调用类似的形式。比如说某个程序需要读取某个文件的数据,开发人员会在代码中执行 read 系统调用来获取数据。

    当 read 实际是本地调用时,read 函数由链接器从依赖库中提取出来,接着链接器会将它链接到该程序中。虽然 read 中执行了特殊的系统调用,但它本身依然是通过将参数压入堆栈的常规方式调用的,调用方并不知道 read 函数的具体实现和行为。

    当 read 实际是一个远程过程时(比如调用远程文件服务器提供的方法),调用方程序中需要引入 read 的接口定义,称为客户端存根(client-stub)。远程过程 read 的客户端存根与本地方法的 read 函数类似,都执行了本地函数调用。不同的是它底层实现上不是进行操作系统调用读取本地文件来提供数据,而是将参数打包成网络消息,并将此网络消息发送到远程服务器,交由远程服务执行对应的方法,在发送完调用请求后,客户端存根随即阻塞,直到收到服务器发回的响应消息为止。


    下图展示了远程方法调用过程中的客户端和服务端各个阶段的操作。
    

                     RPC 示意图


    当客户端发送请求的网络消息到达服务器时,服务器上的网络服务将其传递给服务器存根(server-stub)。服务器存根与客户端存根一一对应,是远程方法在服务端的体现,用来将网络请求传递来的数据转换为本地过程调用。服务器存根一般处于阻塞状态,等待消息输入。

    当服务器存根收到网络消息后,服务器将方法参数从网络消息中提取出来,然后以常规方式调用服务器上对应的实现过程。从实现过程角度看,就好像是由客户端直接调用一样,参数和返回地址都位于调用堆栈中,一切都很正常。实现过程执行完相应的操作,随后用得到的结果设置到堆栈中的返回值,并根据返回地址执行方法结束操作。以 read 为例,实现过程读取本地文件数据后,将其填充到 read 函数返回值所指向的缓冲区。

    read 过程调用完后,实现过程将控制权转移给服务器存根,它将结果(缓冲区的数据)打包为网络消息,最后通过网络响应将结果返回给客户端。网络响应发送结束后,服务器存根会再次进入阻塞状态,等待下一个输入的请求。

    客户端接收到网络消息后,客户操作系统会将该消息转发给对应的客户端存根,随后解除对客户进程的阻塞。客户端存根从阻塞状态恢复过来,将接收到的网络消息转换为调用结果,并将结果复制到客户端调用堆栈的返回结果中。当调用者在远程方法调用 read 执行完毕后重新获得控制权时,它唯一知道的是 read 返回值已经包含了所需的数据,但并不知道该 read 操作到底是在本地操作系统读取的文件数据,还是通过远程过程调用远端服务读取文件数据。

    总结下RPC执行步骤:

      1. 调用客户端句柄,执行传递参数。

      2. 调用本地系统内核发送网络消息。

      3. 消息传递到远程主机,就是被调用的服务端。

      4. 服务端句柄得到消息并解析消息。

      5. 服务端执行被调用方法,并将执行完毕的结果返回给服务器句柄。

      6. 服务器句柄返回结果,并调用远程系统内核。

      7. 消息经过网络传递给客户端。

      8. 客户端接受数据。



  RPC框架的组成

     一个完整的 RPC 框架包含了服务注册发现、负载、容错、序列化、协议编码和网络传输等组件。不同的 RPC 框架包含的组件可能会有所不同,但是一定都包含 RPC 协议相关的组件,RPC 协议包括序列化、协议编解码器和网络传输栈,如下图所示:

    

 

   RPC 协议一般分为公有协议和私有协议。例如,HTTP、SMPP、WebService 等都是公有协议。如果是某个公司或者组织内部自定义、自己使用的,没有被国际标准化组织接纳和认可的协议,往往划为私有协议,例如 Thrift 协议和蚂蚁金服的 Bolt 协议。

  分布式架构所需要的企业内部通信模块,往往采用私有协议来设计和研发。相较公有协议,私有协议虽然有很多弊端,比如在通用性上、公网传输的能力上,但是高度定制化的私有协议可以最大限度地降低成本,提升性能,提高灵活性与效率。定制私有协议,可以有效地利用协议里的各个字段,灵活满足各种通信功能需求,比如:CRC 校验、Server Fail-Fast 机制和自定义序列化器。

  在协议设计上,你还需要考虑以下三个关键问题:

    1. 协议包括的必要字段与主要业务负载字段。协议里设计的每个字段都应该被使用到,避免无效字段。
    2. 通信功能特性的支持。比如,CRC 校验、安全校验、数据压缩机制等。
    3. 协议的升级机制。毕竟是私有协议,没有长期的验证,字段新增或者修改,是有可能发生的,因此升级机制是必须考虑的。

   

  RPC和HTTP区别
    RPC 和 HTTP都是微服务间通信较为常用的方案之一,其实RPC 和 HTTP 并不完全是同一个层次的概念,它们之间还是有所区别的。
      1. RPC 是远程过程调用,其调用协议通常包括序列化协议和传输协议。序列化协议有基于纯文本的 XML 和 JSON、二进制编码的Protobuf和Hessian。传输协议是指其底层网络传输所使用的协议,比如 TCP、HTTP。
      2. 可以看出HTTP是RPC的传输协议的一个可选方案,比如说 gRPC 的网络传输协议就是 HTTP。HTTP 既可以和 RPC 一样作为服务间通信的解决方案,也可以作为 RPC 中通信层的传输协议(此时与之对比的是 TCP 协议)。

 

  常见的 PRC 框架
    目前流行的开源 RPC 框架还是比较多的,有阿里巴巴的 Dubbo、Google 的 gRPC、Facebook 的 Thrift 和 Twitter 的 Finagle 等。

    1. Go RPC:Go 语言原生支持的 RPC 远程调用机制,简单便捷。
    2. gRPC:Google 发布的开源 RPC 框架,是基于 HTTP 2.0 协议的,并支持众多常见的编程语言,它提供了强大的流式调用能力,目前已经成为最主流的 RPC 框架之一。
    3. Thrift:Facebook 的开源 RPC 框架,主要是一个跨语言的服务开发框架,作为老牌开源 RPC 协议,以其高性能和稳定性成为众多开源项目提供数据的方案选项。

 

    关于更多RPC内容可以看这篇:https://www.cs.rutgers.edu/~pxk/417/notes/03-rpc.html

 

 

Go语言RPC过程调用

  Go语言原生有RPC包,RPC过程调用实现起来非常简单。服务端只需实现对外提供的远程过程方法和结构体,然后将其注册到 RPC 服务中,客户端就可以通过其服务名称和方法名称进行 RPC 方法调用。
  包文档地址:https://studygolang.com/pkgdoc
 

    这里面有两个重要方法:

  1. 服务端

  

 

   2. 客户端调用

  

 

   编写服务端代码 server.go

package main

import (
    "fmt"
    "net/rpc"
    "io"
    "net"
    "net/http"
)

//创建一个int类型对象
type Panda int 

/**
 * argType是客户端发送过来的内容
 * replyType是服务端返回给客户端的内容
 */
func (this *Panda)GetInfo(argType int, replyType *int) error {//GetInfo首字母大小 因为要被外部访问
    fmt.Println("打印对方发送过来的数据:",argType)
    //执行
    *replyType = argType + 123456
    return nil
}

func pandatext(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "hello panda")
}

func main() {
    //客户端页面的请求
    http.HandleFunc("/panda", pandatext)
    //将类实例化为对象
    pd := new(Panda)
    //服务端注册一个对象,该对象就作为一个服务被暴露出去
    rpc.Register(pd)
    //连接到网络
    rpc.HandleHTTP()

    //监听端口
    ln,err := net.Listen("tcp", ":10086")
    if err != nil {
        fmt.Println("network error")
    }
    http.Serve(ln,nil)

}

在编写客户端代码client.go

package main

import (
    "fmt"
    "net/rpc"
)

func main() {
    //建立网络连接
    cli, err := rpc.DialHTTP("tcp", "127.0.0.1:10086")
    if err != nil {
        fmt.Println("network failed")
    }

    var pd int 
    //客户端调用服务端GetInfo方法,并传递参数
    err = cli.Call("Panda.GetInfo", 10086, &pd)
    if err != nil {
        fmt.Println("call() failed")
    }
    fmt.Println("服务端输出的值:", pd)
    

}

编写好之后先运行server.go 在运行client.go文件

就可以可以看到返回结果

 

 

gRPC特点

   在gRPC的客户端应用可以想调用本地对象一样直接调用另一台不同的机器上的服务端的应用的对象或者方法,这样在创建分布式应用的时候更容易。下面看看gRPC的特点:

    1. 语言无关,支持多种语言;

    2. 基于 IDL 文件定义服务,gRPC使用protocol buffer 作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub。

    3. 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;

    4. 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。


gRPC使用说明
 
    gRPC使用和上面RPC使用方法类似,首先定义服务,指定其能够被远程调用的方法,包括参数和返回类型,这里使用protobuf来定义服务。在服务端实现定义的服务接口,并运行一个gRPC服务器来处理客户端调用。
    

gRPC安装

使用go命令下载

go get -u google.golang.org/grpc

如果上面不行就使用git下载。

使用git下载

git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net
git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text
git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genprot

 因为gRPC要使用proto及相关依赖,安装protobuf可以看我这篇文章:https://www.cnblogs.com/songgj/p/11560565.html

 都安装好之后,可以测试gRPC是否可以正常运行。
#进入服务端 (先启动)
cd /Users/songguojun/go/src/google.golang.org/grpc/examples/helloworld/greeter_server/ #进入客户端端 (服务端启动后在启动) cd /Users/songguojun/go/src/google.golang.org/grpc/examples/helloworld/greeter_client/

 运行结果,输出hello world表面可以通信。

 gRPC案例

   下面使用gRPC来实现一个客户端和服务端的通行。

   我本地代码结构

  

   1. 先使用protobuf定义服务。

         创建myProtobuf.proto文件,编辑如下内容。

syntax = "proto3" ;

//package myproto ;      
#上面注释掉是因为报错 参考https://www.cnblogs.com/tomtellyou/p/12936651.html这篇文章第七点
#https://learnku.com/articles/43758
option go_package = ".;protoes"; //定义服务 service HelloServer { rpc SayHello (HelloReq) returns (HelloRsp){} rpc SayName (NameReq) returns (NameRsp){} } //客户端发送给服务端 message HelloReq { string name = 1 ; } //服务端返回给客户端 message HelloRsp { string msg = 1 ; } //客户端发送给服务端 message NameReq { string name = 1 ; } //服务端返回给客户端 message NameRsp { string msg = 1 ; }

 定义了两个服务SayHelloSayName及对应的四个消息(message)。

 然后在执行命令生成pd.go文件

protoc --go_out=plugins=grpc:./ *.proto     #添加grpc插件

 2. 编写服务端server.go

package main     


import (
    "fmt"
    "net"
    "google.golang.org/grpc"
    pd "demo/myproto"  //导入proto
    "context"
)

type server struct {}

func (this *server) SayHello(ctx context.Context, in *pd.HelloReq) (out *pd.HelloRsp,err error) {
    return &pd.HelloRsp{Msg:"hello"}, nil
}

func (this *server) SayName(ctx context.Context, in *pd.NameReq) (out *pd.NameRsp,err error){
    return &pd.NameRsp{Msg:in.Name + "it is name"}, nil
}

func main()  {
    ln, err := net.Listen("tcp", ":10088")
    if err != nil {
        fmt.Println("network error", err)
    }

    //创建grpc服务
    srv := grpc.NewServer()
    //注册服务
    pd.RegisterHelloServerServer(srv, &server{})
    err = srv.Serve(ln)
    if err != nil {
        fmt.Println("Serve error", err)
    }
}

3. 编写客户端client.go

package main     

import (
    "fmt"
    "google.golang.org/grpc"
    pd "demo/myproto"  //导入proto
    "context"
)

func main() {
    //客户端连接服务端
    conn, err := grpc.Dial("127.0.0.1:10088", grpc.WithInsecure())
    if err != nil {
        fmt.Println("network error", err)
    }

    //网络延迟关闭
    defer  conn.Close()
    //获得grpc句柄
    c := pd.NewHelloServerClient(conn)
    //通过句柄进行调用服务端函数SayHello
    re1, err := c.SayHello(context.Background(),&pd.HelloReq{Name:"songguojun"})
    if err != nil {
        fmt.Println("calling SayHello() error", err)
    }
    fmt.Println(re1.Msg)

    //通过句柄进行调用服务端函数SayName
    re2, err := c.SayName(context.Background(),&pd.NameReq{Name:"songguojun"})
    if err != nil {
        fmt.Println("calling SayName() error", err)
    }
    fmt.Println(re2.Msg)
}

运行结果如下

 

 

 

gRPC四种通信方式

  gRPC 允许你定义四类服务方法:

    1. 简单RPC(Simple RPC):即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

rpc SayHello(HelloRequest) returns (HelloResponse){
}

    2. 服务端流式RPC(Server-side streaming RPC):一个请求对象,服务端可以传回多个结果对象。即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}

      3. 客户端流式RPC(Client-side streaming RPC):客户端传入多个请求对象,服务端返回一个响应结果。即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

   4. 双向流式RPC(Bidirectional streaming RPC):结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象。即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

 

 

 

 

posted @ 2020-10-28 23:45  songguojun  阅读(46088)  评论(0编辑  收藏  举报