2020年04月12日19:38:25

GRPC初始

GRPC协议(Google Remote Procedure Call Protocol)是谷歌发布的一个基于HTTP2协议承载的高性能、通用的RPC开源软件框架。通信双方都基于该框架进行二次开发,从而使得通信双方聚焦在业务,无需关注由GRPC软件框架实现的底层通信。

官方文档:


GRPC协议栈分层如下图所示:

图1-1 GRPC协议栈分层

grpc协议报文格式 grpc是什么协议_grpc协议报文格式

表1-1 GRPC协议栈分层

字段

说明

TCP层

底层通信协议,基于TCP连接。

TLS层

该层是可选的,基于TLS 1.2加密通道和双向证书认证等。

HTTP2层

GRPC承载在HTTP2协议上,利用了HTTP2的双向流、流控、头部压缩、单连接上的多路复用请求等特性。

GRPC层

远程过程调用,定义了远程过程调用的协议交互格式。

数据模型层

通信双方需要了解彼此的数据模型,才能正确交互。

gRPC 是基于 HTTP/2 协议的,要深刻理解 gRPC,理解下 HTTP/2 是必要的,这里先简单介绍一下 HTTP/2 相关的知识,然后在介绍下 gRPC 是如何基于 HTTP/2 构建的。

HTTP/1.x

HTTP 协议可以算是现阶段 Web 上面最通用的协议了,在之前很长一段时间,很多应用都是基于 HTTP/1.x 协议,HTTP/1.x 协议是一个文本协议,可读性非常好,但其实并不高效,笔者主要碰到过几个问题:

Parser

如果要解析一个完整的 HTTP 请求,首先我们需要能正确的读出 HTTP header。HTTP header 各个 fields 使用 \r\n 分隔,然后跟 body 之间使用 \r\n\r\n 分隔。解析完 header 之后,我们才能从 header 里面的 content-length 拿到 body 的 size,从而读取 body。

这套流程其实并不高效,因为我们需要读取多次,才能将一个完整的 HTTP 请求给解析出来,虽然在代码实现上面,有很多优化方式,譬如:

  • 一次将一大块数据读取到 buffer 里面避免多次 IO read
  • 读取的时候直接匹配 \r\n 的方式流式解析

但上面的方式对于高性能服务来说,终归还是会有开销。其实最主要的问题在于,HTTP/1.x 的协议是 文本协议,是给人看的,对机器不友好,如果要对机器友好,二进制协议才是更好的选择。

如果大家对解析 HTTP/1.x 很感兴趣,可以研究下 http-parser,一个非常高效小巧的 C library,见过不少框架都是集成了这个库来处理 HTTP/1.x 的。

Request/Response

HTTP/1.x 另一个问题就在于它的交互模式,一个连接每次只能一问一答,也就是client 发送了 request 之后,必须等到 response,才能继续发送下一次请求。

这套机制是非常简单,但会造成网络连接利用率不高。如果需要同时进行大量的交互,client 需要跟 server 建立多条连接,但连接的建立也是有开销的,所以为了性能,通常这些连接都是长连接一直保活的,虽然对于 server 来说同时处理百万连接也没啥太大的挑战,但终归效率不高。

Push

用 HTTP/1.x 做过推送的同学,大概就知道有多么的痛苦,因为 HTTP/1.x 并没有推送机制。所以通常两种做法:

  • Long polling 方式,也就是直接给 server 挂一个连接,等待一段时间(譬如 1 分钟),如果 server 有返回或者超时,则再次重新 poll。
  • Web-socket,通过 upgrade 机制显式的将这条 HTTP 连接变成裸的 TCP,进行双向交互。

相比 Long polling,笔者还是更喜欢 web-socket 一点,毕竟更加高效,只是 web-socket 后面的交互并不是传统意义上面的 HTTP 了。

Hello HTTP/2

虽然 HTTP/1.x 协议可能仍然是当今互联网运用最广泛的协议,但随着 Web 服务规模的不断扩大,HTTP/1.x 越发显得捉紧见拙,我们急需另一套更好的协议来构建我们的服务,于是就有了 HTTP/2。

HTTP/2 是一个二进制协议,这也就意味着它的可读性几乎为 0,但幸运的是,我们还是有很多工具,譬如 Wireshark, 能够将其解析出来。

在了解 HTTP/2 之前,需要知道一些通用术语:

  • Stream: 一个双向流,一条连接可以有多个 streams。
  • Message: 也就是逻辑上面的 request,response。
  • Frame::数据传输的最小单位。每个 Frame 都属于一个特定的 stream 或者整个连接。一个 message 可能有多个 frame 组成。

gRPC 一开始由 google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

grpc协议报文格式 grpc是什么协议_grpc协议报文格式_02

特性

  • 基于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中,其主要应用场景如下:
- 低延迟、高扩展性、分布式的系统
- 同云服务器进行通信的移动应用客户端
- 设计语言独立、高效、精确的新协议
- 便于各方面扩展的分层设计,如认证、负载均衡、日志记录、监控等

HTTP2.0 特性

