[Dart, Flutter] gRPC + Dart, Сервис + Клиент, напишем

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
18-Июн-2021 17:35

Привет! Меня зовуте Андрей и я работаю разработчиком Flutter.Написание материала вызвано желанием показать пример создания сервиса c использованием технологии gRPC в экосистеме Dart и, соответственно, Flutter. Желание периодически возникает, когда приходится испытывать "боль", при переключении на проекты, в которых до сих пор применяется REST + JSON.Планирую сделать серию из 3-4 статей.Кратко о gRPCgRPC (Remote Procedure Calls от Гугл) технология для создания информационных систем (сервисов и клиентских приложений).Для сериализации данных и их передачи по сети, как правило, в связке с gRPC используется Protocol Buffers (Protobuf).Protobuf применяется и как IDL (Interface Definition Language) для описания типов данных и вызываемых процедур.Технология gRPC является достойной альтернативой широко распространённым подходам, при которых сетевые вызовы используют HTTP методы, а обмен данными происходит в формате JSON или XML.Основные преимущества gRPC это:
  • HTTP/2 в качестве транспорта
  • Отсутствие привязок к HTTP-методам при взаимодействии компонентов системы
  • Возможность использования Protocol Buffers (Protobuf) для сериализации / десериализации данных и их передачи по сети
  • Protobuf IDL удобен для описания системы
  • Нет необходимости вручную писать модели, сериализацию / десериализацию данных, интерфейсы вызовов процедур. Применяется кодогенерация для популярных языков программирования, в том числе и для Dart

Упрощённый пример gRPC системыПример написания сервиса и клиентаВ примере приведено несколько CLI команд, которые для вашей системы могут чуть разниться.Подготовка среды разработкиЕсли на машине нет Dart SDK его нужно установить. Пример команды установки для Mac brew install dart, для Ubuntu 20.4 sudo apt install dart.Проверить, что Dart успешно установлен dart --version.Установить protobuf (пример для Mac brew install protobuf, пример для Ubuntu 20.4 sudo apt install -y protobuf-compiler).Проверить, что все прошло успешно protoc --version.
Установить плагин для кодогенерации .proto файлов, описывающих систему, в Dart:dart pub global activate protoc_plugin.Pub устанавливает утилиты в $HOME/.pub-cache/bin.Чтобы плагин был доступен из любой директории в вашем терминале, добавьте в его конфигурационный файл (.bashrc, .bash_profile, .zshrc и т.п.) строчку export PATH="$PATH":"$HOME/.pub-cache/bin" и перезагрузите терминал (или выполните команду source на обновленный файл).Подготовка проектаВ качестве примера давайте сделаем сервис, который будет задавать "Клиентам" вопросы и получать от них ответы. Название пусть будет "Umka".В выбранной папке создаем проект:dart create umkaПерейдя в папку проекта добавим директорию protos/ и в неё файл umka.proto, в котором мы и опишем нашу систему:mkdir protos && touch protos/umka.protoДля исходного кода сделаем папку lib/ с файлами service.dart и client.dart:mkdir lib && touch lib/service.dart lib/client.dartСоздадим также папку для сгенерированного кода:mkdir lib/generatedВ результате структура проекта выглядит следующим образом:
Добавление зависимостейВ качестве сторонней библиотеки нам пока потребуется только grpc. Остальные зависимости она "подтянет" сама.Добавим её в pubspec.yaml и удалим из файла все лишнее:
Для загрузки из pub.dev репозитория библиотеки и её зависимостей в папке проекта выполним команду: dart pub getОписание системы в с помощью IDL proto3Опишем наш сервис Umka следующим образом:
Кому лень печатать, прогуляйтесь по ссылке на код.В первой строчке обязательно нужно указать версию IDL syntax="proto3";.Строки с 3 по 24 содержат описание типов передаваемых данных:
  • Ученик
  • Вопрос
  • Ответ
  • Оценка
Обратите внимание, что записи подобные string text = 2; выглядят как присваивание значения, но на самом деле это номера полей, которые используются для их идентификации в бинарном потоке данных при сериализации / десериализации.Типы выглядят как в привычных языках программирования:
  • встроенные (Scalar Value Types) int32 и string
  • созданные Student, Question
В конце файла описан сам сервис, который содержит пока только два вызова:
  • получить вопрос
  • отправить ответ
Структура записи вызова rpc sendAnswer(Answer) returns(Evaluation) {} следующая:
  • sendAnswer - название удаленного вызова
  • Answer - тип запроса
  • Evaluation - тип ответа
Генерируем код сервиса на основе его описания в umka.protoДля этого из папки проекта запустим в терминале команду:protoc -I protos/ protos/umka.proto --dart_out=grpc:lib/generatedРазберем команду:
  • protoc утилита генерации (мы установили ее ранее)
  • -I protos/ указание расположения файлов .proto
  • protos/umka.proto файл описания сервиса
  • --dart_out=grpc:lib/generated grpc - указание плагина, lib/generated - директория для сгенерированного кода
