正在加载,请稍候…

gRPC 与 Node.js:构建超越 REST 的高性能 API

在 Node.js 中实现 gRPC 服务——协议缓冲区、流式 RPC、拦截器、错误处理、负载均衡,以及 gRPC 与 REST 的对比。

gRPC with Node.js: Build High-Performance APIs Beyond REST

为什么选择 gRPC?

gRPC 提供二进制序列化(比 JSON 更快)、通过 Protocol Buffers 实现契约优先开发,以及对流式传输的原生支持——这些都是 REST 无法比拟的。

gRPC with Node.js: Build High-Performance APIs Beyond REST illustration

定义你的服务

// user.proto
syntax = "proto3";

package user;

service UserService {
  // Unary RPC
  rpc GetUser (GetUserRequest) returns (UserResponse);
  
  // Server streaming
  rpc ListUsers (ListUsersRequest) returns (stream UserResponse);
  
  // Client streaming
  rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
  
  // Bidirectional streaming
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message GetUserRequest {
  string id = 1;
}

message UserResponse {
  string id = 1;
  string name = 2;
  string email = 3;
  int64 created_at = 4;
}

message ListUsersRequest {
  int32 page = 1;
  int32 page_size = 2;
  string filter = 3;
}

服务器实现

import grpc from '@grpc/grpc-js';
import protoLoader from '@grpc/proto-loader';
import { fileURLToPath } from 'url';
import path from 'path';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const packageDef = protoLoader.loadSync(
  path.join(__dirname, 'user.proto'),
  { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }
);

const UserProto = grpc.loadPackageDefinition(packageDef).user;

const userService = {
  getUser: async (call, callback) => {
    const { id } = call.request;
    try {
      const user = await db.users.findById(id);
      if (!user) {
        return callback({
          code: grpc.status.NOT_FOUND,
          message: 'User not found',
        });
      }
      callback(null, user);
    } catch (err) {
      callback({ code: grpc.status.INTERNAL, message: err.message });
    }
  },
  
  listUsers: async (call) => {
    const { page, page_size, filter } = call.request;
    try {
      const users = await db.users.findMany({ page, pageSize: page_size, filter });
      for (const user of users) {
        call.write(user);  // Stream each user
      }
      call.end();
    } catch (err) {
      call.destroy(err);
    }
  },
};

const server = new grpc.Server();
server.addService(UserProto.UserService.service, userService);
server.bindAsync(
  '0.0.0.0:50051',
  grpc.ServerCredentials.createInsecure(),
  (err, port) => {
    if (err) throw err;
    console.log('gRPC server running on port', port);
  }
);

gRPC with Node.js: Build High-Performance APIs Beyond REST illustration

客户端实现

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

// Unary call
const getUser = (id) => new Promise((resolve, reject) => {
  client.getUser({ id }, (err, response) => {
    if (err) reject(err);
    else resolve(response);
  });
});

// Server streaming
function streamUsers(filter) {
  const stream = client.listUsers({ page: 1, page_size: 100, filter });
  
  return new Promise((resolve, reject) => {
    const users = [];
    stream.on('data', (user) => users.push(user));
    stream.on('end', () => resolve(users));
    stream.on('error', reject);
  });
}

拦截器(中间件)

// Server interceptor for logging
function loggingInterceptor(options, nextCall) {
  return new grpc.ServerInterceptingCall(nextCall(options), {
    start: function(metadata, listener, next) {
      const start = Date.now();
      next(metadata, {
        ...listener,
        onReceiveMessage: function(message, next) {
          next(message);
        },
        onSendMessage: function(message, next) {
          console.log(`RPC ${options.method_definition.path}: ${Date.now() - start}ms`);
          next(message);
        }
      });
    }
  });
}

gRPC with Node.js: Build High-Performance APIs Beyond REST illustration

gRPC vs REST

特性 gRPC REST
协议 HTTP/2 HTTP/1.1+
载荷 二进制(Protobuf) JSON/XML
流式传输 原生支持 SSE/WebSocket
类型安全 编译时 运行时
浏览器支持 有限 通用
性能 约快10倍 标准

生产环境中的 TLS

const credentials = grpc.credentials.createSsl(
  fs.readFileSync('ca-cert.pem'),
  fs.readFileSync('client-key.pem'),
  fs.readFileSync('client-cert.pem')
);