gRPC + Dart, Сервис + Клиент, напишем? Часть 2

от автора

Первая часть находится здесь

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

Давайте развивать сервис Umka основы которого мы заложили в первой части.

Реализация отправки ответа на полученный вопрос

Для начала чуть изменим нашу «базу вопросов», таким образом, чтобы она содержала правильный ответ к каждому вопросу:

[     {         "id": 0,         "text": "7 x 5 = ?",         "answer": "35"     },     {         "id": 1,         "text": "12 x 13 = ?",         "answer": "156"     },     {         "id": 2,         "text": "2 ** 5 = ?",         "answer": "32"     },     {         "id": 3,         "text": "2 ** 10 = ?",         "answer": "1024"     },     {         "id": 4,         "text": "2 ** 11 = ?",         "answer": "2048"     } ] 

В файл lib/questions_db_driver.dart добавим метод getCorrectAnswerById, для получения корректного ответа по идентификатору вопроса и сделаем небольшой рефакторинг кода:

import 'dart:io'; import 'dart:convert';  import 'generated/umka.pb.dart';  final List<Question> questionsDb = _readDb();  List _getQuestionsList() {   final jsonString = File('db/questions_db.json').readAsStringSync();   return jsonDecode(jsonString); }  List<Question> _readDb() => _getQuestionsList()     .map((entry) => Question()       ..id = entry['id']       ..text = entry['text'])     .toList();  String? getCorrectAnswerById(int questionId) {   final jsonList = _getQuestionsList();    final correctAnswer = jsonList.firstWhere(     (element) => element['id'] == questionId,     orElse: () => null,   );    return correctAnswer?['answer']; } 

В класс UmkaService добавим реализацию метода sendAnswer в котором:

  • получим из «базы» правильный ответ

  • если по какой-то причине «клиент» передал несуществующий идентификатор вопроса «выбросим» ошибку

    throw grpc.GrpcError.invalidArgument('Invalid question id!');

  • оценим ответ (за правильный в поле mark запишем 5, за неверный «влепим двойку») и вернём оценку «клиенту»

  @override   Future<Evaluation> sendAnswer(ServiceCall call, Answer request) async {     print('Received answer for the question: $request');      final correctAnswer = getCorrectAnswerById(request.question.id);      if (correctAnswer == null) {       throw grpc.GrpcError.invalidArgument('Invalid question id!');     }      final evaluation = Evaluation()       ..id = 1       ..answerId = request.id;      if (correctAnswer == request.text) {       evaluation.mark = 5;     } else {       evaluation.mark = 2;     }     return evaluation;   } 

Остальной код в файле lib/service.dart без изменений.

Реализация метода sendAnswer на стороне клиентского приложения такая:

  Future<void> sendAnswer(Student student, Question question) async {     final answer = Answer()       ..question = question       ..student = student;      print('Enter your answer: ');      answer.text = stdin.readLineSync()!;      final evaluation = await stub.sendAnswer(answer);      print('Evaluation for the answer: ${answer.text} '         '\non the question ${question.text}:'         '\n$evaluation');   } 
  • создаем «экземпляр» класса Answer

  • добавляем в него текст ответа введённый «студентом» в терминал

  • отправляем ответ на «оценку»

  • дождавшись оценки от сервиса выводим её в консоль

Также чуть изменим метод обращения к сервису Umka callService:

  Future<void> callService(Student student) async {     final question = await getQuestion(student);     await sendAnswer(student, question);     await channel.shutdown();   } 

Здесь все просто:

  • запрашиваем у сервиса вопрос

  • отправляем на него ответ

  • закрываем соединение

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

Для запуска сервиса на localhost из директории проекта выполним команду:

dart lib/service.dart

В окне терминала сервиса будут видны логи отправленных ответов. Чтобы завершить работу сервиса, можно нажать ctrl+c.

Подключаемся к сервису терминальным клиентом

Командой dart lib/client.dart в соседнем окне терминала из папки проекта запустим нашего «клиента» и представим себя студентом, которому нужно ответить на полученный вопрос. Для этого читаем вопрос, и в терминал в виде числа вводим ответ. После этого нам «прилетит» оценка mark: 5 или mark: 2.

Демонстрация вышеописанного:

Ошибки gRPC

Давайте «заставим» вызов sendAnswer прислать нам ошибку. Для этого подменим question.id на нелепый, например так:

  Future<void> callService(Student student) async {     final question = await getQuestion(student);     question.id = 777;     await sendAnswer(student, question);     await channel.shutdown();   } } 

Сервис пришлет нам ошибку

Unhandled exception: gRPC Error (code: 3, codeName: INVALID_ARGUMENT, message: Invalid question id!, details: [], rawResponse: null) 

Демонстрация:

Ошибки, конечно же, требуют корректной обработки.

Отправка потокa данных клиентскому приложению

Давайте добавим нашему сервису возможность обучать «студентов». Для этого организуем периодическую отправку вопросов клиентскому приложению вместе с ответом на него.

Здесь нам и пригодится возможность gRPC отправлять stream c сервера «клиентам».

Добавим к описанию нашего сервиса в файл protos/umka.proto один тип:

message AnsweredQuestion {   Question question = 1;   string answer = 2; }

И один удалённый вызов:

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

Обратите внимание на аннотацию stream перед возвращаемым типом. Именно она «решает», что при вызове данной процедуры клиент будет получать поток данных, а не одиночный ответ.

Теперь описание выглядит так:

Мы изменили описание сервиса, поэтому нужна «регенерация» gRPC Dart кода, и мы в папке проекта просто запускаем знакомую уже команду:

protoc -I protos/ protos/umka.proto --dart_out=grpc:lib/generated

Можно заглянуть во вновь сгенерированный код и убедиться, что там появился новый «вызов» в виде метода getTutorial класса UmkaServiceBase в файле umka.pbgrpc.dart и класс AnsweredQuestion в файле umka.pb.dart.

Код метода getTutorial для сервиса

Перейдя в файл lib/service.dart обнаруживаем «ворчание» компилятора на то, что в классе UmkaService отсутствует реализация метода getTutorial.

Напишем его код следующим образом:

  @override   Stream<AnsweredQuestion> getTutorial(       ServiceCall call, Student request) async* {     for (var question in questionsDb) {       final answeredQuestion = AnsweredQuestion()         ..question = question         ..answer = getCorrectAnswerById(question.id)!;        yield answeredQuestion;        await Future.delayed(Duration(seconds: 2));     }   } } 

В Dart для того, чтобы функция возвращала стрим её нужно пометить ключевым словом async*. После этого в стрим объекты указанного типа Stream<AnsweredQuestion> отправляются с помощью ключевого слова yield.

Метод getTutoriad по одному будет брать вопросы из «базы», отправлять их «студенту» в клиентское приложение, делать паузу 2 секунды для «подумать». Процесс будет повторяться пока не закончатся данные. После этого соединение, установленное при вызове метода getTutorial, будет прервано.

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

Изменения здесь небольшие:

  • добавим в UmkaTerminalClient метод запроса урока takeTutorial,

  • изменим обращение к сервису сосредоточившись только на «уроке».

  Future<void> takeTutorial(Student student) async {     await for (var answeredQuestion in stub.getTutorial(student)) {       print(answeredQuestion);     }   }    Future<void> callService(Student student) async {     await takeTutorial(student);     await channel.shutdown();   } } 

Dart конструкция await for позволяет удобно брать данные из потока до тех пор, пока поток не «иссякнет», после чего метод takeTutorial завершится. Текст вопросов с ответами на них просто печатаем в консоль.

Запускаем dart lib/service.dart в одном терминальном окне и dart lib/client.dart в другом, и наблюдаем поток вопросов в клиентский терминал:

На этом вторая и завершим вторую часть.

Мы поработали над развитием нашей системы добавив полезные «фичи»:

  • Отправка на сервер ответа на полученный вопрос.

  • Получение обучающего материала в виде потока задачек с ответами.

Посмотрели как «прилетает» ошибка.

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

До встречи в части №3 где мы продолжим добавлять полезные возможности нашему сервису на основе потока данных от клиента к сервису и двунаправленного потока данных.

ссылка на оригинал статьи https://habr.com/ru/post/563986/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *