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

от автора

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

Продолжим развивать сервис Umka.

Экзамен

На примере реализации кода для проведения «экзамена» мы познакомимся с возможностью технологии gRPC передавать данные в виде потока от клиентского приложения на сервис.

Сценарий экзамена пусть будет таким:

  • Ученик запрашивает у сервиса «Экзамен», представляющий из себя список вопросов.

  • Ученик поочерёдно, на каждый вопрос, вводит в консоль ответ, который тут же отображается на серверной стороне сервиса.

  • Если ответ верен, то сервис к экзаменационной оценке добавляет 1.

  • После оценки последнего вопроса, сервис отправляет ученику результат.

Добавляем описание

Дополним описание сервиса одним типом Exam и двумя вызовами getExam, takeExam:

syntax="proto3";  message Student {   int32 id = 1;   string name = 2; }  message Question {   int32 id = 1;   string text = 2; }  message Answer {   int32 id = 1;   Student student = 2;   Question question = 3;   string text = 4; }  message Evaluation {   int32 id = 1;   int32 answerId = 2;   int32 mark = 3; }  message AnsweredQuestion {   Question question = 1;   string answer = 2; }  message Exam {   int32 id = 1;   repeated Question questions = 2; }  service Umka {   rpc getQuestion(Student) returns(Question) {}    rpc sendAnswer(Answer) returns(Evaluation) {}    rpc getTutorial(Student) returns (stream AnsweredQuestion) {}    rpc getExam(Student) returns (Exam) {}    rpc takeExam(stream Answer) returns(Evaluation) {} } 

Ключевое слово repeated говорит о том, что поле questions содержит список. Для языка Dart это будет List<Question>.

В описании удаленного вызова rpc takeExam(stream Answer) returns(Evaluation) {} аннотация stream перед передаваемым типом Answer сообщает компилятору protoc, что код нужно сгенерировать таким образом, чтобы сервис от клиентского приложения получал ответы в виде потока данных.

Выполним «регенерацию» базового Dart кода:

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

Дополняем серверный код сервиса

В класс UmkaService добавим реализацию метода getExam для получения вопросов экзамена от сервиса:

  @override   Future<Exam> getExam(ServiceCall call, Student request) async {     final exam = Exam()..id = 1;     exam.questions.addAll(questionsDb);     return exam;   } 

Клиентскому приложению отправляются все вопросы из нашей «базы».

Реализация метода takeExam потребует чуть больше кода:

  @override   Future<Evaluation> takeExam(ServiceCall call, Stream<Answer> asnswers) async {        var score = 0;          await for (var answer in asnswers) {       final isCorrect = getCorrectAnswerById(answer.question.id) == answer.text;              print('Received an answer from ${answer.student.name}\n'           'for a question: ${answer.question.text}'           'answer: ${answer.text} is correct: $isCorrect');                  if (isCorrect) {         score++;       }     }      print('The student: ${call.clientMetadata?['student_name']}'         ' finished exam with the score: $score');              return Evaluation()       ..id = 1       ..mark = score;   } } 
  • В await for получаем по одному ответы из потока, созданного клиентским приложением.

  • В переменную var score = 0; добавляем балл за каждый правильный ответ.

  • После отправки последнего ответа, «клиент» закроет стрим, а мы (сервис) вернём ему оценку.

  • В консоль выводим полезную информацию по ходу «экзамена».

Кусочек кода call.clientMetadata?['student_name'] показывает пример, как можно получить дополнительную информацию из метаданных отправленных клиентским приложением. По своей сути, «под капотом», это один из заголовков HTTP/2 запроса.

Дополняем код клиента

В UmkaTerminalClient добавим метод takeExam:

  Future<Evaluation> takeExam(Student student) async {     final exam = await stub.getExam(student);      final questions = exam.questions;      final answersStream = StreamController<Answer>();      final evaluationFuture = stub.takeExam(answersStream.stream,         options: CallOptions(metadata: {'student_name': '${student.name}'}));      for (var question in questions) {       final answer = Answer()         ..question = question         ..student = student;        print('Enter the answer for the question: ${question.text}');              answer.text = stdin.readLineSync()!;        answersStream.add(answer);        await Future.delayed(Duration(milliseconds: 1));     }     unawaited(answersStream.close());      return evaluationFuture;   } 
  • Получаем вопросы.

  • Создаем поток для передачи ответов.

  • Устанавливаем соединение, передав на сервис созданный «стрим» и метаданные: 'student_name'.

  • На каждый полученный вопрос, по очереди, ученик вводит ответ в терминал.

  • После формирования ответ добавляется в поток: answersStream.add(answer);.

  • Отправив последний ответ, немедленно закрываем поток данных: unawaited(answersStream.close());

  • Возвращаем Future с оценкой.

