gRPC知识点

概览

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

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

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

概念

服务定义

正如其他 RPC 系统,gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型。gRPC 默认使用 protocol buffers 作为接口定义语言,来描述服务接口和有效载荷消息结构。如果有需要的话,可以使用其他替代方案。

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

gRPC 允许你定义四类服务方法,使用场景的区别主要在于通信需求的不同。胡乱使用场景可能会导致性能问题,内存占用,延迟问题,资源竞争;

  • 单项 RPC(一元RPC),即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
  • 场景:一般用于简单的请求-响应场景,请求和响应之间的通信是单向的;客户端请求服务器返回某个主题的实时数据流,服务器将数据流发送给客户端,直到没有更多数据可发送为止。
rpc SayHello(HelloRequest) returns (HelloResponse){
}
  • 服务端流式 RPC,即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
  • 场景:适用于服务器端有大量数据需要传输给客户端的情况
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
  • 客户端流式 RPC,即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。
  • 场景:适用于客户端需要发送大量数据到服务器,而服务器只需要返回一个简单响应的情况。客户端将一系列数据发送给服务器,服务器收到所有数据后执行计算并返回结果给客户端
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
  • 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
  • 场景:适用于需要双向实时通信的场景,允许客户端和服务器之间的异步操作;客户端和服务器建立一个视频通话,双方可以同时发送和接收视频和音频数据。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
总结

gRPC 利用 Protocol BuffersHTTP/2 这两项技术,实现了其四种类型的服务方法。Protocol Buffers 用于定义消息格式,而 HTTP/2 则用于在客户端和服务器之间进行数据传输,并支持双向流、流量控制等功能,从而满足了不同类型的服务方法的需求

使用 API 接口

gRPC 提供 protocol buffer 编译插件,能够从一个服务定义的 .proto 文件生成客户端和服务端代码。通常 gRPC 用户可以在服务端实现这些API,并从客户端调用它们

  • 在服务侧,服务端实现服务接口,运行一个 gRPC 服务器来处理客户端调用。gRPC 底层架构会解码传入的请求,执行服务方法,编码服务应答。
  • 在客户侧,客户端有一个_存根_实现了服务端同样的方法。客户端可以在本地存根调用这些方法,用合适的 protocol buffer 消息类型封装这些参数— gRPC 来负责发送请求给服务端并返回服务端 protocol buffer 响应。

同步 vs 异步

同步调用
  • 客户端发送请求到服务器并等待服务器处理完成并返回响应之后才能继续执行后续代码。这种模式下,调用方(客户端)会被阻塞,直到接收到完整的响应或者发生超时错误。
  • 同步调用适用于简单的请求-响应场景,通常在代码中使用起来比较直观和简单。
异步调用
  • 客户端发送请求后不会立即等待服务器的响应,而是可以继续执行后续的代码逻辑。同时,一旦服务器处理完成并返回响应,客户端会得到通知并处理响应。
  • 异步调用适用于需要处理大量并发请求的场景,或者需要实现非阻塞的响应逻辑的情况下。通过异步调用,可以更好地利用系统资源,提高系统的并发性能和响应速度。

gRPC 提供了对应的同步和异步 API,BlockingStub 来创建客户端,它提供了同步的 RPC 方法调用。而在异步调用中,可以使用 AsyncStub 或者 FutureStub 来创建客户端,它们提供了异步的 RPC 方法调用,允许开发者使用回调函数或者 Future 对象来处理响应结果

RPC 生命周期

单项 RPC

首先我们来了解一下最简单的 RPC 形式:客户端发出单个请求,获得单个响应。

  • 一旦客户端通过桩调用一个方法,服务端会得到相关通知 ,通知包括客户端的元数据,方法名,允许的响应期限(如果可以的话)
  • 服务端既可以在任何响应之前直接发送回初始的元数据,也可以等待客户端的请求信息,到底哪个先发生,取决于具体的应用。
  • 一旦服务端获得客户端的请求信息,就会做所需的任何工作来创建或组装对应的响应。如果成功的话,这个响应会和包含状态码以及可选的状态信息等状态明细及可选的追踪信息返回给客户端 。
  • 假如状态是 OK 的话,客户端会得到应答,这将结束客户端的调用。

服务端流式 RPC

服务端流式 RPC 除了在得到客户端请求信息后发送回一个应答流之外,与我们的简单例子一样。在发送完所有应答后,服务端的状态详情(状态码和可选的状态信息)和可选的跟踪元数据被发送回客户端,以此来完成服务端的工作。客户端在接收到所有服务端的应答后也完成了工作。

客户端流式 RPC

客户端流式 RPC 也基本与我们的简单例子一样,区别在于客户端通过发送一个请求流给服务端,取代了原先发送的单个请求。服务端通常(但并不必须)会在接收到客户端所有的请求后发送回一个应答,其中附带有它的状态详情和可选的跟踪数据。

双向流式 RPC

双向流式 RPC ,调用由客户端调用方法来初始化,而服务端则接收到客户端的元数据,方法名和截止时间。服务端可以选择发送回它的初始元数据或等待客户端发送请求。 下一步怎样发展取决于应用,因为客户端和服务端能在任意顺序上读写 - 这些流的操作是完全独立的。例如服务端可以一直等直到它接收到所有客户端的消息才写应答,或者服务端和客户端可以像"乒乓球"一样:服务端后得到一个请求就回送一个应答,接着客户端根据应答来发送另一个请求,以此类推。

截止时间

gRPC 允许客户端在调用一个远程方法前指定一个最后期限值。这个值指定了在客户端可以等待服务端多长时间来应答,超过这个时间值 RPC 将结束并返回DEADLINE_EXCEEDED错误。在服务端可以查询这个期限值来看是否一个特定的方法已经过期,或者还剩多长时间来完成这个方法。 各语言来指定一个截止时间的方式是不同的 - 比如在 Python 里一个截止时间值总是必须的,但并不是所有语言都有一个默认的截止时间。

RPC 终止

在 gRPC 里,客户端和服务端对调用成功的判断是独立的、本地的,他们的结论可能不一致。这意味着,比如你有一个 RPC 在服务端成功结束(“我已经返回了所有应答!”),到那时在客户端可能是失败的(“应答在最后期限后才来到!”)。也可能在客户端把所有请求发送完前,服务端却判断调用已经完成了。

在 gRPC 中,RPC 终止指的是客户端和服务器之间的通信过程结束的情况。RPC 终止可能发生在以下几种情况下:

  1. 成功完成:RPC 调用成功完成,服务器成功处理了客户端的请求并返回了响应。这种情况下,RPC 终止是正常的,客户端可以处理服务器返回的响应结果。

  2. 超时:RPC 调用在规定的时间内没有得到响应,客户端可能会超时并终止该 RPC。超时是一种常见的情况,通常是由于网络延迟或服务器负载高导致的。

  3. 取消:客户端主动取消了 RPC 调用,可能是因为不再需要该调用的结果,或者发生了一些意外情况需要取消调用。取消 RPC 调用可以帮助释放资源和减轻服务器负担。

  4. 失败:RPC 调用失败,可能是由于网络错误、服务器错误或其他异常情况导致的。在这种情况下,RPC 调用无法正常完成,客户端可能需要根据失败原因进行错误处理。

gRPC 提供了一些机制来处理 RPC 终止的情况,例如客户端可以设置超时时间来处理 RPC 超时,也可以在需要时取消 RPC 调用。此外,gRPC 还提供了一些错误处理机制,例如通过状态码和错误消息来传递错误信息。

取消 RPC

无论客户端还是服务端均可以再任何时间取消一个 RPC 。一个取消会立即终止 RPC 这样可以避免更多操作被执行。它_不是_一个"撤销", 在取消前已经完成的不会被回滚。当然,通过同步调用的 RPC 不能被取消,因为直到 RPC 结束前,程序控制权还没有交还给应用。

元数据集

元数据是一个特殊 RPC 调用对应的信息(授权详情]) ,这些信息以键值对的形式存在,一般键的类型是字符串,值的类型一般也是字符串(当然也可以是二进制数据)。元数据对 gRPC 本事来说是不透明的 - 它让客户端提供调用相关的信息给服务端,反之亦然。 对于元数据的访问是语言相关的。

流控制

流控制是在数据传输过程中数据流量进行管理和调节的一种机制。在 gRPC 中,流控制主要涉及到两个方面:客户端流控制服务器端流控制

  1. 客户端流控制

    • 客户端流控制指的是客户端控制向服务器端发送数据的速率的机制。在 gRPC 中,客户端可以通过设置流量控制窗口的大小来控制发送数据的速率。流量控制窗口是一个缓冲区,用于存储待发送的数据。当流量控制窗口的大小达到一定限制时,客户端将停止发送数据,直到收到服务器端的确认信号,然后才会继续发送数据。
    • 客户端流控制可以帮助客户端适应服务器端的处理能力,防止发送数据过快导致服务器端无法及时处理的情况。
  2. 服务器端流控制

    • 服务器端流控制指的是服务器端控制向客户端发送数据的速率的机制。在 gRPC 中,服务器端可以通过设置流量控制窗口的大小来控制发送数据的速率。当服务器端的发送速率超过客户端的处理能力时,服务器端将停止发送数据,直到收到客户端的确认信号,然后才会继续发送数据。
    • 服务器端流控制可以帮助服务器适应客户端的处理能力,防止发送数据过快导致客户端无法及时处理的情况。****