В результате её выполнения в проекте появится 4 новых файла:
Это основа нашего сервиса.Эмуляция работы с даннымиДобавим в корень проекта папку с файлом db/questions_db.json со списком вопросов:
[
    {
        "id": 0,
        "text": "7 x 5 = ?"
    },
    {
        "id": 1,
        "text": "12 x 13 = ?"
    },
    {
        "id": 3,
        "text": "2 ** 5 = ?"
    },
    {
        "id": 4,
        "text": "2 ** 10 = ?"
    },
    {
        "id": 5,
        "text": "2 ** 11 = ?"
    }
]
В папку lib добавим файл lib/questions_db_driver.dart с кодом для получения списка вопросов из нашей импровизированной базы данных:
import 'dart:io';
import 'dart:convert';
import 'generated/umka.pb.dart';
final List<Question> questionsDb = _readDb();
List<Question> _readDb() {
  final jsonString = File('data/questions_db.json').readAsStringSync();
  final List db = jsonDecode(jsonString);
  return db
      .map((entry) => Question()
        ..id = entry['id']
        ..text = entry['text'])
      .toList();
}
Пишем код для сервераВ файле lib/service.dart создадим класс UmkaService, расширив UmkaServiceBase, находящийся в сгенерированном файле lib/generated/umka.pbgrpc.dart:class UmkaService extends UmkaServiceBase {}Добавим реализацию одного из двух обязательных методов абстрактного родительского класса getQuestion, а для второго sendAnswer оставим пока заглушку TODO:
@override
Future<Question> getQuestion(ServiceCall call, Student request) async {
  print('Received question request from: $request');
  return questionsDb[Random().nextInt(questionsDb.length)];
}
@override
Future<Evaluation> sendAnswer(ServiceCall call, Answer request) {
  // TODO: implement sendAnswer
  throw UnimplementedError();
}
Я намеренно оставил имя второго параметра обоих вызовов request - каждый удаленный вызов должен содержать объект запроса, соответствующий типу, описанному в файле umka.proto.В этот же файл lib/service.dart добавим код запуска нашего сервиса на сервере:
class Server {
  Future<void> run() async {
    final server = grpc.Server([UmkaService()]);
    await server.serve(port: 5555);
    print('Serving on the port: ${server.port}');
  }
}
Future<void> main() async {
  await Server().run();
}
Теперь наш сервис готов "служить клиентам на 5555 порту", отвечая пока только на один вызов getQuestion.код сервисаПишем код клиентского приложения для терминалаФайл lib/client.dart
import 'package:grpc/grpc.dart';
import 'generated/umka.pbgrpc.dart';
class UmkaTerminalClient {
  late final ClientChannel channel;
  late final UmkaClient stub;
  UmkaTerminalClient() {
    channel = ClientChannel(
      '127.0.0.1',
      port: 5555,
      options: ChannelOptions(credentials: ChannelCredentials.insecure()),
    );
    stub = UmkaClient(channel);
  }
  Future<Question> getQuestion(Student student) async {
    final question = await stub.getQuestion(student);
    print('Received question: $question');
    return question;
  }
  Future<void> callService(Student student) async {
    await getQuestion(student);
    await channel.shutdown();
  }
}
Future<void> main(List<String> args) async {
  final clientApp = UmkaTerminalClient();
  final student = Student()
    ..id = 42
    ..name = 'Alice Bobich';
  await clientApp.callService(student);
}
ClientChannel channel; является абстракцией сетевых вызовов по протоколу HTTP/2. Можно представить его как канал к виртуальному "gRPC endpoint".stub - экземпляр "клиента" любезно сгенерированного нам утилитой protoc. Вызовы его методов фактически и являются RPC - удалёнными вызовами процедур.В конструкторе мы инициализируем channel передав ему адрес localhost (для запуска локально), произвольный порт, и отключаем для простоты демонстрации "секьюрность".Далее инициализируем stub передав ему созданный channel.Метод запроса случайного вопроса getQuestion очень прост - вызываем соответствующий метод у нашего экземпляра stub, ждём пока вопрос не "прилетит", печатаем его и "возвращаем".Метод callService в классе UmkaTerminalClient присутствует для демонстрации работы.Также для запуска примера в файл client.dart добавлен метод main в котором "создаётся студент" и от его имени запрашивается вопрос у нашего сервиса./Запускаем сервисДля запуска сервиса на localhost из директории проекта выполним команду:dart lib/service.dartСтартуем клиентское приложениеКомандой dart lib/client.dart в другом окне терминала из папки проекта запустим нашего "клиента", который создаст канал, установит соединение с сервисом, запросит случайный вопрос, получит его и разорвёт соединение, заглушив канал.
ЗаключениеНа этом первая часть закончена, мы проделали отличную работу:
  • Подготовили среду разработки
  • Создали Dart проект
  • Добавили все необходимые зависимости
  • Описали нашу систему с помощью IDL proto3
  • Сгенерировали базовый Dart код системы утилитой protoc
  • Добавили "базу вопросов" и код для чтения из неё
  • Написали код для запуска сервиса на сервере
  • Создали терминального "клиента"
  • Запустили сервис на локальной машине и обратились к нему получив запрошенные данные
Далее можно "получать удовольствие" развивая наш сервис и клиентское приложение.В следующих частях мы посмотрим как отвечать сервису, получать от сервиса поток данных, отправлять поток данных на сервис, открывать двунаправленное соединение обмениваясь данными в режиме непрерывного потока между сервисом и клиентским приложением.До встречи в следующей части!
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_dart, #_flutter, #_dart (дарт), #_flatter (флаттер), #_dart, #_flutter, #_grpc, #_protobuf, #_dart, #_flutter
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 13:33
Часовой пояс: UTC + 5