lenec ru

← все посты

gRPC в Node.js: protobuf, streaming, error handling, interceptors

13K

REST API с JSON — стандарт для веб-сервисов, но в микросервисной архитектуре с высокой нагрузкой он показывает слабые стороны: текстовый формат, отсутствие строгой типизации, overhead HTTP/1.1. gRPC решает эти проблемы, используя бинарный протокол Protobuf и HTTP/2 с мультиплексированием. В Node.js gRPC позволяет строить высокопроизводительные RPC-сервисы с типобезопасностью и встроенной поддержкой streaming.

Зачем gRPC вместо REST

REST API передаёт данные в JSON — текстовом формате, который нужно парсить на каждом запросе. gRPC использует Protocol Buffers (protobuf) — бинарный формат, который в 3–10 раз компактнее и быстрее в сериализации. HTTP/2 в gRPC поддерживает мультиплексирование: несколько запросов в одном TCP-соединении без head-of-line blocking.

Ключевые преимущества gRPC:

  • Производительность: бинарный протокол, меньше трафика, быстрее парсинг.
  • Типизация: protobuf-схемы генерируют типы для TypeScript, Go, Python — контракт API проверяется на этапе компиляции.
  • Streaming: server streaming, client streaming, bidirectional streaming из коробки.
  • Кодогенерация: из .proto файлов автоматически генерируются клиенты и серверы.

Недостатки: нет поддержки в браузерах без grpc-web прокси, сложнее отладка (бинарный формат), меньше инструментов для мониторинга.

Установка и настройка

Установите пакеты @grpc/grpc-js (чистый JS, без нативных зависимостей) и @grpc/proto-loader для динамической загрузки .proto файлов:

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

Для статической кодогенерации (рекомендуется для production) используйте grpc-tools:

npm install --save-dev grpc-tools

Определение сервиса через Protobuf

Создайте файл user.proto с описанием сервиса:

syntax = "proto3";

package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
  rpc CreateUser (stream CreateUserRequest) returns (CreateUserResponse);
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message GetUserRequest {
  string user_id = 1;
}

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

message ListUsersRequest {
  int32 limit = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
}

message CreateUserResponse {
  string user_id = 1;
}

message ChatMessage {
  string user_id = 1;
  string text = 2;
  int64 timestamp = 3;
}

Четыре типа RPC:

  • Unary: GetUser — один запрос, один ответ (как REST).
  • Server streaming: ListUsers — один запрос, поток ответов.
  • Client streaming: CreateUser — поток запросов, один ответ.
  • Bidirectional streaming: Chat — поток запросов и ответов одновременно.

Реализация gRPC-сервера

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';

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

const userProto = grpc.loadPackageDefinition(packageDefinition).user as any;

// Unary RPC
function getUser(call: any, callback: any) {
  const userId = call.request.user_id;
  const user = { id: userId, name: 'Alice', email: 'alice@example.com', age: 30 };
  callback(null, user);
}

// Server streaming
function listUsers(call: any) {
  const users = [
    { id: '1', name: 'Alice', email: 'alice@example.com', age: 30 },
    { id: '2', name: 'Bob', email: 'bob@example.com', age: 25 },
  ];
  users.forEach(user => call.write(user));
  call.end();
}

// Client streaming
function createUser(call: any, callback: any) {
  const users: any[] = [];
  call.on('data', (request: any) => {
    users.push({ name: request.name, email: request.email });
  });
  call.on('end', () => {
    callback(null, { user_id: 'batch-' + users.length });
  });
}

// Bidirectional streaming
function chat(call: any) {
  call.on('data', (message: any) => {
    console.log('Received:', message.text);
    call.write({ user_id: 'server', text: 'Echo: ' + message.text, timestamp: Date.now() });
  });
  call.on('end', () => call.end());
}

const server = new grpc.Server();
server.addService(userProto.UserService.service, {
  getUser,
  listUsers,
  createUser,
  chat,
});

server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), (err, port) => {
  if (err) throw err;
  console.log(`gRPC server running on port ${port}`);
  server.start();
});

Реализация gRPC-клиента

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';

const PROTO_PATH = './user.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const userProto = grpc.loadPackageDefinition(packageDefinition).user as any;

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

// Unary call
client.getUser({ user_id: '123' }, (err: any, response: any) => {
  if (err) console.error(err);
  else console.log('User:', response);
});

// Server streaming
const stream = client.listUsers({ limit: 10 });
stream.on('data', (user: any) => console.log('User:', user));
stream.on('end', () => console.log('Stream ended'));
stream.on('error', (err: any) => console.error(err));

// Client streaming
const createStream = client.createUser((err: any, response: any) => {
  if (err) console.error(err);
  else console.log('Created:', response.user_id);
});
createStream.write({ name: 'Alice', email: 'alice@example.com' });
createStream.write({ name: 'Bob', email: 'bob@example.com' });
createStream.end();

// Bidirectional streaming
const chatStream = client.chat();
chatStream.on('data', (msg: any) => console.log('Server:', msg.text));
chatStream.write({ user_id: 'client', text: 'Hello', timestamp: Date.now() });
chatStream.write({ user_id: 'client', text: 'World', timestamp: Date.now() });
chatStream.end();

Error Handling

gRPC использует статус-коды (аналог HTTP): OK, CANCELLED, INVALID_ARGUMENT, NOT_FOUND, INTERNAL и т.д.

function getUser(call: any, callback: any) {
  const userId = call.request.user_id;
  if (!userId) {
    return callback({
      code: grpc.status.INVALID_ARGUMENT,
      message: 'user_id is required',
    });
  }
  const user = findUserById(userId);
  if (!user) {
    return callback({
      code: grpc.status.NOT_FOUND,
      message: `User ${userId} not found`,
    });
  }
  callback(null, user);
}

На клиенте обрабатывайте ошибки через err.code:

client.getUser({ user_id: '' }, (err: any, response: any) => {
  if (err) {
    if (err.code === grpc.status.INVALID_ARGUMENT) {
      console.error('Invalid request:', err.message);
    } else if (err.code === grpc.status.NOT_FOUND) {
      console.error('User not found');
    } else {
      console.error('gRPC error:', err);
    }
  } else {
    console.log('User:', response);
  }
});

Interceptors: middleware для gRPC

Interceptors позволяют добавлять логику до/после вызова RPC: логирование, аутентификацию, метрики.

Server interceptor

function loggingInterceptor(call: any, methodDefinition: any, next: any) {
  console.log(`[gRPC] ${methodDefinition.path} called`);
  const start = Date.now();
  return next(call).on('finish', () => {
    console.log(`[gRPC] ${methodDefinition.path} took ${Date.now() - start}ms`);
  });
}

server.addService(userProto.UserService.service, {
  getUser,
  listUsers,
}, {
  interceptors: [loggingInterceptor],
});

Client interceptor

function authInterceptor(options: any, nextCall: any) {
  return new grpc.InterceptingCall(nextCall(options), {
    start(metadata, listener, next) {
      metadata.add('authorization', 'Bearer token123');
      next(metadata, listener);
    },
  });
}

const client = new userProto.UserService(
  'localhost:50051',
  grpc.credentials.createInsecure(),
  { interceptors: [authInterceptor] }
);

Interceptor добавляет заголовок authorization ко всем запросам. На сервере читайте metadata:

function getUser(call: any, callback: any) {
  const token = call.metadata.get('authorization')[0];
  if (!token || token !== 'Bearer token123') {
    return callback({
      code: grpc.status.UNAUTHENTICATED,
      message: 'Invalid token',
    });
  }
  // ...
}

Подводные камни

  • Нет поддержки в браузерах: gRPC работает поверх HTTP/2, который браузеры не поддерживают для RPC. Используйте grpc-web с Envoy прокси.
  • Отладка: бинарный формат сложнее инспектировать. Используйте grpcurl или Postman с поддержкой gRPC.
  • Streaming и backpressure: если клиент не успевает читать stream, буфер переполнится. Контролируйте скорость через call.pause() / call.resume().
  • Версионирование: изменения в .proto должны быть обратно совместимы. Не удаляйте поля, используйте reserved.

Вывод

gRPC — мощный инструмент для межсервисного взаимодействия в микросервисной архитектуре. Бинарный протокол Protobuf, HTTP/2, типобезопасность и встроенный streaming делают его идеальным выбором для высоконагруженных систем. В Node.js реализация gRPC-сервисов проста благодаря @grpc/grpc-js, а interceptors позволяют централизовать логику аутентификации и логирования. Для публичных API REST остаётся стандартом, но для внутренних коммуникаций gRPC даёт значительный прирост производительности.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.