阶段四:gRPC
gRPC在微服务间表现出色,但浏览器和移动端不直接支持gRPC。gRPC网关( grpc-gateway)解决这个问题——它将RESTful HTTP请求翻译为gRPC调用,让同一套gRPC服务同时支持REST和gRPC两种访问方式。本课深入网关原理、grpc-gateway实现和gRPC-Web方案。
| 方案 | 原理 | 客户端要求 | 适用场景 |
|---|---|---|---|
| grpc-gateway | 反向代理REST→gRPC | 只需HTTP客户端 | 对外API、浏览器兼容 |
| gRPC-Web | 浏览器JS直接调用gRPC | grpc-web客户端库 | 前端SPA应用 |
| 自定义适配层 | 手动编写REST→gRPC转换 | HTTP客户端 | 高度定制需求 |
// grpc-gateway.js - Node.js实现的gRPC→REST网关
const express = require('express');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const app = express();
app.use(express.json());
// 加载Proto
const packageDef = protoLoader.loadSync('product.proto', {});
const productProto = grpc.loadPackageDefinition(packageDef).product;
// gRPC客户端
const grpcClient = new productProto.ProductService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// ===== REST → gRPC 路由映射 =====
// GET /api/products → ListProducts
app.get('/api/products', (req, res) => {
const { category, page, page_size } = req.query;
grpcClient.ListProducts({
category: category || '',
page: parseInt(page) || 1,
pageSize: parseInt(page_size) || 20,
}, (err, response) => {
if (err) {
return res.status(mapGrpcCode(err.code)).json({
success: false,
error: { code: 'GRPC_ERROR', message: err.message },
});
}
res.json({
success: true,
data: response.products,
pagination: { total: response.total, page: parseInt(page) || 1 },
});
});
});
// GET /api/products/:id → GetProduct
app.get('/api/products/:id', (req, res) => {
grpcClient.GetProduct({ id: parseInt(req.params.id) }, (err, product) => {
if (err) {
return res.status(mapGrpcCode(err.code)).json({
success: false,
error: { code: 'GRPC_ERROR', message: err.message },
});
}
res.json({ success: true, data: product });
});
});
// POST /api/products → CreateProduct
app.post('/api/products', (req, res) => {
grpcClient.CreateProduct({
name: req.body.name,
category: req.body.category,
price: req.body.price,
stock: req.body.stock,
}, (err, product) => {
if (err) {
return res.status(mapGrpcCode(err.code)).json({
success: false,
error: { code: 'GRPC_ERROR', message: err.message },
});
}
res.status(201).json({ success: true, data: product });
});
});
// GET /api/products/export → ExportProducts (服务端流→SSE)
app.get('/api/products/export', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = grpcClient.ExportProducts({ category: req.query.category || '' });
stream.on('data', (product) => {
res.write(`data: ${JSON.stringify(product)}\n\n`);
});
stream.on('end', () => {
res.write('data: [DONE]\n\n');
res.end();
});
stream.on('error', (err) => {
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
res.end();
});
});
// POST /api/products/import → ImportProducts (客户端流)
app.post('/api/products/import', (req, res) => {
const products = Array.isArray(req.body) ? req.body : [req.body];
const stream = grpcClient.ImportProducts((err, response) => {
if (err) {
return res.status(mapGrpcCode(err.code)).json({
success: false,
error: { code: 'GRPC_ERROR', message: err.message },
});
}
res.json({
success: true,
data: {
imported: response.successCount,
failed: response.failCount,
errors: response.errors,
},
});
});
products.forEach(product => stream.write({
name: product.name,
category: product.category,
price: product.price,
stock: product.stock,
}));
stream.end();
});
// gRPC错误码→HTTP状态码映射
function mapGrpcCode(grpcCode) {
const map = {
[grpc.status.OK]: 200,
[grpc.status.INVALID_ARGUMENT]: 400,
[grpc.status.NOT_FOUND]: 404,
[grpc.status.ALREADY_EXISTS]: 409,
[grpc.status.PERMISSION_DENIED]: 403,
[grpc.status.UNAUTHENTICATED]: 401,
[grpc.status.RESOURCE_EXHAUSTED]: 429,
[grpc.status.FAILED_PRECONDITION]: 412,
[grpc.status.ABORTED]: 409,
[grpc.status.OUT_OF_RANGE]: 400,
[grpc.status.UNIMPLEMENTED]: 501,
[grpc.status.INTERNAL]: 500,
[grpc.status.UNAVAILABLE]: 503,
[grpc.status.DEADLINE_EXCEEDED]: 504,
};
return map[grpcCode] || 500;
}
// 健康检查
app.get('/health', (req, res) => {
// 检查gRPC连接
grpcClient.ListProducts({ page: 1, pageSize: 1 }, (err) => {
res.json({
status: err ? 'degraded' : 'healthy',
grpc: err ? 'disconnected' : 'connected',
timestamp: new Date().toISOString(),
});
});
});
app.listen(8080, () => console.log('🌉 gRPC网关运行在 http://localhost:8080'));
# 先启动gRPC服务端
node grpc-server.js
# 再启动网关
node grpc-gateway.js
# 通过REST访问gRPC服务
curl -s http://localhost:8080/api/products | jq .
{
"success": true,
"data": [
{"id":1,"name":"机械键盘","category":"外设","price":599,"stock":120,"status":"active"},
{"id":2,"name":"无线鼠标","category":"外设","price":199,"stock":250,"status":"active"}
],
"pagination": {"total":5,"page":1}
}
# 创建商品
curl -s -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"蓝牙音箱","category":"音频","price":299,"stock":50}' | jq .
# 过滤
curl -s "http://localhost:8080/api/products?category=外设" | jq .
# SSE导出
curl -N http://localhost:8080/api/products/export
# 批量导入
curl -s -X POST http://localhost:8080/api/products/import \
-H "Content-Type: application/json" \
-d '[{"name":"商品A","price":100},{"name":"商品B","price":200}]' | jq .
// gRPC-Web允许浏览器直接调用gRPC服务
// 需要envoy或grpc-web代理
// envoy.yaml配置
/*
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: '0.0.0.0', port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.router
clusters:
- name: grpc_service
connect_timeout: 5s
type: logical_dns
hosts:
- socket_address: { address: 'localhost', port_value: 50051 }
*/
// 前端使用grpc-web客户端
/*
const { ProductServiceClient } = require('./product_grpc_web_pb');
const { GetProductRequest } = require('./product_pb');
const client = new ProductServiceClient('http://localhost:8080');
const request = new GetProductRequest();
request.setId(1);
client.getProduct(request, {}, (err, response) => {
if (err) console.error(err);
else console.log('商品:', response.getName());
});
*/
// 使用google/api/annotations.proto自动生成REST映射
syntax = "proto3";
package product;
import "google/api/annotations.proto";
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product) {
option (google.api.http) = {
get: "/v1/products/{id}"
};
}
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse) {
option (google.api.http) = {
get: "/v1/products"
};
}
rpc CreateProduct(CreateProductRequest) returns (Product) {
option (google.api.http) = {
post: "/v1/products"
body: "*"
};
}
rpc UpdateProduct(UpdateProductRequest) returns (Product) {
option (google.api.http) = {
patch: "/v1/products/{id}"
body: "*"
};
}
rpc DeleteProduct(DeleteProductRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/v1/products/{id}"
};
}
}
// 使用protoc-gen-grpc-gateway生成反向代理
// protoc --grpc-gateway_out=. product.proto
| 方案 | 适用场景 | 维护方 | 特点 |
|---|---|---|---|
| grpc-gateway(Go) | Go微服务 | 社区 | Proto注解自动生成 |
| Envoy | 大型微服务 | Lyft/CNCF | 高性能、gRPC-Web原生支持 |
| Kong | API管理平台 | Kong Inc | 插件生态丰富 |
| 自定义Node.js | 灵活定制 | 自维护 | 完全可控 |
| Nginx+gRPC | 简单代理 | F5 | 成熟稳定 |
# Nginx gRPC代理配置
# nginx.conf
server {
listen 8080 http2;
# gRPC代理
location / {
grpc_pass grpc://localhost:50051;
grpc_connect_timeout 5s;
grpc_read_timeout 30s;
grpc_send_timeout 30s;
}
# gRPC-Web支持(需要grpcweb模块)
location /grpcweb {
grpc_pass grpc://localhost:50051;
grpc_set_header X-Real-IP $remote_addr;
}
}
# Envoy gRPC-Web配置(envoy.yaml)
static_resources:
listeners:
- name: grpc-web
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
allow_methods: GET, POST, OPTIONS
allow_headers: content-type,x-grpc-web
- name: envoy.filters.http.router
clusters:
- name: grpc_backend
connect_timeout: 5s
type: STATIC
hosts:
- socket_address: { address: 127.0.0.1, port_value: 50051 }
为本课所有gRPC方法(包括流式)实现REST映射,支持PUT/PATCH/DELETE和错误转换。
使用Docker运行Envoy,配置grpc-web过滤器,让浏览器前端直接调用gRPC服务。