Apache APISIX 新技能,代理 gRPC-Web 请求

帅进超

更新时间 1/25/2022

gRPC Web 背景介绍

gRPC 最初由谷歌开发,是一个基于 HTTP/2 实现的高性能远程过程调用框架。但由于浏览器没有直接暴露 HTTP/2,所以 Web 应用程序不能直接使用 gRPC。gRPC Web 是一个标准化协议,它解决了这个问题。

第一个 gRPC-web 实现是在 2018 年作为一个 JavaScript 库发布的,Web 应用程序可以通过它直接与 gRPC 服务通信。其原理是创建与 HTTP/1.1 和 HTTP/2 兼容的端到端 gRPC 管道,然后浏览器发送常规的 HTTP 请求,位于浏览器和服务器之间的 gRPC-Web 代理对请求和响应进行转换。与 gRPC 类似,gRPC Web 在 Web 客户端和后端 gRPC 服务之间使用预定义的契约。Protocol Buffers 被用来序列化和编码消息。

gRPC-web 工作原理

有了 gRPC Web,用户可以使用浏览器或 Node 客户端直接调用后端的 gRPC 应用程序。不过,在浏览器端使用 gRPC-Web 调用 gRPC 服务也存在一些限制:

  • 不支持客户端流和双向流调用。
  • 跨域调用 gRPC 服务需要在服务器端配置 CORS。
  • gRPC 服务器端必须配置为支持 gRPC-Web,或者必须有第三方服务代理在浏览器和服务器之间对调用进行转换。

Apache APISIX gRPC Web Proxy

Apache APISIX 通过插件的方式支持 gRPC Web 协议的代理,在 grpc-web 插件中完成了 gRPC Web与 gRPC Server 通讯时的协议转换及数据编解码工作,其通讯的过程如下:

gRPC Web Client -> Apache APISIX(protocol conversion & data codec) -> gRPC server

接下来通过一个完整的示例向大家演示怎样构建一个 gRPC Web 客户端,并通过 Apache APISIX 进行 gRPC Web 请求的代理。在以下的示例中,我们会将 Go 作为 gRPC Server 服务端处理程序,Node 作为 gRPC Web 客户端请求程序。

配置 Protocol Buffer

首先进行第一步,安装 Protocol Buffer 编译器及相关插件。

  1. 安装 protocproto-grpc-* 插件。

    在编写客户端和服务端程序前,需要在系统中安装 Protocol Buffer 编译器 protoc 和 用于生成 .proto 的 Go、JavaScript、gRPC Web 接口代码的 protoc-gen-goprotoc-gen-grpc-web 插件。

    请运行以下脚本,安装上述组件。

    1#!/usr/bin/env bash
    2
    3 set -ex
    4
    5 PROTOBUF_VERSION="3.19.0"
    6 wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    7 unzip protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    8 mv bin/protoc /usr/local/bin/protoc
    9 mv include/google /usr/local/include/
    10 chmod +x /usr/local/bin/protoc
    11
    12 PROTO_GO_PLUGIN_VER="1.2.0"
    13 wget https://github.com/grpc/grpc-go/releases/download/cmd/protoc-gen-go-grpc/v${PROTO_GO_PLUGIN_VER}/protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz
    14 tar -zxvf protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz
    15 mv protoc-gen-go-grpc /usr/local/bin/protoc-gen-go
    16 chmod +x /usr/local/bin/protoc-gen-go
    17
    18 PROTO_JS_PLUGIN_VER="1.3.0"
    19 wget https://github.com/grpc/grpc-web/releases/download/${PROTO_JS_PLUGIN_VER}/protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64
    20 mv protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64 /usr/local/bin/protoc-gen-grpc-web
    21 chmod +x /usr/local/bin/protoc-gen-grpc-web
  2. 创建 SayHello 示例 proto 文件。

    1 // a6/echo.proto
    2
    3 syntax = "proto3";
    4
    5 package a6;
    6
    7 option go_package = "./;a6";
    8
    9 message EchoRequest {
    10 string message = 1;
    11 }
    12
    13 message EchoResponse {
    14 string message = 1;
    15 }
    16
    17 service EchoService {
    18 rpc Echo(EchoRequest) returns (EchoResponse);
    19 }

