Reference document:

什么是RPC?

RPC,全称为Remote Procedure Call,即远程过程调用,是一种计算机程序的通信协议,常见于分布式计算、后端服务集群部署等场景。遵循C/S模式,经典实现是一个通过发送请求<->接受回应进行信息交互的系统。随着微服务架构在业界的大规模应用,RPC逐渐被大众所熟知(有关RPC的想法至少可以追溯到1976年)。

常见的RPC协议:

  • gRPC,由Google推出,基于HTTP2协议开发,默认采用Protocol Buffers数据序列化协议,支持C#、C++、Dart、GoLang、Java、Kotlin、Node、Objective-C、PHP、Python、Ruby等一系列语言。
  • Thrift,由Facebook推出,后捐献给Apache基金会,支持C#、C++、Delphi、ErLang、Haxe、GoLang、Java、Lua、Node、Perl、PHP、Python、Ruby等一些列语言。
  • bRPC,由Baidu推出,后捐献给Apache基金会,目前仅支持C++。
  • Dubbo,由Alibaba推出,后捐献给Apache基金会,目前仅支持Java。严格意义来讲Dubbo不属于RPC协议,而是一款RPC框架。
  • 自定义协议,部分大的公司会选择自行开发一套协议来使用,随着上述协议的不断优化和完善,自定义协议出现的也越来越少。

为什么要使用RPC?

随着互联网的普及,大规模的应用越来越多,大企业对于开发效率和软件质量的需求不断的上升,从而衍生出了越来越多的微服务需求。微服务,简而言之就是将一个大的应用拆分为多个独立的小应用。服务的拆分使得多个需求能进行并行开发、测试,减小了各个需求之前的耦合,从而去提升开发效率和软件的质量。

在RPC之前,我们一般采用HTTP(S)协议进行远端调用,但是由于HTTP(S)为无状态协议,不支持长连接(HTTP1.1版本开始支持长连接),每次通信都要进行连接握手等操作,并且需要携带臃肿的Header参数,导致消耗了很多的时间和带宽。RPC协议一般基于TCP/UDP协议,支持长连接,使用特定的压缩编码格式对数据进行传输,从而极大的提升了通信效率和稳定性。

随着微服务架构的广泛使用,远端调用的需求也越来越多,针对HTTP(S)传输协议的消耗也越来越不能让从业人员接受,随之而来的就是对RPC的广泛使用了。

什么时候应该使用RPC?

目前浏览器端尚不支持RPC请求,所以RPC暂时只能应用于服务端。

对于初创公司来说,由于开发人员数量不会过多,不会存在太多并行需求,而是着重于快速迭代,所以一般不会在项目初期就开始使用微服务架构。相比传统web应用架构而言,微服务架构需要大量的基础设置,如注册中心、配置中心、日志链路追踪、服务监控等。

对于中大型公司,开发人员的数量已经充足,此时的需求一般是复杂并且并行的。对于这种场景,传统web应用架构无论是针对开发还是测试人员来说,都不能很好的进行日常工作,所以此阶段将要开始拆分应用,建立微服务架构的一些基础设施。随着服务数量的增多,服务间调用也越来越频繁,此时便会引入RPC协议,以优化服务间调用的效率和稳定性。

注:对于中大型公司的初创项目来说,因为已经存在很好的基础设施,且开发人员也熟悉了微服务架构开发的流程,所以一般在项目初期便会考虑使用微服务架构。

使用哪种RPC协议?

目前存在多种RPC协议,且大多都支持主流的语言,所以针对协议的选择便会造成一定的困扰。一般来讲主要由以下因素决定:

  • 开发语言,变动量越少越好,好的语言生态有较多的组件支持
  • 基础设施,RPC协议并不是孤立存在的,需要大量基础设施支持,保持一致很重要
  • 接入服务,考虑接入方所使用的协议,不然还要单独提供一套接入转发环境
  • 协议效率,常见于Thrift和其它几类协议之间的对比,考虑因素占比较低

基于以上四个因素,考虑语言生态以及不同语言使用者的习惯,所以特定语言选取特定RPC协议的概率较大。如果项目组成员全是Java开发者,则使用Dubbo框架是个不错的选择(使用框架默认支持的协议);如果项目组成员全是GoLang开发者,则大概率会选用gRPC协议。

gRPC的使用简介

以下内容主要参考gRPC Quick Start,需使用Go 1.11及以上版本。

