g RPC Protobuf

gRPC-Protobuf

#tech / dev / frame #type / concept #status / growing

gRPC-Protobuf

基础概念

gRPC 简介

gRPC (gRPC Remote Procedure Calls) 是由 Google 开发的高性能、开源的通用 RPC 框架,基于 HTTP/2 协议传输,使用 Protocol Buffers 作为接口描述语言。

Protocol Buffers 简介

Protocol Buffers (protobuf) 是 Google 开发的语言无关、平台无关、可扩展的序列化结构数据的方法,常用于通信协议、数据存储等。

核心特点

  • 高性能:基于 HTTP/2,支持多路复用、流控制
  • 跨语言:支持多种编程语言
  • 强类型:基于 .proto 文件定义接口
  • 双向流式传输:支持客户端流、服务端流和双向流
  • 代码生成:自动生成客户端和服务端代码

使用指南

安装依赖

Node.js 环境:

npm install @grpc/grpc-js @grpc/proto-loader
npm install -D grpc-tools

Python 环境:

pip install grpcio grpcio-tools

定义 .proto 文件

syntax = "proto3";

package example;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc CreateUser (CreateUserRequest) returns (User);
  rpc GetUsers (GetUsersRequest) returns (stream User);
  rpc UpdateUsers (stream UpdateUserRequest) returns (UpdateResponse);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
}

