Основные концепции¶
RPC Dart построен на нескольких основных концепциях, которые работают вместе для предоставления мощного и гибкого фреймворка удалённых вызовов процедур. Понимание этих концепций необходимо для эффективного использования библиотеки.
Ключевые компоненты¶
-
Контракты – Определяют интерфейс между сервисами с именами методов и их сигнатурами.
-
Эндпоинты – Обрабатывают коммуникацию между клиентской и серверной сторонами.
-
Транспорты – Управляют базовым механизмом коммуникации (InMemory, HTTP, WebSocket и т.д.).
-
Контекст – Переносят метаданные и управляющую информацию через RPC вызовы.
Контракты¶
Контракты в RPC Dart определяют интерфейс сервиса - какие методы доступны и как их следует вызывать. Они служат единым источником истины для реализаций как клиента, так и сервера.
Определение контракта¶
abstract interface class ICalculatorContract {
static const name = 'Calculator';
static const methodAdd = 'add';
static const methodSubtract = 'subtract';
static const methodDivide = 'divide';
}
Преимущества¶
- Типобезопасность: Обеспечивает согласованность сигнатур методов между клиентом и сервером
- Документация: Служит живой документацией доступных сервисов
- Версионность: Легко версионировать и развивать ваш API
- Генерация кода: Может использоваться для генерации клиентских и серверных заглушек
Эндпоинты¶
Эндпоинты - это точки коммуникации, которые обрабатывают RPC вызовы. Существует два типа:
RpcCallerEndpoint (Клиент)¶
Клиентский эндпоинт, который инициирует RPC вызовы:
final caller = RpcCallerEndpoint(transport: transport);
final calculator = CalculatorCaller(caller);
// Делаем RPC вызов
final result = await calculator.add(AddRequest(5, 3));
RpcResponderEndpoint (Сервер)¶
Серверный эндпоинт, который обрабатывает входящие RPC вызовы:
final responder = RpcResponderEndpoint(transport: transport);
responder.registerServiceContract(CalculatorResponder());
responder.start();
Транспорты¶
Транспорты определяют как сообщения отправляются между эндпоинтами. RPC Dart независим от транспорта, что означает, что вы можете переключать транспорты без изменения бизнес-логики.
Доступные транспорты¶
| Транспорт | Случай использования | Производительность | Сложность |
|---|---|---|---|
| InMemory | Тестирование, одиночный процесс | Высшая (без копирования) | Низшая |
| HTTP | Веб-сервисы, микросервисы | Средняя | Средняя |
| WebSocket | Реальное время, двунаправленная | Средняя | Средняя |
| Isolate | Многоядерность, изоляция | Высокая | Высшая |
Пример транспорта¶
// Разработка/Тестирование
final (clientTransport, serverTransport) = RpcInMemoryTransport.pair();
// Продакшн HTTP/2 (внутри async-функции)
final httpTransport = await RpcHttp2CallerTransport.secureConnect(
host: 'api.example.com',
);
// Реальное время WebSocket
final wsTransport = RpcWebSocketCallerTransport.connect(
Uri.parse('wss://api.example.com/rpc'),
);
Один и тот же код сервиса работает с любым транспортом!
Контекст¶
RPC Контекст переносит дополнительную информацию вместе с фактическими данными RPC вызова:
Что содержит контекст¶
- Correlation ID: Уникальный идентификатор для трассировки запросов
- Deadline: Информация о таймауте запроса
- Metadata: Пользовательские пары ключ-значение
- Cancellation: Возможность отменять текущие операции
Использование контекста¶
// Серверная сторона - доступ к контексту
Future<AddResponse> _add(AddRequest request, {RpcContext? context}) async {
final correlationId = context?.correlationId;
final userAgent = context?.getHeader('user-agent');
if (context?.isCancelled ?? false) {
throw RpcException('Запрос был отменён');
}
return AddResponse(request.a + request.b);
}
// Клиентская сторона - передача метаданных
final context = RpcContextBuilder()
.withHeader('user-agent', 'MyApp/1.0')
.withTimeout(const Duration(seconds: 5))
.build();
final result = await calculator.add(
AddRequest(5, 3),
context: context,
);
Типы RPC¶
RPC Dart поддерживает все стандартные паттерны RPC коммуникации:
Унарный RPC¶
Простой паттерн запрос-ответ:
Future<AddResponse> add(AddRequest request) {
return callUnary<AddRequest, AddResponse>(
methodName: 'add',
request: request,
);
}
Серверная потоковая передача¶
Сервер отправляет несколько ответов на один запрос:
Stream<NumberResponse> getNumbers(NumberRequest request) {
return callServerStream<NumberRequest, NumberResponse>(
methodName: 'getNumbers',
request: request,
);
}
Клиентская потоковая передача¶
Клиент отправляет несколько запросов, сервер отвечает один раз:
Future<SumResponse> sumNumbers(Stream<NumberRequest> requests) {
return callClientStream<NumberRequest, SumResponse>(
methodName: 'sumNumbers',
requests: requests,
);
}
Двунаправленная потоковая передача¶
И клиент, и сервер могут отправлять несколько сообщений:
Stream<EchoResponse> echo(Stream<EchoRequest> requests) {
return callBidirectionalStream<EchoRequest, EchoResponse>(
methodName: 'echo',
requests: requests,
);
}
Оптимизация без копирования¶
Одна из уникальных особенностей RPC Dart - передача объектов без копирования с InMemory транспортом:
class LargeData {
final List<int> data = List.generate(1000000, (i) => i);
}
// С InMemory транспортом этот объект передаётся по ссылке
// Без накладных расходов на сериализацию!
final result = await service.processLargeData(LargeData());
Преимущества¶
- Максимальная производительность: Без накладных расходов на сериализацию
- Эффективность памяти: Объекты не дублируются
- Сохранение типов: Полная информация о типах Dart сохраняется
Когда использовать¶
- Одно-процессные приложения
- Высокопроизводительные вычисления
- Передача больших данных
- Разработка и тестирование
Обработка ошибок¶
RPC Dart предоставляет структурированную обработку ошибок:
RpcException¶
// Сервер выбрасывает структурированные ошибки
if (request.b == 0) {
throw RpcException(
code: 'DIVISION_BY_ZERO',
message: 'Нельзя делить на ноль',
details: {'operand': 'b', 'value': 0},
);
}
// Клиент перехватывает и обрабатывает
try {
final result = await calculator.divide(DivideRequest(10, 0));
} on RpcException catch (e) {
print('RPC Ошибка: ${e.code} - ${e.message}');
print('Детали: ${e.details}');
}
Поддержка middleware¶
RPC Dart поддерживает middleware для сквозных задач:
// Middleware логирования
class LoggingMiddleware implements IRpcMiddleware {
@override
Future<dynamic> processRequest(
String serviceName,
String methodName,
dynamic request,
) async {
print('Вызов $serviceName.$methodName');
return request;
}
@override
Future<dynamic> processResponse(
String serviceName,
String methodName,
dynamic response,
) async {
print('Завершён $serviceName.$methodName');
return response;
}
}
// Регистрация middleware (для запуска хуков расширьте эндпоинт)
responder.addMiddleware(LoggingMiddleware());
Примечание. Базовые эндпоинты хранят зарегистрированные middleware и отображают их количество в диагностике. Чтобы запускать хуки до/после обработчиков уже сейчас, расширьте эндпоинт и вызовите
processRequest/processResponseвручную.
Лучшие практики¶
Дизайн сервисов¶
- Держите контракты простыми: Одна ответственность на сервис
- Используйте значимые имена: Понятные имена методов и параметров
- Версионируйте контракты: Планируйте эволюцию API
- Обрабатывайте ошибки изящно: Используйте структурированные ответы об ошибках
Производительность¶
- Выбирайте подходящий транспорт: InMemory для одного процесса, HTTP для распределённых систем
- Используйте потоки: Для больших наборов данных или коммуникации в реальном времени
- Реализуйте таймауты: Устанавливайте разумные дедлайны для запросов
- Рассмотрите пакетирование: Группируйте связанные операции
Тестирование¶
- Используйте InMemory транспорт: Для быстрых, изолированных тестов
- Создавайте моки сервисов: Создавайте тестовые реализации контрактов
- Тестируйте сценарии ошибок: Проверяйте, что обработка ошибок работает корректно
- Интеграционное тестирование: Тестируйте с реальными транспортами
Следующие шаги¶
Теперь, когда вы понимаете основные концепции, изучите:
- Архитектура - Изучите паттерны архитектуры CORD
- Типы транспортов - Глубокое погружение в доступные транспорты
- Паттерны стриминга - Подробные примеры каждого паттерна RPC
- Тестирование и инструменты - Лучшие практики тестирования RPC приложений