📖 第20课:gRPC网关

阶段四:gRPC

gRPC在微服务间表现出色,但浏览器和移动端不直接支持gRPC。gRPC网关( grpc-gateway)解决这个问题——它将RESTful HTTP请求翻译为gRPC调用,让同一套gRPC服务同时支持REST和gRPC两种访问方式。本课深入网关原理、grpc-gateway实现和gRPC-Web方案。

🌉 为什么需要gRPC网关

三种外部访问gRPC的方案

方案原理客户端要求适用场景
grpc-gateway反向代理REST→gRPC只需HTTP客户端对外API、浏览器兼容
gRPC-Web浏览器JS直接调用gRPCgrpc-web客户端库前端SPA应用
自定义适配层手动编写REST→gRPC转换HTTP客户端高度定制需求

🛠️ 实战:grpc-gateway代理

// 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网关

# 先启动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-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());
});
*/

📐 Proto注解自动生成网关

// 使用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

📝 本课小结

  1. gRPC网关让REST客户端也能访问gRPC服务
  2. grpc-gateway是反向代理模式,REST→gRPC翻译
  3. gRPC-Web让浏览器直接调用gRPC
  4. gRPC错误码需要正确映射为HTTP状态码
  5. 服务端流可以转为SSE推送给浏览器
  6. Proto注解可以自动生成REST映射规则
  7. 健康检查端点验证gRPC连接状态

📊 gRPC网关生产架构

生产级网关架构选型

方案适用场景维护方特点
grpc-gateway(Go)Go微服务社区Proto注解自动生成
Envoy大型微服务Lyft/CNCF高性能、gRPC-Web原生支持
KongAPI管理平台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 }
💡 网关选型建议:新项目从Envoy起步——它同时支持gRPC代理、gRPC-Web、负载均衡和可观测性。Kong适合需要API管理(限流、认证、分析)的场景。Nginx适合简单gRPC代理。自定义Node.js适合原型验证和高度定制需求。

💪 练习

练习1:实现完整的REST-gRPC网关

为本课所有gRPC方法(包括流式)实现REST映射,支持PUT/PATCH/DELETE和错误转换。

练习2:配置Envoy代理

使用Docker运行Envoy,配置grpc-web过滤器,让浏览器前端直接调用gRPC服务。

🏆 本课成就:协议桥接者