Русский
Русский
English
Статистика
Реклама

GRPC Dart, Сервис Клиент, напишем

Привет! Меня зовуте Андрей и я работаю разработчиком Flutter.

Написание материала вызвано желанием показать пример создания сервиса c использованием технологии gRPC в экосистеме Dart и, соответственно, Flutter. Желание периодически возникает, когда приходится испытывать "боль", при переключении на проекты, в которых до сих пор применяется REST + JSON.

Планирую сделать серию из 3-4 статей.

Кратко о gRPC

gRPC (Remote Procedure Calls от Гугл) технология для создания информационных систем (сервисов и клиентских приложений).

Для сериализации данных и их передачи по сети, как правило, в связке с gRPC используется Protocol Buffers (Protobuf).

Protobuf применяется и как IDL (Interface Definition Language) для описания типов данных и вызываемых процедур.

Технология gRPC является достойной альтернативой широко распространённым подходам, при которых сетевые вызовы используют HTTP методы, а обмен данными происходит в формате JSON или XML.

Основные преимущества gRPC это:

  1. HTTP/2 в качестве транспорта

  2. Отсутствие привязок к HTTP-методам при взаимодействии компонентов системы

  3. Возможность использования Protocol Buffers (Protobuf) для сериализации / десериализации данных и их передачи по сети

  4. Protobuf IDL удобен для описания системы

  5. Нет необходимости вручную писать модели, сериализацию / десериализацию данных, интерфейсы вызовов процедур. Применяется кодогенерация для популярных языков программирования, в том числе и для Dart

Упрощённый пример gRPC системыУпрощённый пример 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:

@overrideFuture<Question> getQuestion(ServiceCall call, Student request) async {  print('Received question request from: $request');  return questionsDb[Random().nextInt(questionsDb.length)];}@overrideFuture<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
К списку статей
Опубликовано: 18.06.2021 16:08:17
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Dart

Flutter

Дарт

Флаттер

Grpc

Protobuf

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru