Bootstrap

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

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

  • 不支持客户端流和双向流调用。

  • 跨域调用 gRPC 服务需要在服务器端配置 CORS。

  • gRPC 服务器端必须配置为支持 gRPC-Web,或者必须有第三方服务代理在浏览器和服务器之间对调用进行转换。

Apache APISIX gRPC Web Proxy

Apache APISIX 通过插件的方式支持 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 编译器及相关插件。

   #!/usr/bin/env bash
   
    set -ex
   
    PROTOBUF_VERSION="3.19.0"
    wget https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    unzip protoc-${PROTOBUF_VERSION}-linux-x86_64.zip
    mv bin/protoc /usr/local/bin/protoc
    mv include/google /usr/local/include/
    chmod +x /usr/local/bin/protoc
   
    PROTO_GO_PLUGIN_VER="1.2.0"
    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
    tar -zxvf protoc-gen-go-grpc.v${PROTO_GO_PLUGIN_VER}.linux.amd64.tar.gz
    mv protoc-gen-go-grpc /usr/local/bin/protoc-gen-go
    chmod +x /usr/local/bin/protoc-gen-go
   
    PROTO_JS_PLUGIN_VER="1.3.0"
    wget https://github.com/grpc/grpc-web/releases/download/${PROTO_JS_PLUGIN_VER}/protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64
    mv protoc-gen-grpc-web-${PROTO_JS_PLUGIN_VER}-linux-x86_64 /usr/local/bin/protoc-gen-grpc-web
    chmod +x /usr/local/bin/protoc-gen-grpc-web

    // a6/echo.proto
   
    syntax = "proto3";
   
    package a6;
   
    option go_package = "./;a6";
   
    message EchoRequest {
    string message = 1;
    }
   
    message EchoResponse {
    string message = 1;
    }
   
    service EchoService {
    rpc Echo(EchoRequest) returns (EchoResponse);
    }

配置服务端程序

   protoc -I./a6 echo.proto --go_out=plugins=grpc:./a6

   // a6/echo.impl.go
   
    package a6
   
    import (
    "errors"
    "golang.org/x/net/context"
    )
   
    type EchoServiceImpl struct {
    }
   
    func (esi *EchoServiceImpl) Echo(ctx context.Context, in *EchoRequest) (*EchoResponse, error) {
    if len(in.Message) <= 0 {
        return nil, errors.New("message invalid")
    }
    return &EchoResponse{Message: "response: " + in.Message}, nil
    }

   // server.go
    package main
   
    import (
    "fmt"
    "log"
    "net"
   
    "apisix.apache.org/example/a6"
    "google.golang.org/grpc"
    )
   
    func main() {
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 50001))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
   
    grpcServer := grpc.NewServer()
    a6.RegisterEchoServiceServer(grpcServer, &a6.EchoServiceImpl{})
   
    if err = grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %s", err)
    }
    }

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

配置客户端程序

   $ protoc -I=./a6 echo.proto --js_out=import_style=commonjs:./a6 --grpc-web_out=import_style=commonjs,mode=grpcweb:./a6

   $ npm i grpc-web
   $ npm i google-protobuf

   // client.js
   const {EchoRequest} = require('./a6/echo_pb');
   const {EchoServiceClient} = require('./a6/echo_grpc_web_pb');
   // 连接到 Apache APISIX 的入口
   let echoService = new EchoServiceClient('http://127.0.0.1:9080');
   
   let request = new EchoRequest();
   request.setMessage("hello")
   
   echoService.echo(request, {}, function (err, response) {
       if (err) {
            console.log(err.code);
            console.log(err.message);
        } else {
            console.log(response.getMessage());
        }
    });

   $ tree .
    ├── a6
    │   ├── echo.impl.go
    │   ├── echo.pb.go
    │   ├── echo.proto
    │   ├── echo_grpc_web_pb.js
    │   └── echo_pb.js
    ├── client.js
    ├── server.go
    ├── go.mod
    ├── go.sum
    ├── package.json
    └── package-lock.json

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

配置 Apache APISIX

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

   $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    {
        "uri":"/*", // 前缀匹配模式
        "plugins":{
            "grpc-web":{} //开启 gRPC Web 代理
        },
        "upstream":{
            "scheme":"grpc",
            "type":"roundrobin",
            "nodes":{
                "127.0.0.1:50001":1 // gRPC Server Listen 地址和端口
            }
        }
    }'

   $ node client.js
   response: hello

   $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
    {
        "uri":"/*",
        "plugins":{
        },
        "upstream":{
            "scheme":"grpc",
            "type":"roundrobin",
            "nodes":{
                "127.0.0.1:50001":1
            }
        }
    }'

总结

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

欢迎随时在 中发起讨论,或通过进行交流。