Привет, я Андрей, работаю Flutter разработчиком в компании Финам.

После выхода 4й части, где мы подключили мобильное Flutter приложение к сервису Umka, я получил много вопросов от читателей, которые попробовали запустить Web версию приложения и оно в браузере не заработало.

Так будет ли Flutter приложение на базе gRPC сервиса работать в Web?

TLDR: Да, но не получится "стримить" со стороны клиента, а всё остальное будет работать. Для этого нужно сплясать с бубном преобразовать запросы на сервис и ответы с него в формат понятный для браузера. Можно использовать Envoy в качестве Web proxy, который "из коробки" поддерживает входящие/исходящие gRPC запросы.

Ниже я покажу как это сделать. Хочу отметить, что в Гугл идет работа по развитию gRPC для Web и со временем необходимость в "посреднике" может отпасть.

Конфигурация для Envoy proxy

Давайте поместим umka_envoy.yaml файл в проект нашего сервиса. Конфигурация выглядит следующим образом:

static_resources:
  listeners:
  - name: umka_listener
    address:
      socket_address: { address: 0.0.0.0, port_value: 8888 }
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: umka_route
            virtual_hosts:
            - name: umka_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: umka_service
                  timeout: 0s
                  max_stream_duration:
                    grpc_timeout_header_max: 0s
              cors:
                allow_origin_string_match:
                - prefix: "*"
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                max_age: "1728000"
                expose_headers: custom-header-1,grpc-status,grpc-message
          http_filters:
          - name: envoy.filters.http.grpc_web
          - name: envoy.filters.http.cors
          - name: envoy.filters.http.router
  clusters:
  - name: umka_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    load_assignment:
      cluster_name: cluster_0
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 0.0.0.0
                    port_value: 5555

Кратко суть в следующем: клиентским приложением Umka, запущенном в браузере мы будем отправлять запросы и получать ответы на сервер Envoy взаимодействуя с ним через порт 8888. Прокси же в свою очередь будет перенаправлять эти запросы преобразованные в gRPC вызовы на порт 5555, на котором мы и запускали наш сервис в предыдущих четырёх частях. Кластер в нашем примере будет состоять из одного узла (локальный компьютер), на котором и будут работать и Umka sevice и Envoy.

У Envoy хорошая документация и при желании можно познакомиться с этим замечательным продуктом подробнее.

Доработка Flutter приложения

В зависимости от того запущена Web версия Flutter приложения или мобильная, клиентский канал для удаленных gRPC вызовов нужно строить по-разному.

"По дефолту" канал создаётся так:

ClientChannel buildChannel({
    required String host,
    int port = 443,
    bool secure = true,
  }) {
    return ClientChannel(host,
        port: port,
        options: ChannelOptions(
            credentials: secure
                ? ChannelCredentials.secure()
                : ChannelCredentials.insecure()));
}

В случае запуска в Web так:

ClientChannel buildChannel({
    required String host,
    int port = 443,
  }) {
    return GrpcWebClientChannel.xhr(Uri.parse('$host:$port'));
}

Я написал небольшую утилитку build_grpc_channel, которая именно этим и "занимается".

Добавим её в зависимости нашего приложения:

dependencies:
  ...
  build_grpc_channel:
  ...

Чуть изменим код класса UmkaService:

const host = 'http://127.0.0.1';

int get port => kIsWeb ? 8888 : 5555;

class UmkaService {
  late final UmkaClient stub;

  UmkaService() {
    stub = UmkaClient(buildGrpcChannel(host: host, port: port, secure: false));
  }
  ...
}

Канал строим с помощью метода buildGrpcChannel(host: host, port: port, secure: false) из утилиты build_grpc_channel. При запуске в Web передаем порт 8888, на котором запросы будет слушать Envoy.

Вот и все изменения, которые нужно сделать, чтобы приложение заработало в браузере.

Запускаем

На машине должен быть установлен Envoy.

Пример установки на маке: brew install envoy.

Из директории, где расположен конфигурационный файл umka_envoy.yaml выполним команду запуска прокси-сервера:

envoy --config-path umka_envoy.yaml или envoy -c umka_envoy.yaml

Соберём проект для работы в Web и перейдем в его директорию:

flutter build web && cd build/web

Запустим локальный сервер из данной директории build/web, где расположен файл index.html, например php:

php -S localhost:8080

Или с помощью Python:

python -m http.server 8080

Теперь можно открыть в браузере адрес localhost:8080 и проверить работу приложения.

Ложка дёгтя...

Мы видим, что вкладки Quiz и Tutorial работают точно так же, как и мобильной версии приложения, но вот вкладка Exam не работает. Происходит это потому, что код для экзамена подразумевает использование потока данных с "клиента" на сервис

rpc takeExam(stream Answer) returns(Evaluation) {},

а это, на данный момент времени, в gRPC Web не поддерживается.

В направлении от сервиса к "клиенту" стрим работает. Мы видим это по нормальной работе вкладки Tutorial, где используется вызов:

rpc getTutorial(Student) returns (stream AnsweredQuestion) {}

Спасибо всем, кто следил за данной серией статей или прочитал позже. Надеюсь, что было полезно.

Комментарии (3)


  1. Mitai
    18.11.2021 12:12
    +1

    на http3 не пробовали? ну просто мало ли а вдруг, там не нужен будет енвой и с вебом будет все нормуль, он же на UDP


    1. Andrey_chik Автор
      19.11.2021 23:27

      Тема нужная, слежу за этим.

      Пока вопрос поддержки HTTP/3 & QUIC языком Dart "открыт":

      https://github.com/dart-lang/sdk/issues/38595
      https://github.com/grpc/grpc-dart/issues/374

      Очень хотелось бы, чтобы это стало возможно и я бы смог дополнить данную серию 6й частью, раскрыв эту тему.


      1. Mitai
        21.11.2021 11:30

        Это будет просто ачешуенно!
        Благодахрю за ссылочки, добавлю в отслеживаемые))