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 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 编译器及相关插件。
安装
protoc和proto-grpc-*插件。在编写客户端和服务端程序前,需要在系统中安装 Protocol Buffer 编译器
protoc和 用于生成.proto的 Go、JavaScript、gRPC Web 接口代码的protoc-gen-go和protoc-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创建
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 }
配置服务端程序
生成服务端 Go 原始消息和服务/客户端存根。
1protoc -I./a6 echo.proto --go_out=plugins=grpc:./a6实现服务端处理程序接口。
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 }服务端程序运行入口文件。
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 }编译并启动服务端服务。
1go build -o grpc-server server.go 2./grpc-server
配置客户端程序
生成客户端
proto代码生成客户端 JavaScript 原始消息、服务/客户端存根和 gRPC Web 的 JavaScript 的接口代码。 gRPC Web 的
proto插件提供了两种代码生成模式:- mode=grpcwebtext: 默认生成的代码以 grpc-web-text 格式发送 payload
- Content-type: application/grpc-web-text
- Payload 使用 base64 编码
- 支持一元和服务器流式调用
- 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$ npm i grpc-web 2$ npm i google-protobuf客户端执行入口文件
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 });最终项目结构
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 请求的代理。
启用
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 }'验证 gRPC Web 代理请求
通过 Node 执行
client.js即可向 Apache APISIX 发送 gRPC Web 协议请求。 上述客户端和服务端的处理逻辑分别是:客户端向服务端发送一条消息内容是hello,服务端收到消息后响应response: hello,执行结果如下。1$ node client.js 2response: hello关闭
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 中发起讨论,或通过邮件列表进行交流。