📖 第16课:gRPC概述

阶段四:gRPC

gRPC是Google开源的高性能RPC框架,基于HTTP/2和Protocol Buffers,天生适合微服务间通信。与REST的文本JSON不同,gRPC使用二进制协议,性能提升10倍以上。本课从gRPC的架构、核心概念到第一个gRPC服务的实现。

🏗️ gRPC架构

gRPC核心组成

组件说明
Protocol Buffers接口定义语言(IDL)和序列化格式
HTTP/2传输层协议,支持多路复用和流控
gRPC核心通道、存根、服务端框架
代码生成器根据.proto自动生成客户端/服务端代码

⚖️ gRPC vs REST vs GraphQL

维度RESTGraphQLgRPC
协议HTTP/1.1HTTP/1.1+HTTP/2
数据格式JSON(文本)JSON(文本)Protobuf(二进制)
序列化速度快10x+
代码生成OpenAPI(可选)代码优先✅ 内置
流式支持Subscription✅ 四种模式
浏览器支持⚠️ 需gRPC-Web
人类可读❌ 二进制
适用场景外部API复杂前端微服务间通信

🛠️ 实战:第一个gRPC服务

使用Node.js实现一个商品管理的gRPC服务。

// 文件1: product.proto - 接口定义
syntax = "proto3";

package product;

