gRPC详解

gRPC是什么?

gRPC是什么可以用官网的一句话来概括

A high-performance, open-source universal RPC framework

所谓RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。如下图所示就是一个典型的RPC结构图。

 
gRPC详解
RPC通信

gRPC有什么好处以及在什么场景下需要用gRPC

既然是server/client模型,那么我们直接用restful api不是也可以满足吗,为什么还需要RPC呢?下面我们就来看看RPC到底有哪些优势

gRPC vs. Restful API

gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:

  • gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。关于protobuf可以参见笔者之前的小文Google Protobuf简明教程
  • 另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
  • gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)

使用场景

  • 需要对接口进行严格约束的情况,比如我们提供了一个公共的服务,很多人,甚至公司外部的人也可以访问这个服务,这时对于接口我们希望有更加严格的约束,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时gRPC就可以通过protobuf来提供严格的接口约束。
  • 对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率。

但是,通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等等必要的组件。不过,这就不属于本篇文章讨论的主题了,我们还是先继续看下如何使用gRPC。

gRPC HelloWorld实例详解

gRPC的使用通常包括如下几个步骤:

  1. 通过protobuf来定义接口和数据类型
  2. 编写gRPC server端代码
  3. 编写gRPC client端代码
    下面来通过一个实例来详细讲解上述的三步。
    下边的hello world实例完成之后,其目录结果如下:


     
    gRPC详解
    project helloworld

定义接口和数据类型

  • 通过protobuf定义接口和数据类型
syntax = "proto3";

package rpc_package;

// define a service
service HelloWorldService {
    // define the interface and data type
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// define the data type of request
message HelloRequest {
    string name = 1;
}

// define the data type of response
message HelloReply {
    string message = 1;
}
  • 使用gRPC protobuf生成工具生成对应语言的库函数
python -m grpc_tools.protoc -I=./protos --python_out=./rpc_package --grpc_python_out=./rpc_package ./protos/user_info.proto

这个指令会自动生成rpc_package文件夹中的helloworld_pb2.pyhelloworld_pb2_grpc.py,但是不会自动生成__init__.py文件,需要我们手动添加

关于protobuf的详细解释请参考Google Protobuf简明教程

gRPC server端代码

#!/usr/bin/env python
# -*-coding: utf-8 -*-

from concurrent import futures
import grpc
import logging
import time

from rpc_package.helloworld_pb2_grpc import add_HelloWorldServiceServicer_to_server, \ 
    HelloWorldServiceServicer
from rpc_package.helloworld_pb2 import HelloRequest, HelloReply


class Hello(HelloWorldServiceServicer):

    # 这里实现我们定义的接口
    def SayHello(self, request, context):
        return HelloReply(message=‘Hello, %s!‘ % request.name)


def serve():
    # 这里通过thread pool来并发处理server的任务
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

    # 将对应的任务处理函数添加到rpc server中
    add_HelloWorldServiceServicer_to_server(Hello(), server)

    # 这里使用的非安全接口,世界gRPC支持TLS/SSL安全连接,以及各种鉴权机制
    server.add_insecure_port(‘[::]:50000‘)
    server.start()
    try:
        while True:
            time.sleep(60 * 60 * 24)
    except KeyboardInterrupt:
        server.stop(0)


if __name__ == "__main__":
    logging.basicConfig()
    serve()

gRPC client端代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
import logging

import grpc
from rpc_package.helloworld_pb2 import HelloRequest, HelloReply
from rpc_package.helloworld_pb2_grpc import HelloWorldServiceStub

def run():
    # 使用with语法保证channel自动close
    with grpc.insecure_channel(‘localhost:50000‘) as channel:
        # 客户端通过stub来实现rpc通信
        stub = HelloWorldServiceStub(channel)

        # 客户端必须使用定义好的类型,这里是HelloRequest类型
        response = stub.SayHello(HelloRequest(name=‘eric‘))
    print ("hello client received: " + response.message)

if __name__ == "__main__":
    logging.basicConfig()
    run()

演示

先执行server端代码

python hello_server.py

接着执行client端代码如下:

?  grpc_test python hello_client.py
hello client received: Hello, eric!

References

gRPC 是一个高性能、通用的开源RPC框架,基于HTTP/2协议标准和Protobuf序列化协议开发,支持众多的开发语言。

概述

在gRPC框架中,客户端可以像调用本地对象一样直接调用位于不同机器的服务端方法,如此我们就可以非常方便的创建一些分布式的应用服务。

在服务端,我们实现了所定义的服务和可供远程调用的方法,运行一个gRPC server来处理客户端的请求;在客户端,gRPC实现了一个stub(可以简单理解为一个client),其提供跟服务端相同的方法。

 
gRPC详解
image

gRPC使用protocol buffers作为接口描述语言(IDL)以及底层的信息交换格式,一般情况下推荐使用 proto3因为其能够支持更多的语言,并减少一些兼容性的问题。

特性

基于HTTP/2
HTTP/2 提供了连接多路复用、双向流、服务器推送、请求优先级、首部压缩等机制。可以节省带宽、降低TCP链接次数、节省CPU,帮助移动设备延长电池寿命等。gRPC 的协议设计上使用了HTTP2 现有的语义,请求和响应的数据使用HTTP Body 发送,其他的控制信息则用Header 表示。
IDL使用ProtoBuf
gRPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似于XML、JSON、hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。压缩和传输效率高,语法简单,表达力强。
多语言支持(C, C++, Python, PHP, Nodejs, C#, Objective-C、Golang、Java)
gRPC支持多种语言,并能够基于语言自动生成客户端和服务端功能库。目前已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它语言的版本正在积极开发中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等语言,grpc-java已经支持Android开发。
gRPC已经应用在Google的云服务和对外提供的API中,其主要应用场景如下:

  • 低延迟、高扩展性、分布式的系统
  • 同云服务器进行通信的移动应用客户端
  • 设计语言独立、高效、精确的新协议
  • 便于各方面扩展的分层设计,如认证、负载均衡、日志记录、监控等

grpc优缺点:

优点:

  • 1、protobuf二进制消息,性能好/效率高(空间和时间效率都很不错)
  • 2、proto文件生成目标代码,简单易用
  • 3、序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式)
  • 4、支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级
  • 5、支持多种语言(可以把proto文件看做IDL文件)
  • 6、Netty等一些框架集成

缺点:

  • 1、GRPC尚未提供连接池,需要自行实现
  • 2、尚未提供“服务发现”、“负载均衡”机制
  • 3、因为基于HTTP2,绝大部多数HTTP Server、Nginx都尚不支持,即Nginx不能将GRPC请求作为HTTP请求来负载均衡,而是作为普通的TCP请求。(nginx1.9版本已支持)
  • 4、Protobuf二进制可读性差(貌似提供了Text_Fromat功能)
  • 5、默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)