HTTP/2,也就是超文本传输协议第2版,不论是1还是2,HTTP的基本语义是不变的,比如方法语义(GET/PUST/PUT/DELETE),状态码(200/404/500等),Range Request,Cacheing,Authentication、URL路径, 不同的主要是下面几点:

多路复用 (Multiplexing)

在 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」。

HTTP/2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。
因此 HTTP/2 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。

grpc协议报文格式 grpc是什么协议_HTTP_03

二进制帧

HTTP/2 传输的数据是二进制的。相比 HTTP/1.1 的纯文本数据,二进制数据一个显而易见的好处是:更小的传输体积。这就意味着更低的负载。二进制的帧也更易于解析而且不易出错,纯文本帧在解析的时候还要考虑处理空格、大小写、空行和换行等问题,而二进制帧就不存在这个问题。

grpc协议报文格式 grpc是什么协议_grpc协议报文格式_04

首部压缩(Header Compression)

HTTP是无状态协议。简而言之,这意味着每个请求必须要携带服务器需要的所有细节,而不是让服务器保存住之前请求的元数据。因为http2没有改变这个范式,所以它也需要这样(携带所有细节),因此 HTTP 请求的头部需要包含用于标识身份的数据比如 cookies,而这些数据的量也在随着时间增长。每一个请求的头部都包含这些大量的重复数据,无疑是一种很大的负担。对请求头部进行压缩,将会大大减轻这种负担,尤其对移动端来说,性能提高非常明显。

HTTP/2 使用的压缩方式是 HPACK。 http://http2.github.io/http2-spec/compression.html

HTTP2.0在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。

grpc协议报文格式 grpc是什么协议_grpc协议报文格式_05

事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。

如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP2.0的连接存续期内始终存在,由客户端和服务器共同渐进地更新。

服务端推送(Server Push)

HTTP/2 的服务器推送所作的工作就是,服务器在收到客户端对某个资源的请求时,会判断客户端十有八九还要请求其他的什么资源,然后一同把这些资源都发送给客户端,即便客户端还没有明确表示它需要这些资源。

客户端可以选择把额外的资源放入缓存中(所以这个特点也叫 Cache push),也可以选择发送一个 RST_STREAM frame 拒绝任何它不想要的资源。

grpc协议报文格式 grpc是什么协议_客户端_06

主动重置链接

Length的HTTP消息被送出之后,我们就很难中断它了。当然,通常我们可以断开整个TCP链接(但也不总是可以这样),但这样导致的代价就是需要重新通过三次握手建立一个新的TCP连接。

HTTP/2 引入了一个 RST_STREAM frame 来让客户端在已有的连接中发送重置请求,从而中断或者放弃响应。当浏览器进行页面跳转或者用户取消下载时,它可以防止建立新连接,避免浪费所有带宽。

gRPC优缺点:

优点:

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

缺点:

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

例子:

gRPC 和通常的基于TCP的实现不同,是直接基于HTTP2 协议的。HTTP2 使得grpc 能够更好的适用于移动客户端和服务端通信的使用场景,并且连接多路复用也保证了RPC 的效率。

gRPC 的协议设计上很好的使用了HTTP2 现有的语义,请求和响应的数据使用HTTP Body 发送,其他的控制信息则用Header 表示。

先看个例子,假设Protobuf 定义如下:

package foo.bar;

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

service HelloService {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}

在这里面我们定义了一个service HelloServicegrpc 为这样一个调用发送的请求为:

HEADERS (flags = END_HEADERS)
:method = POST
:scheme = http
:path = /foo.bar.HelloService/SayHello
:authority = api.test.com
grpc-timeout = 1S
content-type = application/grpc+proto
grpc-encoding = gzip
authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v

DATA (flags = END_STREAM)
<Delimited Message>

Http 请求的Path 部分用来表示调用哪个服务,格式是/{package}.{ServiceName}/{RpcMethodName}

content-type 目前取值都是application/grpc+proto

将来grpc 支持除Protobuf 之外的协议如Json 时,会有别的值。

grpc-encoding 可以有gzip, deflate, snappy 等取值,表示采用的压缩方法。

grpc-timeout 表示调用的超时时间,单位有Hour(H), Minute(M), Second(S), Millisecond(m), Microsecond(u), Nanosecond(n) 等。

除了grpc 定义的标准头之外,也可以自己添加新的头。如果是二进制的Header,则Header Name 以-bin 结尾,

Header Value 是经过Base64 编码的二进制数据。

服务端对这个请求返回一个Response:

HEADERS (flags = END_HEADERS)
:status = 200
grpc-encoding = gzip

DATA
<Delimited Message>

HEADERS (flags = END_STREAM, END_HEADERS)
grpc-status = 0 # OK
trace-proto-bin = jher831yy13JHy3hc

grpc-status 为0 表示请求没有出现问题,成功返回。

grpc 还定义了GOAWAY Frame, 当Server 断开一个连接的时候,需要向客户端发送这样一条消息;以及PING Frame,接受到PING Frame 后直接原样返回数据,用于连接存活检测和延迟检测。

HTTP2 的Header 并不是特别高效的格式,存储上和解析上都有一些效率问题。如果启用加密连接的话,则会有更多的效率开销。