// 商品服务
service ProductService {
  // 一元RPC:获取单个商品
  rpc GetProduct(GetProductRequest) returns (Product);
  
  // 一元RPC:列出商品
  rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
  
  // 一元RPC:创建商品
  rpc CreateProduct(CreateProductRequest) returns (Product);
  
  // 服务端流:批量导出
  rpc ExportProducts(ExportRequest) returns (stream Product);
  
  // 客户端流:批量导入
  rpc ImportProducts(stream Product) returns (ImportResponse);
  
  // 双向流:实时聊天
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

// 消息定义
message Product {
  int32 id = 1;
  string name = 2;
  string category = 3;
  double price = 4;
  int32 stock = 5;
  string status = 6;
}

message GetProductRequest {
  int32 id = 1;
}

message ListProductsRequest {
  string category = 1;
  int32 page = 2;
  int32 page_size = 3;
}

message ListProductsResponse {
  repeated Product products = 1;
  int32 total = 2;
}

message CreateProductRequest {
  string name = 1;
  string category = 2;
  double price = 3;
  int32 stock = 4;
}

message ExportRequest {
  string category = 1;
}

message ImportResponse {
  int32 success_count = 1;
  int32 fail_count = 2;
  repeated string errors = 3;
}

message ChatMessage {
  string user = 1;
  string message = 2;
}
// 文件2: grpc-server.js - gRPC服务端
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// 加载proto定义
const packageDef = protoLoader.loadSync('product.proto', {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
const productProto = grpc.loadPackageDefinition(packageDef).product;

// 数据
const products = [
  { id: 1, name: '机械键盘', category: '外设', price: 599.0, stock: 120, status: 'active' },
  { id: 2, name: '无线鼠标', category: '外设', price: 199.0, stock: 250, status: 'active' },
  { id: 3, name: '4K显示器', category: '显示器', price: 2999.0, stock: 45, status: 'active' },
  { id: 4, name: 'USB扩展坞', category: '配件', price: 399.0, stock: 0, status: 'inactive' },
  { id: 5, name: '降噪耳机', category: '音频', price: 1299.0, stock: 60, status: 'active' },
];
let nextId = 6;

// ===== 服务实现 =====

// 获取单个商品
function GetProduct(call, callback) {
  const product = products.find(p => p.id === call.request.id);
  if (!product) {
    return callback({ code: grpc.status.NOT_FOUND, message: `商品 ${call.request.id} 不存在` });
  }
  callback(null, product);
}

// 列出商品
function ListProducts(call, callback) {
  const { category, page, pageSize } = call.request;
  let result = [...products];
  if (category) result = result.filter(p => p.category === category);

  const p = page || 1;
  const ps = pageSize || 20;
  const total = result.length;
  result = result.slice((p - 1) * ps, p * ps);

  callback(null, { products: result, total });
}

// 创建商品
function CreateProduct(call, callback) {
  const { name, category, price, stock } = call.request;
  if (!name) {
    return callback({ code: grpc.status.INVALID_ARGUMENT, message: 'name不能为空' });
  }
  const product = { id: nextId++, name, category: category || '未分类', price: price || 0, stock: stock || 0, status: 'active' };
  products.push(product);
  callback(null, product);
}

// 服务端流:批量导出
function ExportProducts(call) {
  const { category } = call.request;
  const filtered = category ? products.filter(p => p.category === category) : products;

  filtered.forEach((product, i) => {
    // 模拟逐条发送
    setTimeout(() => {
      call.write(product);
      if (i === filtered.length - 1) call.end();
    }, i * 100);
  });

  if (filtered.length === 0) call.end();
}

// 客户端流:批量导入
function ImportProducts(call, callback) {
  let successCount = 0;
  let failCount = 0;
  const errors = [];

  call.on('data', (product) => {
    try {
      if (!product.name) {
        failCount++;
        errors.push(`商品名称为空`);
        return;
      }
      products.push({ ...product, id: nextId++, status: 'active' });
      successCount++;
    } catch (e) {
      failCount++;
      errors.push(e.message);
    }
  });

  call.on('end', () => {
    callback(null, { successCount, failCount, errors });
  });
}

// 双向流:聊天
function Chat(call) {
  call.on('data', (msg) => {
    console.log(`💬 ${msg.user}: ${msg.message}`);
    // 回显+服务端消息
    call.write({ user: 'Server', message: `收到 "${msg.message}",已处理` });
  });
  call.on('end', () => call.end());
}

// ===== 启动服务 =====

const server = new grpc.Server();
server.addService(productProto.ProductService.service, {
  GetProduct,
  ListProducts,
  CreateProduct,
  ExportProducts,
  ImportProducts,
  Chat,
});

server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), (err, port) => {
  if (err) { console.error(err); return; }
  console.log(`🚀 gRPC服务运行在端口 ${port}`);
  server.start();
});
// 文件3: grpc-client.js - gRPC客户端
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDef = protoLoader.loadSync('product.proto', {});
const productProto = grpc.loadPackageDefinition(packageDef).product;

const client = new productProto.ProductService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

// 一元RPC调用
console.log('=== 获取商品 ===');
client.GetProduct({ id: 1 }, (err, product) => {
  if (err) return console.error('错误:', err.message);
  console.log('商品:', product);
});

// 列表
client.ListProducts({ category: '外设', page: 1, pageSize: 10 }, (err, res) => {
  if (err) return console.error('错误:', err.message);
  console.log(`\n=== 外设商品(${res.total}条) ===`);
  res.products.forEach(p => console.log(`  ${p.name} ¥${p.price}`));
});

// 创建
client.CreateProduct({ name: '蓝牙音箱', category: '音频', price: 299.0, stock: 50 }, (err, product) => {
  if (err) return console.error('错误:', err.message);
  console.log('\n=== 创建商品 ===');
  console.log('新商品:', product);
});

// 服务端流
console.log('\n=== 导出商品 ===');
const exportStream = client.ExportProducts({ category: '外设' });
exportStream.on('data', (product) => console.log(`  导出: ${product.name}`));
exportStream.on('end', () => console.log('导出完成'));

// 双向流
console.log('\n=== 聊天 ===');
const chat = client.Chat();
chat.on('data', (msg) => console.log(`${msg.user}: ${msg.message}`));
chat.write({ user: '客户端', message: '你好!' });
setTimeout(() => { chat.write({ user: '客户端', message: '查询商品' }); }, 500);
setTimeout(() => chat.end(), 1500);

运行gRPC服务

# 安装依赖
npm install @grpc/grpc-js @grpc/proto-loader

# 启动服务端
node grpc-server.js

# 另一终端运行客户端
node grpc-client.js
=== 获取商品 ===
商品: { id: 1, name: '机械键盘', category: '外设', price: 599, stock: 120, status: 'active' }

=== 外设商品(2条) ===
  机械键盘 ¥599
  无线鼠标 ¥199

=== 创建商品 ===
新商品: { id: 6, name: '蓝牙音箱', category: '音频', price: 299, stock: 50, status: 'active' }

=== 导出商品 ===
  导出: 机械键盘
  导出: 无线鼠标
导出完成

📊 gRPC四种通信模式

模式请求响应类比场景
一元RPC1个1个HTTP请求CRUD操作
服务端流1个流(N个)订阅导出、日志
客户端流流(N个)1个上传批量导入
双向流WebSocket聊天、实时

📝 本课小结

  1. gRPC基于HTTP/2 + Protobuf,高性能二进制通信
  2. 代码自动生成,强类型约束贯穿前后端
  3. 四种通信模式覆盖所有场景
  4. 适合微服务间通信,不适合直接暴露给浏览器
  5. Proto文件是服务契约,先定义后实现

⚡ gRPC性能优势

Protobuf vs JSON 性能对比

指标JSONProtobuf差距
序列化大小100%~30%小3-5倍
序列化速度基准5-10x快5-10倍
反序列化速度基准5-10x快5-10倍
网络传输文本二进制带宽省60%+
类型安全运行时编译时更早发现错误
// Node.js性能对比测试
const protobuf = require('protobufjs');

// JSON序列化
const jsonObj = { id: 1, name: '机械键盘', category: '外设', price: 599.0, stock: 120 };
const jsonStart = process.hrtime.bigint();
for (let i = 0; i < 100000; i++) {
  const serialized = JSON.stringify(jsonObj);
  JSON.parse(serialized);
}
const jsonDuration = Number(process.hrtime.bigint() - jsonStart) / 1e6;

// Protobuf序列化
const root = protobuf.loadSync('product.proto');
const Product = root.lookupType('product.Product');
const msg = Product.create(jsonObj);
const pbStart = process.hrtime.bigint();
for (let i = 0; i < 100000; i++) {
  const serialized = Product.encode(msg).finish();
  Product.decode(serialized);
}
const pbDuration = Number(process.hrtime.bigint() - pbStart) / 1e6;

console.log(`JSON: ${jsonDuration.toFixed(0)}ms`);
console.log(`Protobuf: ${pbDuration.toFixed(0)}ms`);
console.log(`差距: ${(jsonDuration / pbDuration).toFixed(1)}x`);

// 典型结果:
// JSON: 450ms
// Protobuf: 85ms
// 差距: 5.3x

💪 练习

练习1:实现用户管理gRPC服务

定义user.proto,实现GetUser、ListUsers、CreateUser、UpdateUser、DeleteUser五个一元RPC方法。

练习2:使用grpcurl测试

安装grpcurl工具,用它测试本课的gRPC服务。提示:grpcurl -plaintext localhost:50051 list

🏆 本课成就:gRPC入门者