Перейти к содержимому

Основные концепции

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 вызовы. Существует два типа:

Клиентский эндпоинт, который инициирует RPC вызовы:

final caller = RpcCallerEndpoint(transport: transport);
final calculator = CalculatorCaller(caller);
// Делаем RPC вызов
final result = await calculator.add(AddRequest(5, 3));

Серверный эндпоинт, который обрабатывает входящие RPC вызовы:

final responder = RpcResponderEndpoint(transport: transport);
responder.registerServiceContract(CalculatorResponder());
responder.start();

Транспорты определяют как сообщения отправляются между эндпоинтами. RPC Dart независим от транспорта, что означает, что вы можете переключать транспорты без изменения бизнес-логики.

ТранспортСлучай использованияПроизводительностьСложность
InMemoryТестирование, одиночный процессВысшая (без копирования)Низшая
HTTPВеб-сервисы, микросервисыСредняяСредняя
WebSocketРеальное время, двунаправленнаяСредняяСредняя
IsolateМногоядерность, изоляцияВысокаяВысшая
// Разработка/Тестирование
final (clientTransport, serverTransport) = RpcInMemoryTransport.pair();
// Продакшн HTTP
final httpTransport = RpcHttpTransport(
url: 'https://api.example.com',
timeout: Duration(seconds: 30),
);
// Реальное время WebSocket
final wsTransport = RpcWebSocketTransport(
url: '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?.metadata['user-agent'];
if (context?.isCancelled ?? false) {
throw RpcException('Запрос был отменён');
}
return AddResponse(request.a + request.b);
}
// Клиентская сторона - передача метаданных
final context = RpcContext()
..metadata['user-agent'] = 'MyApp/1.0'
..deadline = DateTime.now().add(Duration(seconds: 5));
final result = await calculator.add(
AddRequest(5, 3),
context: context,
);

RPC Dart поддерживает все стандартные паттерны 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 предоставляет структурированную обработку ошибок:

// Сервер выбрасывает структурированные ошибки
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}');
}

RPC Dart поддерживает middleware для сквозных задач:

// Middleware логирования
class LoggingMiddleware extends RpcMiddleware {
@override
Future<T> call<T>(RpcCall call, RpcNext next) async {
print('Вызов ${call.method}');
final result = await next(call);
print('Завершён ${call.method}');
return result;
}
}
// Регистрация middleware
responder.addMiddleware(LoggingMiddleware());
  1. Держите контракты простыми: Одна ответственность на сервис
  2. Используйте значимые имена: Понятные имена методов и параметров
  3. Версионируйте контракты: Планируйте эволюцию API
  4. Обрабатывайте ошибки изящно: Используйте структурированные ответы об ошибках
  1. Выбирайте подходящий транспорт: InMemory для одного процесса, HTTP для распределённых систем
  2. Используйте потоки: Для больших наборов данных или коммуникации в реальном времени
  3. Реализуйте таймауты: Устанавливайте разумные дедлайны для запросов
  4. Рассмотрите пакетирование: Группируйте связанные операции
  1. Используйте InMemory транспорт: Для быстрых, изолированных тестов
  2. Создавайте моки сервисов: Создавайте тестовые реализации контрактов
  3. Тестируйте сценарии ошибок: Проверяйте, что обработка ошибок работает корректно
  4. Интеграционное тестирование: Тестируйте с реальными транспортами

Теперь, когда вы понимаете основные концепции, изучите: