HomeBlogProjects

使用 gRPC 与前端交互

sorcererxw

gRPC 是服务端环境里面被广泛使用的通信协议,得益于 Protobuf IDL,我们可以快速生成 Client 和 Server Stub,大大提升开发效率,也避免了通信过程当中的类型错误问题。

那么我们是否也能够将 gRPC 使用到与前端的对接当中?答案显然是可以的,只需要客户端能够和服务端顺利建立 HTTP2 连接,就能在上面以 gRPC 协议完成数据交互。但是对于网页来说,虽然浏览器支持 HTTP/2,网页代码并不能强制指定某一个请求是走 HTTP/2 还是 HTTP/1.1,网络底层被浏览器屏蔽了,可以参考下面的这篇文章:

The state of gRPC in the browser
A high-performance, open source universal RPC framework
https://grpc.io/blog/state-of-grpc-web/#the-grpc-web-spec
How to implement HTTP/2 stream connection in browser?
Nowadays HTTP/2 is rising as of its performance. The recent version of Node.js supports HTTP/2 very well. https://nodejs.org/api/http2.html But I have no idea how to implement HTTP/2 client in the
https://stackoverflow.com/questions/52273174/how-to-implement-http-2-stream-connection-in-browser

所以如果希望将 gRPC 这套机制搬到浏览器上,就需要在中间再套一层协议转换层,以实现同时支持 HTTP/1.x 和 HTTP/2。

目前比较成熟的方案有 gRPC-Gateway 和 gRPC-Web:

gRPC-Gateway

gRPC-Gateway 是由社区主导的项目,它通过解析 Protobuf IDL 当中的 google.api.http 声明,为 gRPC 生成 http+JSON gateway server。用户只需要启动这个 gateway server,它就会接收请求并将其映射到相应的 gRPC 方法,并去调用相应的服务。

rpc GetUser(GetUserRequest) returns (GetUserResponse) {
  (google.api.http) = {
    get: "/v1/user/{id}"
  }
}; 

gRPC-Web

gRPC-Web 项目是由 gRPC 官方给出的,在浏览器当中使用 gRPC 的方案。它基于以下规范,在 gRPC 和 gRPC-Web 当中做转码:

grpc/PROTOCOL-WEB.md at master · grpc/grpc
The C based gRPC (C++, Python, Ruby, Objective-C, PHP, C#) - grpc/PROTOCOL-WEB.md at master · grpc/grpc
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md

不同于 gRPC-Gateway,gRPC-Web 不需要额外 IDL 声明,它的 server 依然是直接接收 gRPC 请求,只不过为了适应 HTTP/1.x 在数据编码和传输上做了一定的调整。

gRPC-Web 在客户端与服务端之间交互的 payload 可以是二进制形式,也可以通过 BASE64 编码。

curl -v 'http://localhost:8080/api.v1.API/GetInfo' \
  -H 'accept: application/grpc-web-text' \
  -H 'x-grpc-web: 1' \
  -H 'content-type: application/grpc-web-text' \
  --data-raw 'AAAAAD4KPGh0dHBzOi8vdHdpdHRlci5jb20vY29vbFhpYW8vc3RhdHVzLzE0ODIyNTQ3MjU3ODk1NDQ0NDg/cz0yMA==' \
  --compressed

*   Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> POST /api.v1.API/GetInfo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.77.0
> Accept-Encoding: deflate, gzip
> accept: application/grpc-web-text
> x-grpc-web: 1
> content-type: application/grpc-web-text
> Content-Length: 92
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Access-Control-Expose-Headers: Content-Type, Vary, Date, grpc-status, grpc-message
< Content-Type: application/grpc-web-text
< Vary: Origin
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
AAAAARSKAZACCgpoYXBweSB4aWFvEglAY29vbFhpYW8aRWh0dHBzOi8vcGJzLnR3aW1nLmNvbS9wcm9maWxlX2ltYWdlcy8xMzk4MTQ1NTE0NjY3ODQzNTg2Ly1rNHRheHJMLmpwZyKnAei/meS4que9keermeaPkOS+m+WFjei0ueeahCBtYWNPUyDlupTnlKjlm77moIcKCmh0dHBzOi8vdC5jby9Wd0p2RWVPQndnCgrmiJHmkJzkuobkuIDkuIsgT2JzaWRpYW4g5ZKMIE5vdGlvbgrmnInkupvnmoTnoa7mr5Tljp/phY3nmoTlpb3nnIvwn5mCIGh0dHBzOi8vdC5jby83a1lLSXozTzg3KgYI6PGJjwY=gAAAABBncnBjLXN0YXR1czogMA0K%

真是因为直接传递二进制的原因,会导致我们在开发的时候难以调试,光光通过抓包无法理解 payload 当中的内容,一定程度上降低了开发的效率。

不过因为编解码 payload 都强依赖 proto,也在很大程度上避免自己的接口被他人直接使用。使用下面这个工具可以直接查看 proto 内容,不过无法知道各个字段的意义,聊胜于无。

Protobuf Decoder
https://protobuf-decoder.netlify.app/

在 HTTP/1.x 实现 Streaming

不同于 HTTP/2 是一个全双工的网络协议,在 HTTP/1.x 当中服务端并不能主动推送数据给客户端。所以,有两种方式可以实现:

  • Websocket

    服务接收到请求的时候,直接和客户端协商升级协议到 Websocket,之后就可以自由地传输数据了。

  • Payload Chunk

    在一个 response 下发多条记录,记录之间使用 boundary 隔开,这样就可以模拟出服务端流了。

    就如我之前介绍的 SSE 也是通过这种方式下发数据,只是通过统一的标准,浏览器在上层又做了一层封装。

在 gRPC-Gateway 和 gRPC-Web 当中,为了实现 Server Streaming,它们都使用了 Transfer Encoding Chunk 的方案,他们会在 response 加上 header Transfer-Encoding: chunked ,这样客户端就会根据 boundary 分片接收服务端的数据了。

总结

gRPC-GatewaygRPC-Web
Unary
Server Streaming⚠️(需要使用 Base64 编码)
Client Streaming⚠️(需要使用转发模式)
Bi-directional Streaming
默认编码JSONProtocol Buffers + Base64
场景同时支持 rest 和 grpc 两套接口,使既有系统可以从 rest 平滑迁移到 grpc前后端直接使用 gRPC 协议交互,使用 gRPC-Web 为 HTTP/1.x 提供支持