Привет, я Андрей, работаю 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) {}
Спасибо всем, кто следил за данной серией статей или прочитал позже. Надеюсь, что было полезно.
Mitai
на http3 не пробовали? ну просто мало ли а вдруг, там не нужен будет енвой и с вебом будет все нормуль, он же на UDP
Andrey_chik Автор
Тема нужная, слежу за этим.
Пока вопрос поддержки HTTP/3 & QUIC языком Dart "открыт":
https://github.com/dart-lang/sdk/issues/38595
https://github.com/grpc/grpc-dart/issues/374
Очень хотелось бы, чтобы это стало возможно и я бы смог дополнить данную серию 6й частью, раскрыв эту тему.
Mitai
Это будет просто ачешуенно!
Благодахрю за ссылочки, добавлю в отслеживаемые))