gRPC有四种通信方式:

  • 1、 Simple RPC
    简单rpc
    这就是一般的rpc调用,一个请求对象对应一个返回对象
    proto语法:
rpc simpleHello(Person) returns (Result) {}
  • 2、 Server-side streaming RPC
    服务端流式rpc
    一个请求对象,服务端可以传回多个结果对象
    proto语法
rpc serverStreamHello(Person) returns (stream Result) {}
  • 3、 Client-side streaming RPC
    客户端流式rpc
    客户端传入多个请求对象,服务端返回一个响应结果
    proto语法
rpc clientStreamHello(stream Person) returns (Result) {}
  • 4、 Bidirectional streaming RPC
    双向流式rpc
    结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象
    proto语法
rpc biStreamHello(stream Person) returns (stream Result) {}

服务定义及ProtoBuf

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

protoc编译工具

protoc工具可在https://github.com/google/protobuf/releases 下载到源码。 且将protoc的bin目录配置到环境变量中,如下图:

 
gRPC详解
image.png

实际开发中一般都通过在idea上配置com.google.protobuf插件进行开发,这一点在https://github.com/grpc/grpc-java上的文档有详细说明,如果使用gradle进行项目构建的话,https://github.com/google/protobuf-gradle-plugin上有protobuf-gradle-plugin插件的详细使用说明。

protobuf语法

  • 1、syntax = “proto3”;
    文件的第一行指定了你使用的是proto3的语法:如果你不指定,protocol buffer 编译器就会认为你使用的是proto2的语法。这个语句必须出现在.proto文件的非空非注释的第一行。

  • 2、message SearchRequest {……}
    message 定义实体,c/c++/go中的结构体,php中类

  • 3、基本数据类型


     
    gRPC详解
    image.png
  • 4、注释符号: 双斜线,如://xxxxxxxxxxxxxxxxxxx

  • 5、字段唯一数字标识(用于在二进制格式中识别各个字段,上线后不宜再变动):Tags
    1到15使用一个字节来编码,包括标识数字和字段类型(你可以在Protocol Buffer 编码中查看更多详细);16到2047占用两个字节。因此定义proto文件时应该保留1到15,用作出现最频繁的消息类型的标识。记得为将来会继续增加并可能频繁出现的元素留一点儿标识区间,也就是说,不要一下子把1—15全部用完,为将来留一点儿。
    标识数字的合法范围:最小是1,最大是 229 - 1,或者536,870,911。
    另外,不能使用19000 到 19999之间的数字(FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber),因为它们被Protocol Buffers保留使用

  • 6、字段修饰符:
    required:值不可为空
    optional:可选字段
    singular:符合语法规则的消息包含零个或者一个这样的字段(最多一个)
    repeated:一个字段在合法的消息中可以重复出现一定次数(包括零次)。重复出现的值的次序将被保留。在proto3中,重复出现的值类型字段默认采用压缩编码。你可以在这里找到更多关于压缩编码的东西: Protocol Buffer Encoding。
    默认值: optional PhoneType type = 2 [default = HOME];
    proto3中,省略required,optional,singular,由protoc自动选择。

  • 7、代理类生成
    1)、C++, 每一个.proto 文件可以生成一个 .h 文件和一个 .cc 文件
    2)、Java, 每一个.proto文件可以生成一个 .java 文件
    3)、Python, 每一个.proto文件生成一个模块,其中为每一个消息类型生成一个静态的描述器,在运行时,和一个metaclass一起使用来创建必要的Python数据访问类
    4)、Go, 每一个.proto生成一个 .pb.go 文件
    5)、Ruby, 每一个.proto生成一个 .rb 文件
    6)、Objective-C, 每一个.proto 文件可以生成一个 pbobjc.h 和一个pbobjc.m 文件
    7)、C#, 每一个.proto文件可以生成一个.cs文件.
    8)、php, 每一个message消息体生成一个.php类文件,并在GPBMetadata目录生成一个对应包名的.php类文件,用于保存.proto的二进制元数据。

  • 8、字段默认值

  • strings, 默认值是空字符串(empty string)
  • bytes, 默认值是空bytes(empty bytes)
  • bools, 默认值是false
  • numeric, 默认值是0
  • enums, 默认值是第一个枚举值(value必须为0)
  • message fields, the field is not set. Its exact value is langauge-dependent. See the generated code guide for details.
  • repeated fields,默认值为empty,通常是一个空list
  • 9、枚举
// 枚举类型,必须从0开始,序号可跨越。同一包下不能重名,所以加前缀来区别
enum WshExportInstStatus {
    INST_INITED = 0;
    INST_RUNNING = 1;
    INST_FINISH = 2;
    INST_FAILED = 3;
}
  • 10、Maps字段类型
map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。
例如,如果你希望创建一个project的映射,每个Projecct使用一个string作为key,你可以像下面这样定义:

map<string, Project> projects = 3;

Map的字段可以是repeated。
序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。

  • 11、默认值
    字符串类型默认为空字符串
    字节类型默认为空字节
    布尔类型默认false
    数值类型默认为0值
    enums类型默认为第一个定义的枚举值,必须是0

  • 12、服务
    服务使用service{}包起来,每个方法使用rpc起一行申明,一个方法包含一个请求消息体和一个返回消息体

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

更多protobuf参考(google)
更多protobuf参考(csdn)

对于开发者而言:

  • 1、需要使用protobuf定义接口,即.proto文件

  • 2、然后使用compile工具生成特定语言的执行代码,比如JAVA、C/C++、Python等。类似于thrift,为了解决跨语言问题。

  • 3、启动一个Server端,server端通过侦听指定的port,来等待Client链接请求,通常使用Netty来构建,GRPC内置了Netty的支持。

  • 4、启动一个或者多个Client端,Client也是基于Netty,Client通过与Server建立TCP长链接,并发送请求;Request与Response均被封装成HTTP2的stream Frame,通过Netty Channel进行交互。

相关推荐