Привет, я Андрей, работаю 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.
Метод getTutorial по одному будет брать вопросы из "базы", отправлять их "студенту" в клиентское приложение, делать паузу 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 где мы продолжим добавлять полезные возможности нашему сервису на основе потока данных от клиента к сервису и двунаправленного потока данных.