message GetUserRequest {
  int32 id = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

message GetUsersRequest {
  int32 limit = 1;
  int32 offset = 2;
}

message UpdateUserRequest {
  int32 id = 1;
  User user = 2;
}

message UpdateResponse {
  bool success = 1;
  string message = 2;
}

生成代码

Node.js:

npx grpc_tools_node_protoc \
  --js_out=import_style=commonjs,binary:./src/generated \
  --grpc_out=grpc_js:./src/generated \
  --plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin \
  -I ./protos \
  ./protos/user.proto

Python:

python -m grpc_tools.protoc \
  --proto_path=./protos \
  --python_out=./src/generated \
  --grpc_python_out=./src/generated \
  ./protos/user.proto

服务端实现(Node.js)

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// 加载 proto 文件
const packageDefinition = protoLoader.loadSync('./protos/user.proto', {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});

const userProto = grpc.loadPackageDefinition(packageDefinition).example;

// 模拟数据库
const users = [
  { id: 1, name: 'Alice', email: 'alice@example.com', age: 25 },
  { id: 2, name: 'Bob', email: 'bob@example.com', age: 30 }
];

// 服务实现
const userService = {
  GetUser: (call, callback) => {
    const user = users.find(u => u.id === call.request.id);
    if (user) {
      callback(null, user);
    } else {
      callback({
        code: grpc.status.NOT_FOUND,
        message: 'User not found'
      });
    }
  },

  CreateUser: (call, callback) => {
    const newUser = {
      id: users.length + 1,
      name: call.request.name,
      email: call.request.email,
      age: call.request.age
    };
    users.push(newUser);
    callback(null, newUser);
  },

  GetUsers: (call) => {
    const { limit = 10, offset = 0 } = call.request;
    const result = users.slice(offset, offset + limit);
    
    result.forEach(user => {
      call.write(user);
    });
    call.end();
  }
};

// 创建服务器
const server = new grpc.Server();
server.addService(userProto.UserService.service, userService);

const address = '0.0.0.0:50051';
server.bindAsync(address, grpc.ServerCredentials.createInsecure(), (err, port) => {
  if (err) {
    console.error('Server failed to bind:', err);
    return;
  }
  console.log(`Server running at ${address}`);
  server.start();
});

客户端实现(Node.js)

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

// 加载 proto 文件
const packageDefinition = protoLoader.loadSync('./protos/user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).example;

// 创建客户端
const client = new userProto.UserService('localhost:50051', 
  grpc.credentials.createInsecure());

// 调用服务
function getUserById(id) {
  return new Promise((resolve, reject) => {
    client.GetUser({ id }, (err, response) => {
      if (err) {
        reject(err);
      } else {
        resolve(response);
      }
    });
  });
}

function createUser(name, email, age) {
  return new Promise((resolve, reject) => {
    client.CreateUser({ name, email, age }, (err, response) => {
      if (err) {
        reject(err);
      } else {
        resolve(response);
      }
    });
  });
}

// 流式调用示例
function getUsers() {
  const call = client.GetUsers({ limit: 10, offset: 0 });
  
  call.on('data', (user) => {
    console.log('Received user:', user);
  });
  
  call.on('end', () => {
    console.log('Stream ended');
  });
  
  call.on('error', (err) => {
    console.error('Stream error:', err);
  });
}

// 使用示例
async function main() {
  try {
    const user = await getUserById(1);
    console.log('User:', user);
    
    const newUser = await createUser('Charlie', 'charlie@example.com', 28);
    console.log('Created user:', newUser);
    
    getUsers();
  } catch (error) {
    console.error('Error:', error);
  }
}

main();

实战经验

最佳实践

  1. 接口设计

    // 使用有意义的消息名称
    message CreateUserRequest {
      // 避免使用 reserved 关键字作为字段名
      string user_name = 1;  // 使用 snake_case
      string email_address = 2;
      optional int32 age = 3;  // 使用 optional 处理可选字段
    }
    
    // 为未来扩展保留字段编号
    message User {
      int32 id = 1;
      string name = 2;
      // reserved 3 to 5; // 保留字段编号
      string email = 6;
    }
  2. 错误处理

    // 服务端错误处理
    const grpc = require('@grpc/grpc-js');
    
    function handleGetUser(call, callback) {
      try {
        const user = findUser(call.request.id);
        if (!user) {
          callback({
            code: grpc.status.NOT_FOUND,
            message: 'User not found'
          });
          return;
        }
        callback(null, user);
      } catch (error) {
        callback({
          code: grpc.status.INTERNAL,
          message: 'Internal server error'
        });
      }
    }
    
    // 客户端错误处理
    client.GetUser({ id: 1 }, (err, response) => {
      if (err) {
        switch (err.code) {
          case grpc.status.NOT_FOUND:
            console.log('User not found');
            break;
          case grpc.status.UNAVAILABLE:
            console.log('Service unavailable');
            break;
          default:
            console.error('Unknown error:', err);
        }
        return;
      }
      console.log('User:', response);
    });
  3. 连接管理

    // 连接池配置
    const client = new userProto.UserService('localhost:50051', 
      grpc.credentials.createInsecure(), {
        'grpc.keepalive_time_ms': 30000,
        'grpc.keepalive_timeout_ms': 5000,
        'grpc.keepalive_permit_without_calls': true,
        'grpc.http2.max_pings_without_data': 0
      });
    
    // 连接状态监控
    client.getChannel().watchConnectivityState(
      grpc.connectivityState.READY, 
      Date.now() + 5000, 
      (error) => {
        if (error) {
          console.log('Connection failed');
        } else {
          console.log('Connected successfully');
        }
      }
    );

性能优化

  1. 消息压缩

    // 启用 gzip 压缩
    const client = new userProto.UserService('localhost:50051', 
      grpc.credentials.createInsecure());
    
    client.GetUser({ id: 1 }, { 
      compression: grpc.compression.gzip 
    }, callback);
  2. 批量操作

    // 使用批量请求减少网络开销
    message BatchCreateUsersRequest {
      repeated CreateUserRequest users = 1;
    }
    
    message BatchCreateUsersResponse {
      repeated User users = 1;
      repeated Error errors = 2;
    }

部署配置

  1. 生产环境配置

    // TLS 配置
    const fs = require('fs');
    
    const credentials = grpc.ServerCredentials.createSsl(
      fs.readFileSync('./certs/ca-cert.pem'),
      [{
        cert_chain: fs.readFileSync('./certs/server-cert.pem'),
        private_key: fs.readFileSync('./certs/server-key.pem')
      }]
    );
    
    server.bindAsync('0.0.0.0:50051', credentials, callback);
  2. 负载均衡

    // 客户端负载均衡
    const client = new userProto.UserService(
      'dns:///user-service:50051',  // 使用 DNS 解析
      grpc.credentials.createInsecure(),
      {
        'grpc.lb_policy_name': 'round_robin'
      }
    );

经验总结

优势

  • 性能优异:基于 HTTP/2,支持多路复用
  • 强类型安全:编译时类型检查
  • 跨语言互操作:支持多种编程语言
  • 流式传输:支持双向流式通信
  • 代码生成:自动生成客户端和服务端代码

注意事项

  • 浏览器兼容性:需要代理层支持 gRPC-Web
  • 调试复杂性:二进制协议不如 JSON 直观
  • 学习成本:需要学习 Protobuf 语法
  • 版本兼容:需要careful管理 .proto 文件版本

适用场景

  • 微服务架构内部通信
  • 高性能要求的系统
  • 需要强类型接口的场景
  • 多语言环境的服务通信

信息参考

创建于 2025/1/1 更新于 2026/5/27