0%

[Protobuf&gRPC] 2.gRPC简介

RPC 框架原理#

RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。

业界主流的 RPC 框架整体上分为三类:

  • 支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift
  • 只支持特定语言的 RPC 框架,例如新浪微博的 Motan
  • 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo。

gRPC 简介#

gRPC 是一个高性能、开源和通用的 RPC 框架,面向服务端和移动端,基于 HTTP/2 设计。

gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。此外,Google 最新 API 将有 gRPC 版本的接口,使你很容易地将 Google 的功能集成到你的应用里。

gRPC 特点 - 语言中立,支持多种语言; - 基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub; - 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量; - 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。

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

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

  • gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。
  • 通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能
  • gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)
  • proto文件生成目标代码,简单易用
  • 序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式)
  • 支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级
  • Netty等一些框架集成

缺点:

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

使用 protocol buffers#

gRPC 默认使用 protocol buffers(protobuf),这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON)。正如你将在下方例子里所看到的,你用 proto files 创建 gRPC 服务,用 protobuf 消息类型来定义方法参数和返回类型。protobuf相关可以看 protobuf简介

基于HTTP 2.0标准设计#

由于gRPC基于HTTP 2.0标准设计,带来了更多强大功能,如多路复用、二进制帧、头部压缩、推送机制。这些功能给设备带来重大益处,如节省带宽、降低TCP连接次数、节省CPU使用等。gRPC既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现两端的通信和简化通信系统的构建

HTTP 版本分为HTTP 1.X、 HTTP 2.0,其中HTTP 1.X是当前使用最广泛的HTTP协议,HTTP 2.0称为超文本传输协议第二代。HTTP 1.X定义了四种与服务器交互的方式,分别为:GET、POST、PUT、DELETE,这些在HTTP 2.0中均保留。我们再来看看HTTP 2.0的新特性:

1.双向流、多路复用#

在HTTP 1.X协议中,客户端在同一时间访问同一域名的请求数量是有限制的,当超过阈值时请求会被阻断,但是这种情况在HTTP 2.0中将被忽略。由于HTTP 1.X传输的是纯文本数据,传输体积较大,而HTTP 2.0传输的基本单元为帧,每个帧都包含消息,并且由于HTTP 2.0允许同时通过一条连接发起多个“请求-响应”消息,无需建立多个TCP链接的同时实现多条流并行,提高吞吐性能,并且在一个连接内对多个消息进行优先级的管理和流控。

2.二进制帧#

相对于HTTP 1.X的纯文本传输,HTTP 2.0传输的是二进制数据,与Protocol Buffers相辅相成。使得传输数据体积小、负载低,保持更加紧凑和高效

3.头部压缩#

因为HTTP是无状态协议,对于业务的处理没有记忆能力,每一次请求都需要携带设备的所有细节,特别是在头部都会包含大量的重复数据,对于设备来说就是在不断地做无意义的重复性工作。HTTP 2.0中使用“头表”来跟踪之前发送的数据,对于相同的数据将不再使用重复请求和发送,进而减少数据的体积


gRPC有四种通信方式:#

1、 Simple RPC#

简单rpc 这就是一般的rpc调用,一个请求对象对应一个返回对象 proto语法:

1
rpc simpleHello(Person) returns (Result) {}
### 2、 Server-side streaming RPC 服务端流式rpc 一个请求对象,服务端可以传回多个结果对象 proto语法
1
rpc serverStreamHello(Person) returns (stream Result) {}
### 3、 Client-side streaming RPC 客户端流式rpc 客户端传入多个请求对象,服务端返回一个响应结果 proto语法
1
rpc clientStreamHello(stream Person) returns (Result) {}
### 4、 Bidirectional streaming RPC 双向流式rpc 结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象 proto语法
1
rpc biStreamHello(stream Person) returns (stream Result) {}
--- ## 服务端创建流程 gRPC 服务端创建采用 Build 模式,对底层服务绑定、transportServer 和 NettyServer 的创建和实例化做了封装和屏蔽,让服务调用者不用关心 RPC 调用细节,整体上分为三个过程:

  • 创建 Netty HTTP/2 服务端;
  • 将需要调用的服务端接口实现类注册到内部的 Registry 中,RPC 调用时,可以根据 RPC 请求消息中的服务定义信息查询到服务接口实现类;
  • 创建 gRPC Server,它是 gRPC 服务端的抽象,聚合了各种 Listener,用于 RPC 消息的统一调度和处理。

gRPC 服务端创建关键流程分析:#

  • NettyServer 实例创建:gRPC 服务端创建,首先需要初始化 NettyServer,它是 gRPC 基于 Netty 4.1 HTTP/2 协议栈之上封装的 HTTP/2 服务端。NettyServer 实例由 NettyServerBuilder 的 buildTransportServer 方法构建,NettyServer 构建完成之后,监听指定的 Socket 地址,即可实现基于 HTTP/2 协议的请求消息接入
  • 绑定 IDL 定义的服务接口实现类:gRPC 与其它一些 RPC 框架的差异点是服务接口实现类的调用并不是通过动态代理和反射机制,而是通过 proto 工具生成代码,在服务端启动时,将服务接口实现类实例注册到 gRPC 内部的服务注册中心上。请求消息接入之后,可以根据服务名和方法名,直接调用启动时注册的服务实例,而不需要通过反射的方式进行调用,性能更优。
  • gRPC 服务实例(ServerImpl)构建:ServerImpl 负责整个 gRPC 服务端消息的调度和处理,创建 ServerImpl 实例过程中,会对服务端依赖的对象进行初始化,例如 Netty 的线程池资源、gRPC 的线程池、内部的服务注册类(InternalHandlerRegistry)等,ServerImpl 初始化完成之后,就可以调用 NettyServer 的 start 方法启动 HTTP/2 服务端,接收 gRPC 客户端的服务调用请求

gRPC HelloWorld实例详解#

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

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

1. 定义接口和数据类型#

通过protobuf定义接口和数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";

package rpc_package;

// 客户端发送rpc方法,response = stub.SayHello(HelloRequest(name='eric'))
// 服务端返回response,是HelloReply类型

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;
}

2.使用gRPC protobuf生成工具生成对应语言的库函数#

1
python -m grpc_tools.protoc -I=./protos --python_out=./rpc_package --grpc_python_out=./rpc_package

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

3. 编写gRPC server端代码#

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
40
#!/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()

4.gRPC client端代码#

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
#!/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通信
# 传入通信channel
stub = HelloWorldServiceStub(channel)

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

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

demo#

运行server端代码

1
python hello_server.py
接着执行client端代码如下:
1
2
➜  grpc_test python hello_client.py
hello client received: Hello, eric!

reference#

  1. gRPC官网
  2. grpc原理
  3. grpc特性分析
  4. gRPC 官方文档中文版
  5. grpc应用详解与实例剖析