Skip to content

HTTP/2 транспорт

HTTP/2 транспорт использует тот же gRPC wire-формат, который ожидают классические gRPC клиенты. Соединение поддерживается постоянно, а все RPC-потоки мультиплексируются поверх одного TCP-сокета. Поэтому после подключения RpcCallerEndpoint и RpcResponderEndpoint можно использовать привычные паттерны — unary, серверные и клиентские стримы, двунаправленный обмен — без дополнительных адаптеров.

Когда выбирать HTTP/2

  • Совместимость с gRPC — подключайте сервисы RPC Dart к существующей gRPC инфраструктуре, инструментам и контрактам без прослойки трансляции.
  • Большое количество параллельных вызовов — выполняйте десятки запросов через одно соединение вместо управления пулом HTTP/1.1 клиентов.
  • Интеграция с сервис-мешем — HTTP/2 поддерживается Envoy, Istio и другими mesh-решениями, поэтому маршрутизация, трассировка и TLS-политики наследуются автоматически.

Клиентская настройка

Создайте транспорт с помощью RpcHttp2CallerTransport.connect и передайте его в RpcCallerEndpoint:

Future<void> main() async {
  final transport = await RpcHttp2CallerTransport.connect(
host: 'localhost',
port: 8765,
logger: RpcLogger('Http2Client'),
  );

  final endpoint = RpcCallerEndpoint(
transport: transport,
debugLabel: 'demo-http2-client',
  );

  final response = await endpoint.unaryRequest<RpcString, RpcString>(
serviceName: 'DemoService',
methodName: 'Echo',
requestCodec: RpcString.codec,
responseCodec: RpcString.codec,
request: RpcString('Hello over HTTP/2!'),
  );

  print('Ответ сервера: ${response.value}');

  await transport.close();
}

Для подключения по TLS используйте RpcHttp2CallerTransport.secureConnect — он настраивает HTTPS и ALPN h2 за один вызов:

final transport = await RpcHttp2CallerTransport.secureConnect(
  host: 'api.example.com',
  port: 443,
  logger: RpcLogger('Http2Client'),
);

Клиентские транспорты реализуют интерфейс IRpcTransport: метод transport.health() возвращает снимок состояния RpcHealthStatus, а transport.reconnect() переподключает HTTP/2 соединение через сохранённую фабрику соединений.

Запуск сервера

Самый быстрый вариант — RpcHttp2Server.createWithContracts. Он открывает TCP порт, поднимает HTTP/2 соединение и создаёт отдельный RpcResponderEndpoint для каждого клиента:

final server = RpcHttp2Server.createWithContracts(
  port: 8765,
  host: '0.0.0.0',
  logger: RpcLogger('Http2Server'),
  contracts: [
DemoServiceContract(),
  ],
);

await server.start();

// ... при завершении работы
await server.stop();

Конструктор RpcHttp2Server используйте, когда нужны хуки жизненного цикла. Колбэки onEndpointCreated, onConnectionOpened и onConnectionClosed позволяют подключить метрики, динамически регистрировать контракты или применять middleware во время работы.

Пример контракта

Так выглядит контракт, который используется в примерах ниже: он регистрирует унарный метод, серверный стрим и двунаправленный стрим с явными кодеками:

class DemoServiceContract extends RpcResponderContract {
  DemoServiceContract() : super('DemoService');

  @override
  void setup() {
addUnaryMethod<RpcString, RpcString>(
  methodName: 'Echo',
  handler: _echo,
  requestCodec: RpcString.codec,
  responseCodec: RpcString.codec,
);

addServerStreamMethod<RpcString, RpcString>(
  methodName: 'GetStream',
  handler: _getStream,
  requestCodec: RpcString.codec,
  responseCodec: RpcString.codec,
);

addBidirectionalMethod<RpcString, RpcString>(
  methodName: 'Chat',
  handler: _chat,
  requestCodec: RpcString.codec,
  responseCodec: RpcString.codec,
);
  }

  Future<RpcString> _echo(RpcString request, {RpcContext? context}) async {
return RpcString('echo: ${request.value}');
  }

  Stream<RpcString> _getStream(
RpcString request, {
RpcContext? context,
  }) async* {
for (var i = 0; i < 3; i++) {
  await Future.delayed(const Duration(milliseconds: 200));
  yield RpcString('${request.value} #$i');
}
  }

  Stream<RpcString> _chat(
Stream<RpcString> requests, {
RpcContext? context,
  }) async* {
await for (final message in requests) {
  yield RpcString('received: ${message.value}');
}
  }
}

Пользовательские листенеры и TLS

Если слушатель уже существует (например, за reverse-proxy, в sidecar сервис-меша или через SecureServerSocket.bind), создайте RpcHttp2ResponderTransport вручную:

Future<void> serveOverTls(SecurityContext context) async {
  final secureSocket = await SecureServerSocket.bind(
'0.0.0.0',
8443,
context,
supportedProtocols: const ['h2'],
  );

  secureSocket.listen((SecureSocket socket) {
final connection = http2.ServerTransportConnection.viaSocket(socket);
final transport = RpcHttp2ResponderTransport(
  connection: connection,
  logger: RpcLogger('Http2Responder'),
);

final endpoint = RpcResponderEndpoint(transport: transport);
endpoint.registerServiceContract(DemoServiceContract());
endpoint.start();
  });
}

Такой endpoint также реализует IRpcTransport, поэтому middleware, перехватчики и health-check работают одинаково на всех транспортах.

Потоки и типы RPC

HTTP/2 транспорт поддерживает все четыре варианта взаимодействия, а клиентский API совпадает с другими транспортами:

final endpoint = RpcCallerEndpoint(transport: transport);

// Серверный стрим
final updates = endpoint.serverStream<RpcString, RpcString>(
  serviceName: 'DemoService',
  methodName: 'GetStream',
  requestCodec: RpcString.codec,
  responseCodec: RpcString.codec,
  request: RpcString('stream please'),
);
await for (final update in updates) {
  print('update: ${update.value}');
}

// Двунаправленный стрим
final responses = endpoint.bidirectionalStream<RpcString, RpcString>(
  serviceName: 'DemoService',
  methodName: 'Chat',
  requestCodec: RpcString.codec,
  responseCodec: RpcString.codec,
  requests: Stream.periodic(
const Duration(milliseconds: 250),
(i) => RpcString('message #$i'),
  ).take(3),
);
await for (final reply in responses) {
  print('reply: ${reply.value}');
}

Диагностика и устойчивость

И клиентский, и серверный транспорт реализуют IRpcTransport, поэтому:

  • transport.health() выдаёт актуальное состояние для вашей системы мониторинга.
  • transport.reconnect() на клиенте восстанавливает соединение после обрыва. Серверный транспорт сообщает, что ручное переподключение не поддерживается, и endpoint корректно завершается.
  • transport.close() освобождает HTTP/2 потоки при остановке endpoint.

Дополните эти возможности RpcEndpoint.health() и RpcEndpointPingProtocol из rpc_dart, чтобы построить liveness/readiness проверки.

Совместимость с gRPC

RPC Dart выставляет gRPC-заголовки и фрейминг, поэтому любой gRPC-клиент может общаться с сервисом, запущенным через RpcHttp2Server:

grpcurl -plaintext -d '{"a": 10, "b": 5}' \
  localhost:8765 DemoService/Add

А клиентский транспорт может ходить к существующим gRPC сервисам — достаточно, чтобы имена сервисов и методов совпадали с контрактами, сгенерированными из их protobuf-описаний.