配置服务端程序

  1. 生成服务端 Go 原始消息和服务/客户端存根。

    1protoc -I./a6 echo.proto --go_out=plugins=grpc:./a6
  2. 实现服务端处理程序接口。

    1// a6/echo.impl.go
    2
    3 package a6
    4
    5 import (
    6 "errors"
    7 "golang.org/x/net/context"
    8 )
    9
    10 type EchoServiceImpl struct {
    11 }
    12
    13 func (esi *EchoServiceImpl) Echo(ctx context.Context, in *EchoRequest) (*EchoResponse, error) {
    14 if len(in.Message) <= 0 {
    15     return nil, errors.New("message invalid")
    16 }
    17 return &EchoResponse{Message: "response: " + in.Message}, nil
    18 }
  3. 服务端程序运行入口文件。

    1// server.go
    2 package main
    3
    4 import (
    5 "fmt"
    6 "log"
    7 "net"
    8
    9 "apisix.apache.org/example/a6"
    10 "google.golang.org/grpc"
    11 )
    12
    13 func main() {
    14 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 50001))
    15 if err != nil {
    16     log.Fatalf("failed to listen: %v", err)
    17 }
    18
    19 grpcServer := grpc.NewServer()
    20 a6.RegisterEchoServiceServer(grpcServer, &a6.EchoServiceImpl{})
    21
    22 if err = grpcServer.Serve(lis); err != nil {
    23     log.Fatalf("failed to serve: %s", err)
    24 }
    25 }
  4. 编译并启动服务端服务。

    1go build -o grpc-server server.go
    2./grpc-server
    

配置客户端程序

  1. 生成客户端 proto 代码

    生成客户端 JavaScript 原始消息、服务/客户端存根和 gRPC Web 的 JavaScript 的接口代码。 gRPC Web 的 proto 插件提供了两种代码生成模式:

    1. mode=grpcwebtext: 默认生成的代码以 grpc-web-text 格式发送 payload
    • Content-type: application/grpc-web-text
    • Payload 使用 base64 编码
    • 支持一元和服务器流式调用
    1. mode=grpcweb:以二进制 protobuf 格式发送 payload
    • Content-type: application/grpc-web+proto
    • Payload 采用二进制 protobuf 格式
    • 目前仅支持一元调用
1$ protoc -I=./a6 echo.proto --js_out=import_style=commonjs:./a6 --grpc-web_out=import_style=commonjs,mode=grpcweb:./a6
  1. 安装客户端依赖库

    1$ npm i grpc-web
    2$ npm i google-protobuf
  2. 客户端执行入口文件

    1// client.js
    2const {EchoRequest} = require('./a6/echo_pb');
    3const {EchoServiceClient} = require('./a6/echo_grpc_web_pb');
    4// 连接到 Apache APISIX 的入口
    5let echoService = new EchoServiceClient('http://127.0.0.1:9080');
    6
    7let request = new EchoRequest();
    8request.setMessage("hello")
    9
    10echoService.echo(request, {}, function (err, response) {
    11    if (err) {
    12         console.log(err.code);
    13         console.log(err.message);
    14     } else {
    15         console.log(response.getMessage());
    16     }
    17 });
  3. 最终项目结构

    1$ tree .
    2 ├── a6
    3 │   ├── echo.impl.go
    4 │   ├── echo.pb.go
    5 │   ├── echo.proto
    6 │   ├── echo_grpc_web_pb.js
    7 │   └── echo_pb.js
    8 ├── client.js
    9 ├── server.go
    10 ├── go.mod
    11 ├── go.sum
    12 ├── package.json
    13 └── package-lock.json
    

完成上述的步骤之后,你已经配置了把 gRPC Server 的服务端程序和 gRPC Web 的客户端程序,并且启动了服务端程序,它将通过 50001 端口接收请求。

配置 Apache APISIX

接下来只需在 Apache APISIX 路由的插件配置中启用 grpc-web 插件,即可进行 gRPC Web 请求的代理。

  1. 启用 grpc-web 代理插件

    启用 grpc-web 代理插件,路由必须使用前缀匹配模式(例如:/* 或 /grpc/example/*), 因 gRPC Web 客户端会在 URI 中传递 proto 中声明的包名称、服务接口名称、方法名称等信息(例如:/path/a6.EchoService/Echo),使用绝对匹配时会使插件无法从 URI 中提取 proto 信息

    1$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    2 {
    3     "uri":"/*", // 前缀匹配模式
    4     "plugins":{
    5         "grpc-web":{} //开启 gRPC Web 代理
    6     },
    7     "upstream":{
    8         "scheme":"grpc",
    9         "type":"roundrobin",
    10         "nodes":{
    11             "127.0.0.1:50001":1 // gRPC Server Listen 地址和端口
    12         }
    13     }
    14 }'
    
  2. 验证 gRPC Web 代理请求

    通过 Node 执行 client.js 即可向 Apache APISIX 发送 gRPC Web 协议请求。 上述客户端和服务端的处理逻辑分别是:客户端向服务端发送一条消息内容是 hello,服务端收到消息后响应 response: hello,执行结果如下。

    1$ node client.js
    2response: hello
    
  3. 关闭 grpc-web 代理插件

    只需将路由插件配置中的 grpc-web 属性移除即可。

    1$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    2 {
    3     "uri":"/*",
    4     "plugins":{
    5     },
    6     "upstream":{
    7         "scheme":"grpc",
    8         "type":"roundrobin",
    9         "nodes":{
    10             "127.0.0.1:50001":1
    11         }
    12     }
    13 }'
    

总结

本文为大家带来了 Apache APISIX 的 grpc-web 插件讲解及实战案例。

欢迎随时在 GitHub Discussions 中发起讨论,或通过邮件列表进行交流。