安装对应的环境依赖

  1. 安装go语言支持,访问GoLang官网下载对应系统安装包安装即可。注意部分系统需手工将go/bin路径添加到环境变量中

  2. 安装gRPC支持,使用Go Module时非必需

    1
    
    $ go get -u -v google.golang.org/grpc
    
  3. 安装protoc编译器以生成gRPC服务代码,访问Protobuf Releases页面下载对应protoc-<version>-<platform>.zip,解压,将对应路径添加到环境变量中

  4. 安装protoc的go语言支持插件

    1
    2
    
    $ go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
    $ go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
    

编写第一个gRPC应用

  1. 创建项目
    1
    2
    
    $ mkdir grpc-demo
    $ go mod init grpc-demo
    
  2. 创建protobuf文件grpc-demo/pb/first-grpc.proto,定义gRPC消息传递的必需信息,具体可参考protocol buffers
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    syntax = "proto3";  // 声明proto版本
    package first_grpc; // 包名,无需显式声明
    
    // 定义一个gRPC服务
    service FirstGRPC {
        // 定义gRPC服务支持调用的方法名、参数、响应信息
        rpc TestGRPC (RequestParam) returns (ReplyBody) {}
    }
    
    // 定义一个消息的结构,这里为请求数据
    message RequestParam {
        string name = 1;
        string value = 2;
    }
    
    // 定义一个消息的结构,这里为响应数据
    message ReplyBody {
        int32 code = 1;
        string message = 2;
    }
    
  3. 使用protoc工具生成上述定义的gRPC服务代码
    1
    
    $ protoc -I pb/ pb/first-grpc.proto --go_out=plugins=grpc:pb
    
    这里将生成grpc-demo/pb/first-grpc.pb.go文件
  4. 编写server端代码grpc-demo/server/main.go
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    package main
    
    import (
        "context"
        "fmt"
        "google.golang.org/grpc"
        firstGRPC "grpc-demo/pb"
        "net"
    )
    
    type FirstGRPCServer struct {
        firstGRPC.UnimplementedFirstGRPCServer
    }
    
    func (firstGRPCServer *FirstGRPCServer) TestGRPC(ctx context.Context, requestParam *firstGRPC.RequestParam) (
        *firstGRPC.ReplyBody, error) {
        fmt.Println("requestParam:", requestParam.String())
        return &firstGRPC.ReplyBody{Code: 0, Message: "first gRPC call success"}, nil
    }
    
    func main() {
        listenAddress, err := net.Listen("tcp", "127.0.0.1:80")
        if err != nil {
            fmt.Println("failed to listen:", err)
            return
        }
        fmt.Println("listen success")
    
        gRPCServer := grpc.NewServer()
        firstGRPC.RegisterFirstGRPCServer(gRPCServer, &FirstGRPCServer{})
        if err := gRPCServer.Serve(listenAddress); err != nil {
            fmt.Println("failed to server:", err)
            return
        }
    }
    
  5. 编写client端代码grpc-demo/client/main.go
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    package main
    
    import (
        "context"
        "fmt"
        "google.golang.org/grpc"
        firstGRPC "grpc-demo/pb"
        "os"
        "time"
    )
    
    const (
        serverAddress = "127.0.0.1:80"
    )
    
    func main() {
        conn, err := grpc.Dial(serverAddress, grpc.WithInsecure(), grpc.WithBlock())
        if err != nil {
            fmt.Println("connect serverAddress failed error:", err)
            return
        }
        defer conn.Close()
        gRPCClient := firstGRPC.NewFirstGRPCClient(conn)
    
        paramName := "defaultName"
        paramValue := "defaultValue"
        if len(os.Args) > 2 {
            paramName = os.Args[1]
            paramValue = os.Args[2]
        }
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        gRPCResponse, err := gRPCClient.TestGRPC(ctx, &firstGRPC.RequestParam{Name: paramName, Value: paramValue})
        if err != nil {
            fmt.Println("call TestGRPC failed error:", err)
            return
        }
        fmt.Println("call TestGRPC success res:", gRPCResponse.GetMessage())
    }
    
  6. 测试
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    # 编译server端代码,生成可执行文件
    $ go build -o server/server server/main.go
    
    # 编译client端代码,生成可执行文件
    $ go build -o client/client client/main.go
    
    # 启用server端代码
    $ ./server/server
    
    # 启用client端代码
    $ ./client/client