Функция unawated нужна чтобы сказать компилятору, что мы уверены в своих действиях и он не показывал «предупреждение».

Она пока доступна только из библиотеки pedantic, поэтому добавим ее в зависимости:

Выполним команду dart pub get.

Метод обращения к сервису напишем так:

  Future<void> callService(Student student) async {     final evaluation = await takeExam(student);     print('${student.name}, your exam score is: ${evaluation.mark}');     await channel.shutdown();   } 
  • Дожидаемся окончания «экзамена», чтобы получить оценку.

  • Выводим результат в консоль.

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

Запуск экзамена

В разных терминальных окнах стартуем сервис и клиентское приложение

  • Сервис: dart lib/service.dart

  • Клиентское приложение: dart lib/client.dart

Демонстрация прохождения «экзамена»:

«Студент» Ваня опечатался при ответе на третий вопрос и получил «четвёрку».

Техническое интервью

Мы подошли к самому интересному. Парой-тройкой десятков строчек кода мы реализуем чат, для проведения технического интервью.

Для этого используем возможность gRPC осуществлять двунаправленную потоковую передачу данных от сервиса к клиентскому приложению и обратно в рамках одного HTTP/2 соединения.

В описание сервиса добавим тип:

message InterviewMessage {   string name = 1;   string body = 2; } 

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

rpc techInterview(stream InterviewMessage) returns(stream InterviewMessage) {} 

Вновь выполним «регенерацию» запустив в папке проекта команду:

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

Код сервиса

На верхний уровень файла lib/service.dart добавим константу с типчными вопросами «собеседования»:

const interviewQuestions = [   'What was wrong in your previous job place?',   'Why do you want to work for Us?',   'Who do you see yourself in 5 years?',   'We will inform you about the decision. Bye!', ]; 

В класс UmkaService добавим вспомогательную функцию:

  InterviewMessage _createMessage(String text, {String name = 'Interviewer'}) =>       InterviewMessage()         ..name = name         ..body = text; 

А также напишем реализацию метода techInterview:

  @override   Stream<InterviewMessage> techInterview(       ServiceCall call, Stream<InterviewMessage> interviewStream) async* {     var count = 0;      await for (var message in interviewStream) {       print('Candidate ${message.name} message: ${message.body}');       if (count >= interviewQuestions.length) {         return;       } else {         yield _createMessage(interviewQuestions[count++]);       }     }   } 
  • После получения первого сообщения от кандидата, что он готов к интервью, отправляем ему вопросы по одному.

  • На каждый отправленный вопрос дожидаемся ответа.

  • Получив ответ на последний вопрос, «выходим» — стрим interviewStream на стороне клиента будет закрыт.

Код клиентского приложения

«На клиенте» код метода techInterview пусть будет такой:

  Future<void> techInterview(String candidateName) async {     final candidateStream = StreamController<InterviewMessage>();     final interviewerStream = stub.techInterview(candidateStream.stream);      candidateStream.add(InterviewMessage()       ..name = candidateName       ..body = 'I am ready!');      await for (var message in interviewerStream) {       print('\nMessage from the ${message.name}:\n${message.body}\n');        print('Enter your answer:');        final answer = stdin.readLineSync();        candidateStream.add(InterviewMessage()..body = answer!);     }      unawaited(candidateStream.close());   } 
  • Создаём стрим final candidateStream = StreamController<InterviewMessage>();.

  • Передаём candidateStream удаленному вызову, получая обратно interviewerStream.

  • Отправляем информацию, что кандидат готов к интервью, чтобы завязать «диалог».

  • Следим за вопросами поступающими в interviewerStream.

  • Ответы вводим в терминал.

  • Читаем введённые ответы и отправляем их на сервис.

  • После завершения потока вопросов от сервиса, закрываем соединение.

К сервису на этот раз будем обращаться так:

  Future<void> callService(Student student) async {     await techInterview(student.name);     await channel.shutdown();   } 

Вот гифка демонстрирующая процесс «интервью». В нижнем окне автоматический интервьюер, в верхнем кандидат отвечает на вопросы.

На этом и завершим третью часть где мы, реализовав для нашего сервиса две полезные функции прохождения экзамена и проведения технического интервью, использовали ещё две полезные возможности, предоставляемые технологией gRPC:

  • Передача данных в виде потока от клиентского приложения к сервису.

  • Двунаправленная потоковая передача данных между «клиентом» и «сервером» в рамках одного HTTP/2 соединения.

Таким образом, к этому моменту мы успели познакомиться с основными возможностями gRPC.

После перерыва, примерно через месяц-другой, я планирую продолжить данную серию. В планах рассмотреть пример создания простенького мобильного Flutter приложения для работы с сервисом Umka, «деплой» сервиса на реальный сервер, … .

До встречи в следующей части!

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


Комментарии

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

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