Привет, я Андрей, работаю 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/
Добавить комментарий