配置

在 gRPC 中,配置是指一组参数或设置,用于控制 gRPC 客户端和服务器端的行为和特性。配置可以包括连接参数、流控制参数、安全设置、超时设置等等,通过配置可以调整和优化 gRPC 的性能、安全性和可靠性。

以下是一些常见的 gRPC 配置概念:

  1. 连接参数:配置连接的属性,如服务器地址、端口号、连接超时时间等。这些参数用于建立和管理 gRPC 客户端和服务器端之间的连接。

  2. 流控制参数:配置流量控制的参数,如流控制窗口大小、流控制策略等。这些参数用于控制数据的发送速率,以防止发送端和接收端之间的数据拥塞和丢失。

  3. 安全设置:配置安全传输的参数,如 SSL/TLS 加密算法、证书验证方式、身份认证方式等。这些参数用于保护数据在传输过程中的安全性和完整性。

  4. 超时设置:配置 RPC 调用的超时时间,如请求超时时间、连接超时时间等。这些参数用于控制 RPC 调用的最大执行时间,以防止因网络延迟或服务器负载高导致的等待时间过长而影响性能。

  5. 拦截器设置:配置拦截器的参数,用于在 RPC 调用的前后执行一些额外的逻辑,如日志记录、性能监控等。这些参数可以帮助开发者更好地理解和调试 gRPC 客户端和服务器端的行为。

频道

在创建客户端存根时,一个 gRPC 频道提供一个特定主机和端口服务端的连接。客户端可以通过指定频道参数来修改 gRPC 的默认行为,比如打开关闭消息压缩。一个频道具有状态,包含已连接空闲 。 gRPC 如何处理关闭频道是语言相关的。有些语言可允许询问频道状态。

安全认证

gRPC 被设计成可以利用插件的形式支持多种授权机制。

支持的授权机制

SSL/TLS

gRP 集成 SSL/TLS 并对服务端授权所使用的 SSL/TLS 进行了改良,对客户端和服务端交换的所有数据进行了加密。对客户端来讲提供了可选的机制提供凭证来获得共同的授权。

OAuth 2.0

gRPC 提供通用的机制(后续进行描述)来对请求和应答附加基于元数据的凭证。当通过 gRPC 访问 Google API 时,会为一定的授权流程提供额外的获取访问令牌的支持,这将通过以下代码例子进行展示。 警告:Google OAuth2 凭证应该仅用于连接 Google 的服务。把 Google 对应的 OAuth2 令牌发往非 Google 的服务会导致令牌被窃取用作冒充客户端来访问 Google 的服务。

API

服务端认证加密使用的 SSL/TLS

通过 Google 进行认证

扩展 gRPC 支持其他的认证机制

HTTP2 协议上的 gRPC

大纲

以下是 gRPC 请求和应答消息流中一般的消息顺序:

  • 请求 → 请求报头 *有定界符的消息 EOS
  • 应答 → 应答报头 *有定界符的消息 EOS
  • 应答 → (应答报头 *有定界符的消息 跟踪信息) / 仅仅跟踪时

请求

  • 请求 → 请求报头 *界定的消息 EOS 请求报头是通过报头+联系帧方式以 HTTP2 报头来发送的。

  • 请求报头 → 调用定义 *自定义元数据

  • 调用定义 → 方法模式路径TE [授权] [超时] [内容类型] [消息类型] [消息编码] [接受消息类型] [用户代理]

  • 方法 → “:method POST”

  • 模式 → “:scheme ” (“http” / “https”)

  • 路径 → “:path” {开放的 API 对应的方法路径}

  • Authority → “:authority” {授权的对应的虚拟主机域名}

  • TE → “te” “trailers” # 用来检测不兼容的代理

  • 超时 → “grpc-timeout” 超时时间值 超时时间单位

  • 超时时间值 → {至少8位数字正整数的 ASCII 码字符串}

  • 超时时间单位 → 时 / 分 / 秒 / 毫秒 / 微秒 / 纳秒

  • → “H”

  • → “M”

  • → “S”

  • 毫秒 → “m”

  • 微秒 → “u”

  • 纳秒 → “n”

  • 内容类型 → “content-type” “application/grpc” [(“+proto” / “+json” / {自定义})]

  • 内容编码 → “gzip” / “deflate” / “snappy” / {自定义}

  • 消息编码 → “grpc-encoding” Content-Coding

  • 接受消息编码 → “grpc-accept-encoding” Content-Coding *(“,” Content-Coding)

  • 用户代理 → “user-agent” {结构化的用户代理字符串}

  • 消息类型 → “grpc-message-type” {消息模式的类型名}

  • 自定义数据 → 二进制报头 / ASCII 码报头

  • 二进制报头 → {以“-bin”结尾小写的报头名称的 ASCII 码 } {以 base64 进行编码的值}

  • ASCII 码报头 → {小写报头名称的 ASCII 码} {}

HTTP2 需要一个在其他报头之前以“:”开始的保留报头。额外的实现应该在保留报头后面马上传送超时信息,并且应该在发送自定义元数据前发送调用定义报头。 如果超时信息被遗漏,服务端会认为是无限时长的超时。客户端实现可以根据发布需要自由地发送一个默认最小超时时间。 自定义元数据是应用层定义的任意的键值对集合。除了 HTTP2 报头部总长度的传输限制外,唯一的约束就是以“grpc-”开始的报头名称是为将来使用保留的。

注意 HTTP2 并不允许随意使用字节序列来作为报头值,所以二进制的报头值必须使用 Base64 来编码,参见https://tools.ietf.org/html/rfc4648#section-4。 实现必须接受填充的和非填充的值,并且发出非填充的值。应用以“-bin”结尾的名称来定义二进制报头。运行时库在报头被发送和接收时,用这个后缀来检测二进制报头并且正确地在报头被发送和接收时进行 Base64 编码和解码。

界定的消息的重复序列通过数据帧来进行传输。

  • 界定的消息 → 压缩标志 消息长度 消息

  • 压缩标志 → 0 / 1 # 编码为 1 byte 的无符号整数

  • 消息长度 → {消息长度} # 编码为 4 byte 的无符号整数

  • 消息 → *{二进制字节}

压缩标志 值为1 表示消息的二进制序列通过消息编码报头声明的机制进行压缩,为0表示消息的字节码没有进行编码。压缩上下文不在消息编辑间维护,声明必须为流中的每个消息创建一个新的上下文。假如 压缩标志 被遗漏了,那么压缩标志 必须为0。

对请求来讲,EOS (end-of-stream)以最后接收到的数据帧出现 END_STREAM 标志为准。 在请求流需要关闭但是没有数据继续发送的情况下,代码必须发送包含这个标志的空数据帧。

应答

  • 应答 → (应答报头 界定的消息 跟踪信息) / 仅仅跟踪

  • 应答报头 → HTTP 状态 [消息编码] [消息接受编码] 内容类型 *自定义元数据

  • 仅仅跟踪 → HTTP 状态 内容类型 跟踪消息

  • 跟踪消息 → 状态 [状态消息] *自定义元数据

  • HTTP状态 → “:status 200”

  • 状态 → “grpc-status” <状态码的 ASCII 字符串>

  • 状态消息 → “grpc-message” <状态描述文本对应的 ASCII 字符串>

应答报头仅仅跟踪 分别在一个HTTP2报头帧块里发送。大多数应答期望既有报头又有跟踪消息,但是调用允许仅仅跟踪生成一个立即的错误。假如状态码是 OK 的话,则必须在跟踪消息里发送状态。 对于应答来讲,通过在最后一个接收的包含跟踪信息的报头帧里提供一个 END_STREAM 标志来表明流结束。

实现应当会让中断的部署在应答里发送一个非200的HTTP状态码和一系列非GRPC内容类型并且省略状态状态消息。 当发生这种情况时实现应当合成状态状态消息来扩散到应用层。

例子

单项调用HTTP2帧序列例子

请求


HEADERS (flags = END_HEADERS)

:method = POST

:scheme = http

:path = /google.pubsub.v2.PublisherService/CreateTopic

:authority = pubsub.googleapis.com

grpc-timeout = 1S

content-type = application/grpc+proto

grpc-encoding = gzip

authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v




DATA (flags = END_STREAM)

<Delimited Message>

应答


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

用户代理

当协议不需要一个用户代理时,建议客户端提供一个结构化的用户代理字符串来对要调用的库、版本和平台提供一个基本的描述来帮助在异质的环境里进行问题诊断。库开发者建议使用以下结构:


User-Agent → “grpc-” Language ?(“-” Variant) “/” Version ?( “ (“  *(AdditionalProperty “;”) “)” )

例如


grpc-java/1.2.3

grpc-ruby/1.2.3

grpc-ruby-jruby/1.3.4

grpc-java-android/0.9.1 (gingerbread/1.2.4; nexus5; tmobile)

HTTP2 传输映射

流识别

所有的 GRPC 调用需要定义指定一个内部 ID。我们将在这个模式里使用 HTTP2 流 ID 来作为调用标识。注意:这些 ID 在一个打开的 HTTP2 会话里是前后关联的,在一个处理多个 HTTP2 会话的进程里不是唯一的,也不能被用作 GUID。

数据帧

数据帧边界与界定消息的边界无关,实现时不应假定它们有一致性。

错误

当应用错误或运行时错误在 PRC 调用过程中出现时,状态状态消息应当通过跟踪消息发送。

在有些情况下可能消息流的帧已经中断,RPC 运行时会选择使用 RST_STREAM 帧来给对方表示这种状态。RPC 运行时声明应当将 RST_STREAM 解释为流的完全关闭,并且将错误传播到应用层。 以下为从 RST_STREAM 错误码到 GRPC 的错误码的映射:

HTTP2 编码GRPC 编码
NO_ERROR(0)INTERNAL -一个显式的GRPC OK状态应当被发出,但是这个也许在某些场景里会被侵略性地使用
PROTOCOL_ERROR(1)INTERNAL
INTERNAL_ERROR(2)INTERNAL
FLOW_CONTROL_ERROR(3)INTERNAL
SETTINGS_TIMEOUT(4)INTERNAL
STREAM_CLOSED无映射,因为没有打开的流来传播。实现应记录。
FRAME_SIZE_ERRORINTERNAL
REFUSED_STREAMUNAVAILABLE-表示请求未作处理且可以重试,可能在他处重试。
CANCEL(8)当是由客户端发出时映射为调用取消,当是由服务端发出时映射为 CANCELLED。注意服务端在需要取消调用时应仅仅使用这个机制,但是有效荷载字节顺序是不完整的
COMPRESSION_ERRORINTERNAL
CONNECT_ERRORINTERNAL
ENHANCE_YOUR_CALMRESOURCE_EXHAUSTED…并且运行时提供有额外的错误详情,表示耗尽资源是带宽
INADEQUATE_SECURITYPERMISSION_DENIED… 并且有额外的信息表明许可被拒绝,因为对调用来说协议不够安全

安全

HTTP2 规范当使用 TLS 时强制使用 TLS 1.2 及以上的版本,并且在部署上对允许的密码施加一些额外的限制以避免已知的比如需要 SNI 支持的问题。并且期待 HTTP2 与专有的传输安全机制相结合,这些传输机制的规格说明不能提供有意义的建议。

连接管理

GOAWAY 帧

服务端发出这种帧给客户端表示服务端在相关的连接上不再接受任何新流。这种帧包含服务端最后成功接受的流的ID。客户端应该认为任何在最后成功的流后面初始化的任意流为 UNAVAILABLE,并且在别处重试这些调用。客户端可以自由地在已经接受的流上继续工作直到它们完成或者连接中断。 服务端应该在终止连接前发送 GOAWAY 帧,以可靠地通知客户端哪些工作已经被服务端接受并执行。

PING 帧

客户端和服务端均可以发送一个 PING 帧,对方必须精确回显它们所接收到的信息。这可以被用来确认连接仍然是活动的,并且能够提供估计端对端延迟估计的方法。假如服务端初始的 PING 在最后期限仍然没有收到运行时所期待的应答的话,所有未完成的调用将会被以取消状态关闭。一个客户端期满的初始的PING则会导致所有的调用被以用不可用状态关闭。注意PING的频率高度依赖于网络环境,实现可以根据网络和应用需要,自由地调整PING频率。

连接失败

假如客户端检测到连接失败,所有的调用都会被以不可用状态关闭。而服务端侧则所有已经打开的调用都会被以取消状态关闭。

附录 A - Protobuf 上的 GRPC

用 protobuf 定义的服务接口可以通过 protoc 的代码生成扩展简单地映射成 GRPC ,以下定义了所用的映射:

  • 路径 → / 服务名 / {方法名}

  • 服务名 → ?( {proto 包名} “.” ) {服务名}

  • 消息类型 → {全路径 proto 消息名}

  • 内容类型 → “application/grpc+proto”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值