跳到主要内容

grpc-web

浏览器网络 API 不公开原生 gRPC 所需的 HTTP/2 帧,因此浏览器无法直接调用原生 gRPC 服务。gRPC-Web 定义了浏览器兼容的协议,支持一元调用和服务端流式调用。

grpc-web 插件将 gRPC-Web 请求转换为原生 gRPC 调用并转发到上游 gRPC 服务。插件还会将响应转换回 gRPC-Web 格式,以供浏览器应用使用。


请求处理​

插件接受用于 RPC 调用的 POST 请求和用于 CORS 预检的 OPTIONS 请求,并识别以下内容类型:

  • application/grpc-web
  • application/grpc-web-text
  • application/grpc-web+proto
  • application/grpc-web-text+proto

官方 gRPC-Web 浏览器客户端的二进制模式支持一元调用,而 Base64 编码的文本模式支持一元调用和服务端流式调用。插件解码请求,将其作为原生 gRPC 转发,并按照客户端请求的格式编码上游响应。

对于跨域请求,插件默认允许所有来源和 POST 请求。插件接受 content-type、x-grpc-web 和 x-user-agent 请求头,并公开 grpc-status 和 grpc-message 响应头。使用 cors_allow_headers 可扩展允许的请求头。

有关协议层面的详细信息,请参阅 gRPC-Web 协议规范和浏览器功能支持。

示例​

以下示例配置浏览器客户端,通过网关向官方 gRPC-Web Echo 服务发送一元请求和服务端流式请求。

运行 APISIX 或 API7 网关。Admin API 和 ADC 示例使用 Docker;Kubernetes 示例使用已部署 API7 Ingress Controller 的 Kubernetes 集群。

在 PATH 中安装 Protocol Buffers 编译器 35.0 和 protoc-gen-grpc-web 2.0.2。如果使用 ADC 配置,请安装 ADC 0.30.4 或更高版本。

客户端使用 2.0.2 版本中的官方 gRPC-Web Echo protobuf 和服务器实现。确认编译器和 gRPC-Web 生成器可用:

protoc --version
protoc-gen-grpc-web --version

命令应分别输出 libprotoc 35.0 和 protoc-gen-grpc-web 2.0.2。

启动 Echo 服务​

为 Echo 服务创建目录,并从固定的 gRPC-Web 发布提交中下载 protobuf 和服务器实现:

mkdir grpc-web-echo
cd grpc-web-echo

curl -fLO "https://raw.githubusercontent.com/grpc/grpc-web/9e6bf0f521ebeecf7cdde62ee10da289ef8d9b0a/net/grpc/gateway/examples/echo/echo.proto"
curl -fLo server.js "https://raw.githubusercontent.com/grpc/grpc-web/9e6bf0f521ebeecf7cdde62ee10da289ef8d9b0a/net/grpc/gateway/examples/echo/node-server/server.js"

创建一个 Dockerfile,为主机架构构建服务并固定其运行时依赖版本:

Dockerfile
FROM node:24.13.1-alpine

WORKDIR /app/node-server

COPY echo.proto /app/echo.proto
COPY server.js ./server.js

RUN npm init -y && \
npm install --omit=dev --save-exact \
@grpc/grpc-js@1.14.4 \
@grpc/proto-loader@0.8.1 \
async@3.2.6 \
lodash@4.18.1

EXPOSE 9090

CMD ["node", "server.js"]

构建镜像:

docker build -t grpc-web-echo:2.0.2 .

在网关所使用的环境中启动服务:

将 GATEWAY_CONTAINER 设置为正在运行的 APISIX 或 API7 网关容器。创建专用网络并将网关连接到该网络:

export GATEWAY_CONTAINER=replace-with-gateway-container-name

docker network create gateway-grpc-web-net
docker network connect gateway-grpc-web-net "$GATEWAY_CONTAINER"

在同一网络中启动 Echo 服务:

docker run -d --name grpc-web-echo \
--network gateway-grpc-web-net \
grpc-web-echo:2.0.2

生成浏览器客户端​

返回包含 echo.proto 的目录。初始化客户端项目并安装固定版本的代码生成和运行时依赖:

npm init -y
npm install --save-exact grpc-web@2.1.1 google-protobuf@4.0.2
npm install --save-dev --save-exact \
@protocolbuffers/protoc-gen-js@4.0.2 \
esbuild@0.25.9

