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.
- Dart How to setup devcontainer for gRPC
- (this post ) Dart The first gRPC server and client with timestamp
- Dart Server Streaming gRPC function example to download a file
- Dart Client Streaming gRPC function example to Upload a file
- Dart gRPC Bidirectional streaming example
You can clone my GitHub repository if you want to try it yourself.
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
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 |
Comments