gRPC + Dart, Service + Client, shall we write? Part 3

  • The first part is here





  • The second part is here





Hi, I'm Andrey, I work as a Flutter developer at Finam.





We will continue to develop the Umka service.





Exam

Using the example of implementing the code for conducting the "exam", we will get acquainted with the ability of the gRPC technology to transfer data as a stream from the client application to the service.





Let the exam script be like this:





  • The student asks the "Exam" service, which is a list of questions.





  • The student, in turn, for each question, enters the answer into the console, which is immediately displayed on the server side of the service.





  • If the answer is correct, then the service adds 1 to the exam grade.





  • After evaluating the last question, the service sends the result to the student.





Add a description

We complete description of one type of service Exam



and the two calls 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) {}
}

      
      



The repeated keyword indicates that the field questions



contains a list. For the Dart language, this will be List<Question>



.





In the description of a remote call, the streamrpc takeExam(stream Answer) returns(Evaluation) {}



annotation before the passed type tells the protoc compiler that the code must be generated in such a way that the service from the client application receives responses as a data stream.Answer







Let's "regenerate" the base Dart code:





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







Supplementing the server code of the service

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 .





Thus, by this moment we have had time to get acquainted with the main features of gRPC.





After a break, in about a month or two, I plan to continue this series. We plan to consider an example of creating a simple mobile Flutter application for working with the Umka service, "deploying" the service to a real server, ....





See you in the next part!








All Articles