gRPC в Node.js: protobuf, streaming, error handling, interceptors
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 даёт значительный прирост производительности.