生成 protobuf 消息类和 gRPC-Web 客户端存根。本示例中的服务端流式请求需要使用文本模式:

protoc -I=. \
--plugin=protoc-gen-js=./node_modules/.bin/protoc-gen-js \
--js_out=import_style=commonjs:. \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:. \
echo.proto

创建浏览器客户端:

client.js
const { EchoServiceClient } = require('./echo_grpc_web_pb');
const { EchoRequest, ServerStreamingEchoRequest } = require('./echo_pb');

const output = document.querySelector('#output');
const log = (message) => {
output.textContent += `${message}\n`;
};

const client = new EchoServiceClient('http://127.0.0.1:9080/grpc/web');

const unaryRequest = new EchoRequest();
unaryRequest.setMessage('hello from unary');

client.echo(unaryRequest, {}, (error, response) => {
if (error) {
log(`unary error: ${error.message}`);
return;
}
log(`unary: ${response.getMessage()}`);
});

const streamRequest = new ServerStreamingEchoRequest();
streamRequest.setMessage('hello from stream');
streamRequest.setMessageCount(3);
streamRequest.setMessageInterval(10);

const stream = client.serverStreamingEcho(streamRequest, {});
stream.on('data', (response) => log(`stream: ${response.getMessage()}`));
stream.on('status', (status) => log(`stream status: ${status.code}`));
stream.on('error', (error) => log(`stream error: ${error.message}`));

创建用于显示响应的页面:

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>gRPC-Web example</title>
</head>
<body>
<pre id="output"></pre>
<script src="bundle.js"></script>
</body>
</html>

打包浏览器客户端:

./node_modules/.bin/esbuild client.js --bundle --outfile=bundle.js

使用前缀路由代理 gRPC-Web​

前缀路由可通过一条网关路由代理 Echo 服务中的所有方法。请为网关环境配置路由:

curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-web-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/grpc/web/*",
"plugins": {
"grpc-web": {}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-web-echo:9090": 1
}
}
}'

❶ 匹配浏览器客户端所用基础路径下的每个 RPC 路径。

❷ 启用 gRPC-Web 转换。

❸ 使用原生 gRPC 连接上游。

❹ 将请求发送到共享 Docker 网络中的 Echo 服务。

/grpc/web/* 路由会先移除匹配的前缀,再转发原生 gRPC 方法路径。例如,/grpc/web/grpc.gateway.testing.EchoService/Echo 会以 /grpc.gateway.testing.EchoService/Echo 转发。使用 /grpc/* 之类更宽泛的路由会在上游路径中留下 web/,从而导致 unknown service 响应。

提供客户端目录,并在浏览器中打开 http://127.0.0.1:8088:

python3 -m http.server 8088

页面应显示一元调用结果和三条流式消息,显示顺序可能不同:

unary: hello from unary
stream: hello from stream
stream: hello from stream
stream: hello from stream
stream status: 0

使用绝对路由代理单个 gRPC-Web 方法​

仅需通过网关公开一个 RPC 方法时,请使用绝对路由。由于浏览器客户端会在原生 gRPC 方法路径前添加 /grpc/web,因此 proxy-rewrite 会在请求到达 Echo 服务前移除该基础路径。

此示例替换前缀路由,并且仅公开一元 Echo 方法。将浏览器客户端基础 URL 保持为 http://127.0.0.1:9080/grpc/web。

curl "http://127.0.0.1:9180/apisix/admin/routes/grpc-web-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"uri": "/grpc/web/grpc.gateway.testing.EchoService/Echo",
"plugins": {
"grpc-web": {},
"proxy-rewrite": {
"uri": "/grpc.gateway.testing.EchoService/Echo",
"set_ngx_uri": true
}
},
"upstream": {
"scheme": "grpc",
"type": "roundrobin",
"nodes": {
"grpc-web-echo:9090": 1
}
}
}'

❶ 匹配一元 Echo 方法面向浏览器的路径。

❷ 将请求重写为上游服务所需的原生 gRPC 方法路径。

❸ 重写后,更新 gRPC-Web 插件使用的 NGINX URI。

在浏览器中刷新 http://127.0.0.1:8088。Echo 响应应成功。流式请求应失败,因为绝对路由中未包含其方法路径。