Dart The first gRPC server and client with timestamp

eye-catch Dart and Flutter

We established a dev-env to work with gRPC in devcontainer in this post.

The next step is to define our own functions in protocol buffers and implement them.

You can clone my GitHub repository if you want to try it yourself.

Sponsored links

Proto definitions

Let’s define the functions that we want to use for our gRPC server and clientin protocol buffer.

syntax = "proto3";

import "google/protobuf/timestamp.proto";

option go_package = "api-test/grpc/apitest";

service Middle {
  // Unary RPC
  rpc Ping(PingRequest) returns (PingResponse) {}
  // Unary RPC
  rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloResponse {
  string message = 1;
}

message PingRequest {}

message PingResponse {
  google.protobuf.Timestamp timestamp = 1;
}

These definitions are used not only for Dart but also for other languages. I’ve already implemented them in Golang. That’s why option go_package is written there but it’s not necessary for Dart.

We have to give a name to the service. It’s Middle in this case. A service defines functions in it. A service is handled as a class.

service Middle {
  // Unary RPC
  rpc Ping(PingRequest) returns (PingResponse) {}
  // Unary RPC
  rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

Each function has only one message definition for the parameter and the return type even though it’s empty. The same message definition can be used in both the parameter and the return type. I personally prefer to give a different name XxxxRequest and XxxxResponse to make it clear and less mistakes.

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

message PingRequest {}

There are many types that we can use in protocol buffer. string is one of the built-in data types. However, timestamp is not built-in type. If we need to use it, we have to import the external proto file.

import "google/protobuf/timestamp.proto";

message PingResponse {
  google.protobuf.Timestamp timestamp = 1;
}

Once we define our own functions, we can generate the code with the following command that we defined in the last post.

make generate
Sponsored links

Implement gRPC server code

How to start gRPC server

Let’s see how to start gRPC server before implementing gRPC functions.

import 'package:dart_grpc/server/middle.dart';
import 'package:grpc/grpc.dart';

Future<void> main(List<String> arguments) async {
  final server = Server.create(
    services: [MiddleService()],
    codecRegistry: CodecRegistry(codecs: const [
      GzipCodec(),
      IdentityCodec(),
    ]),
  );
  await server.serve(port: 8080);
  print('Server listening on port ${server.port}...');
}

It’s simple. It’s almost the same code as an official example. We need to specify our defined services in services property. Then start the server with the desired port number where it’s listening for gRPC clients.

If the following error message is shown, grpc: is not added to --dart_out option for protoc.

The element type 'MiddleService' can't be assigned to the list type 'Service'.dartlist_element_type_not_assignable

sayHello

Let’s start with sayHello function because ping function has a bit tricky part.

import "dart:convert";
import "dart:io";
import "dart:math";

import "package:dart_grpc/proto/middle.pbgrpc.dart" as rpc;
import "package:dart_grpc/server/timestamp.dart";
import "package:grpc/grpc.dart";
import 'package:path/path.dart' as p;
import 'package:fixnum/fixnum.dart' as $fixnum;

class MiddleService extends rpc.MiddleServiceBase {
  @override
  Future<rpc.HelloResponse> sayHello(
    ServiceCall call,
    rpc.HelloRequest request,
  ) async {
    final response = rpc.HelloResponse()..message = "Hello ${request.name}";
    return response;
  }
}

All functions are defined in an abstract MiddleServiceBase class that has no implementation for our functions. Therefore We have to add @override annotation. The first parameter is always call. We can use it to handle timeout and cancel for example but let’s ignore it for now.

We have to return HelloResponse. It is automatically generated by protoc but it doesn’t have any constructor parameter. I don’t know why it has no parameters but it’s the gRPC code for Dart.

Since any property can’t be passed in the constructor, we assign a value by double dots. It’s the same as the following code.

final response = rpc.HelloResponse();
response.message = "Hello ${request.name}";

Ping

ping function returns a timestamp. This is the tricky part that I struggled with. The generated code doesn’t have any parameters for the constructor as I mentioned above.

@override
Future<rpc.PingResponse> ping(
    ServiceCall call, 
    rpc.PingRequest request,
) async {
  return rpc.PingResponse()
    ..timestamp = TimestampParser.parse(DateTime.now());
}

Conversion for timestamp need to be used in different places if timestamp is used as a data type. Therefore, it’s better to define the conversion logic in a separate class.

How to convert DateTime to timestamp and vice versa

I didn’t find any website that implements timestamp but I found a good example on the official site. The example 4 is written in Java but it can be implemented in Dart too.

import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:dart_grpc/proto/google/protobuf/timestamp.pb.dart';

class TimestampParser {
  // https://protobuf.dev/reference/protobuf/google.protobuf/#timestamp
  static Timestamp parse(DateTime value) {
    final ms = value.millisecondsSinceEpoch;
    final result = Timestamp.create();
    result.seconds = $fixnum.Int64((ms / 1000).round());
    result.nanos = (ms % 1000) * 1000000;
    return result;
  }

  static DateTime from(Timestamp value) {
    final ms = value.seconds * 1000 + (value.nanos / 1000).round();
    return DateTime.fromMillisecondsSinceEpoch(ms.toInt(), isUtc: true);
  }
}

With this class, we can easily convert a DateTime to Timestamp and Timestamp to DateTime.

Implement gRPC client code

We need a client to test the code. Let’s implement it.

How to create a client and connect to a server

The same as the server implementation, we need to create an instance of ClientChannel for a client.

import 'package:dart_grpc/client/middle.dart';
import 'package:dart_grpc/proto/middle.pbgrpc.dart';
import 'package:grpc/grpc.dart' as grpc;

Future<void> main(List<String> arguments) async {
  final channel = grpc.ClientChannel(
    'localhost',
    port: 8080,
    options: grpc.ChannelOptions(
      credentials: grpc.ChannelCredentials.insecure(),
      codecRegistry: grpc.CodecRegistry(codecs: const [
        grpc.GzipCodec(),
        grpc.IdentityCodec(),
      ]),
    ),
  );
  final client = MiddleClient(channel);
  final handler = MiddleServiceHandler(client);

  await handler.ping();
  await handler.sayHello();

  await channel.shutdown();
}

MiddleClient is a class that is automatically generated. MiddleServiceHandler is a class that I defined to implement the client code for each function.

We can call the function by passing an instance of ClientChannel to the XxxxClient where Xxxx is a service name defined in protocol buffer.

Add await keyword to call ping and sayHello because both functions return Future.

sayHello and ping

The client code is simple. Let’s see the implementation for both functions.

import 'dart:async';

import 'package:dart_grpc/proto/middle.pbgrpc.dart' as rpc;
import 'package:dart_grpc/server/timestamp.dart';

class MiddleServiceHandler {
  final rpc.MiddleClient client;

  MiddleServiceHandler(this.client);

  Future<void> ping() async {
    print("--- ping ---");
    try {
      final request = rpc.PingRequest();
      final response = await client.ping(request);
      print("timestamp: ${TimestampParser.from(response.timestamp)}");
    } catch (e) {
      print("caught an error: $e");
    }
  }

  Future<void> sayHello() async {
    print("--- sayHello ---");
    try {
      final request = rpc.HelloRequest()..name = "Yuto";
      final response = await client.sayHello(request);
      print(response.message);
    } catch (e) {
      print("caught an error: $e");
    }
  }
}

To call a gRPC function, we need to create an instance of the request message. It’s PingRequest for ping and HelloRequest for sayHello.

Since the constructors don’t have any parameters, necessary values are assigned after the instance creation.

Both function returns ResponseFuture which implements Future. The client side can’t know when the process is done because it’s handled on the server side. Therefore, we need to get the return value by await.

We can use TimestampParser.from() to convert from timestamp to DateTime.

Run the server and call the function from the client

We are ready to run gRPC server and client. Let’s run it. To make it work, we need two terminals.

The second terminal can be placed by clicking Split Terminal

Then, execute make runServer and make runClient which are defined in Makefile.

Same result in the written version.

$ make runClient                     | $ make runServer
--- ping ---                         | Server listening on port 8080...
timestamp: 2023-07-19 03:32:01.000Z  | 
--- sayHello ---                     | 
Hello Yuto                           |

Go to Next Step

Comments

Copied title and URL