Привет, я Андрей, работаю 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 где мы продолжим добавлять полезные возможности нашему сервису на основе потока данных от клиента к сервису и двунаправленного потока данных.
furic
Кому интересен стартер для Spring boot - https://github.com/LogNet/grpc-spring-boot-starter
Disclosure : я - автор