In this tutorial, we’ll cover the basics of gRPC, a performant, open-source, universal RPC framework, review a little about the Dart programming language, and demonstrate how to build a gRPC server in Dart.
We’ll walk you through the following:
gRPC is an interprocess communication (RPC) framework built by Google and released in 2015. It is open-source, language-neutral, and has a compact binary size. gRPC also supports HTTP/2 and is cross-platform compatible.
gRPC is very different from the conventional RPC in the sense that it uses Protocol Buffers as its IDL to define its service interface. Protocol buffers is a serialization tool built by Google that allows you to define your data structures, then use the protocol buffer compiler to generate source code from these data structures to the language of your choice. The generated language is used to write and read the data structures to and from any context we want. According to the official docs, “Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler.”
The protocol buffer is used to write the service definition interface, which is used to define data structures and methods. The data structures are like data types in statically typed languages such as Java; they tell the compiler/interpreter how the data is intended to be used. The data structures in the service definition interface are the argument types that will be passed to the methods and the return types of the methods. This service definition interface is kept in a text file with .proto
extension. The methods in the service interface are the methods the gRPC server will expose to be called by gRPC clients.
gRPC has three components:
server
hosts the methods implementation and listens for requests from clientsprotocol buffer
holds the message format of the data structures and the methods, including their arguments and return typeclient
calls the methods hosted by the server. The client knows about the methods and their return and argument types from the service definition interface in the proto
fileUsing this service interface, the gRPC server sets up its server code implementing the methods in the service interface. It sets itself up and listens for requests (method calls) from clients.
The client uses the service definition interface to generate the client stub. This client sub is from where the methods in the server are called. A gRPC client app can make direct requests to a server application. Both client and server embrace a common interface, like a contract, in which it determines what methods, types, and returns each of the operations is going to have.
The most appealing thing about gRPC is its use of the protocol buffer, which enables the protocol to be platform-agnostics and polyglot. That means the server can be written in a given language and the client developed in another language. The protocol buffer makes this possible because it has compilers that can generate a language source code from the data structure in its definitions.
For example, let’s say the server is to be written in JavaScript. We’ll use the proto compiler to generate JavaScript source code from the definitions in the .proto
file. The server can then access and manipulate the data structures and methods using JavaScript code.
For the client, we want it to be developed in Java, so we’ll generate Java source code from the definitions. The client can then call the methods and access the data structures using Java code. That’s what we mean when we say that gRPC is polyglot and platform-agnostic.
Note that protocol buffers are not only used by gRPC. They can also be used for serialization. It is commonly used to send data through streams so you can read and write your data structure without any overhead loss.
Now that we understand the basics of gRPC and protocol buffers, it’s time to build our gRPC server in Dart.
Before we begin, make sure you have the Dart SDK installed in your machine. The Dart executable must be available globally in your system. Run the following command to check:
âžś grpc-dart dart --version Dart SDK version: 2.10.5 (stable) (Tue Jan 19 13:05:37 2021 +0100) on "macos_x64"
We’ll also need some protoc tools. Since we’re developing the gRPC server in Dart, we’ll have to install the proto compiler for the Dart lang. This compiler will generate Dart source code from the service definitions in the .proto
file.
The protocol buffer compiler is a command-line tool for compiling the IDL code in the .proto
files and generating specified language source code for it. For installation instructions, see the gRPC docs. Make sure to download the version 3.
Finally, the Dart plugin for the protoc compiler generates the Dart source code from the IDL code in the .proto
files.
For Mac users, install the Dart protoc plugin by running the following command:
dart pub global activate protoc_plugin
This installs the protoc_plugin
globally in your machine.
Next, update the $PATH
so the protoc
will see our plugin:
export PATH="$PATH:$HOME/.pub-cache/bin"
Now it’s time to create the server.
For our demonstration, we’ll create a gRPC server that manages a book service. This service will expose methods that will be used to:
GetAllBooks
)GetBook
)DeleteBook
)EditBook
)CreateBook
)Our Dart project will be a console-simple
project. Run the following command to scaffold the Dart project:
dart create --template=console-simple dart_grpc
The create
subcommand tells the Dart executable that we wish to create a Dart project. --template=console-simple
tells the Dart exe that we want the Dart project to be a simple console application.
The output will be as follows:
Creating /Users/.../dart_grpc using template console-simple... .gitignore CHANGELOG.md README.md analysis_options.yaml bin/dart_grpc.dart pubspec.yaml Running pub get... 10.2s Resolving dependencies... Downloading pedantic 1.9.2... Downloading meta 1.2.4... Changed 2 dependencies! Created project dart_grpc! In order to get started, type: cd dart_grpc âžś
Our project will reside in the dart_grpc
folder.
Open the pubspec.yaml
file. This is where we set the configs and dependencies on a Dart application. We want to install the grpc
and protobuf
dependencies. Add the below line in the pubspec.yaml
file and save:
dependencies: grpc: protobuf:
Now, run pub get
in your console so the dependencies are installed.
We define our service definitions in a .proto
file. So let’s create a book.proto
file.
touch book.proto
Add the below Protobuf
code in the book.proto
file:
syntax = "proto3"; service BookMethods { rpc CreateBook(Book) returns (Book); rpc GetAllBooks(Empty) returns (Books); rpc GetBook(BookId) returns (Book); rpc DeleteBook(BookId) returns (Empty) {}; rpc EditBook(Book) returns (Book) {}; } message Empty {} message BookId { int32 id = 1; } message Book { int32 id = 1; string title = 2; } message Books { repeated Book books = 1; }
That’s a lot of code. Let’s go through it line by line.
syntax = "proto3";
Here, we are telling the protocol buffer compiler that we’ll be using version 3 of the protocol buffer lang.
service BookMethods { rpc CreateBook(Book) returns (Book); rpc GetAllBooks(Empty) returns (Books); rpc GetBook(BookId) returns (Book); rpc DeleteBook(BookId) returns (Empty) {}; rpc EditBook(Book) returns (Book) {}; }
Here, we’re declaring the methods and the service they will be under. The service
keyword denotes a single service in a gRPC, so we create a service BookMethods
. To call a method, the method must be referenced by its service. This is analogous to class
and methods
; methods
are called through their class instance. We can have several services defined in a proto.
Methods are denoted inside each service by the rpc
keyword. The rpc
tells the compiler that the method is an rpc
endpoint and will be exposed and called from clients remotely. In our definition, we have five methods inside the BookMethods
service: CreateBook
, GetAllBooks
, GetBook
, DeleteBook
, and EditBook
.
CreateBook
takes a Book
data type as arg and returns a Book
type. This method implementation will create a new bookGetAllBooks
takes an Empty
type as arg and returns a Books
type. Its implementation will return all the booksGetBook
method accepts an input param of type, BookId
and returns a Book
. Its implementation will return a specific bookDeleteBook
takes a BookId
type as input param and returns an Empty
type. Its implementation will delete a book entry from the collectionEditBook
takes a Book
type as arg and returns a Book
type. Its implementation will modify a book in the collectionAll the other data from this point down represents the data or message types. We have:
message Empty {}
The message
keyword denotes message types. Each message type has fields and each field has a number to uniquely identify it in the message type.
Empty
denotes an empty data structure. This is used when we want to send no argument to rpc
methods or when the methods return no value. It is the same as void
in C/C++.
message BookId { int32 id = 1; }
This data structure represents a book ID message object. The id
field will hold an integer going by the int32
keyword before it. The id
field will hold the ID of a book.
message Book { int32 id = 1; string title = 2; }
This data structure represents a book. The id
field holds the book’s unique ID and the title
holds the book’s title. The title
field will be a string going by the string
keyword before it.
message Books { repeated Book books = 1; }
This represents an array of books. The books
field is an array that holds books. repeated
denotes a field that will be a list or an array. The Book
before the field name denotes that the array will be of Book
types.
Now that we’re done writing our service definition, let’s compile the book.proto
file.
The protoc tool is used to compile our .proto
files. Make sure the protoc tool is globally available in your system:
protoc --version libprotoc 3.15.8
That’s the version of my protoc tool at the time of this writing. your version might be different, it does not matter.
Now, make sure your terminal is opened at the dart_grpc
root folder. Run the below command to compile the book.proto
file:
protoc -I=. --dart_out=grpc:. book.proto
The I=.
tells the compiler the source folder which proto
field we are trying to compile.
The dart_out=grpc:.
subcommand tells the protoc compiler that we’re generating Dart source code from the book.proto
definitions and using it for gRPC =grpc:
. The .
tells the compiler to write the dart files in the root folder we are operating from.
This command will generate the following files:
book.pb.dart
book.pbenum.dart
book.pbgrpc.dart
book.pbjson.dart
The most important file is book.pb.dart
, which contains Dart source code for the message data structures in the book.proto
file. It also contains Dart classes for Empty
, BookId
, Book
, and Books
. From these, we create their instances and use them when calling the rpc
methods.
The book.grpc.dart
file contains the class BookMethodClient
, which we’ll use to create instances to call the rpc
methods and an interface BookMethodsServiceBase
. This interface will be implemented by the server to add the methods’ implementations.
Next, we’ll write our server code.
We’ll write our gRPC server code in the dart_grpc.dart
file. Open the file and paste the below code:
import 'package:grpc/grpc.dart'; import 'package:grpc/src/server/call.dart'; import './../book.pb.dart'; import './../book.pbgrpc.dart'; class BookMethodsService extends BookMethodsServiceBase { Books books = Books(); @override Future<Book> createBook(ServiceCall call, Book request) async { var book = Book(); book.title = request.title; book.id = request.id; books.books.add(book); return book; } @override Future<Books> getAllBooks(ServiceCall call, Empty request) async { return books; } @override Future<Book> getBook(ServiceCall call, BookId request) async { var book = books.books.firstWhere((book) => book.id == request.id); return book; } @override Future<Empty> deleteBook(ServiceCall call, BookId request) async { books.books.removeWhere((book) => book.id == request.id); return Empty(); } @override Future<Book> editBook(ServiceCall call, Book request) async { var book = books.books.firstWhere((book) => book.id == request.id); book.title = request.title; return book; } } Future<void> main(List<String> args) async { final server = Server( [BookMethodsService()], const <Interceptor>[], CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]), ); await server.serve(port: 50051); print('Server listening on port ${server.port}...'); }
What a chunk of code! It looks daunting, but it’s simpler than you might think.
The first part imports the required files. We imported the grpc
code and grpc
Dart code. We imported the book.pb.dart
and book.pbgrpc.dart
files because we need the classes therein.
Below, we extend the BookMethodsServiceBase
interface in BookMethodsService
to provide the implementations for all the methods in the BookMethods
service.
In the BookMethodsService
class, we override all the methods to provide their implementations. Notice the two parameters in the methods. The first param, ServiceCall call
, contains meta-information on the request. The second param holds the info that is sent, which is the type of data the rpc
method will accept as an argument.
Books books = Books();
The above command sets a books
array.
In the createBook
method, we created a new Book
, set the id
, title
, and added it to the books
array in the books
variable.
In the getAllBooks
method, we just returned the books
variable.
In the getBook
method, we fetched the ID from the BookId request
object and used it to get the book from the books
array using the List#firstWhere
method and return it.
In deleteBook
, we oet the bookID from the BookId request
and used it as cursor to remove the book from the books
array using the List#removeWhere
method.
In the editBook
method, the request
arg contains the Book
info. We retrieved the book from the books
array and edited its title
property value to the one sent in the request
arg.
Finally, we set up the server in the main
function. We passed the BookMethodsService
instance in an array to the Server
constructor. Then, we called the serve
method to start the server at port 50051
.
Now let’s build the client.
Create a client.dart
file inside the bin
folder:
touch bin/client.dart
Open it and paste the following code:
import 'package:grpc/grpc.dart'; import './../book.pb.dart'; import './../book.pbgrpc.dart'; class Client { ClientChannel channel; BookMethodsClient stub; Future<void> main(List<String> args) async { channel = ClientChannel('localhost', port: 50051, options: // No credentials in this example const ChannelOptions(credentials: ChannelCredentials.insecure())); stub = BookMethodsClient(channel, options: CallOptions(timeout: Duration(seconds: 30))); try { //... var bookToAdd1 = Book(); bookToAdd1.id = 1; bookToAdd1.title = "Things Fall Apart"; var addedBook1 = await stub.createBook(bookToAdd1); print("Added a book: " + addedBook1.toString()); var bookToAdd2 = Book(); bookToAdd2.id = 2; bookToAdd2.title = "No Longer at Ease"; var addedBook2 = await stub.createBook(bookToAdd2); print("Added a book: " + addedBook2.toString()); var allBooks = await stub.getAllBooks(Empty()); print(allBooks.books.toString()); var bookToDel = BookId(); bookToDel.id = 2; await stub.deleteBook(bookToDel); print("Deleted Book with ID: " + 2.toString()); var allBooks2 = await stub.getAllBooks(Empty()); print(allBooks2.books); var bookToEdit = Book(); bookToEdit.id = 1; bookToEdit.title = "Beware Soul Brother"; await stub.editBook(bookToEdit); var bookToGet = BookId(); bookToGet.id = 1; var bookGotten = await stub.getBook(bookToGet); print("Book Id 1 gotten: " + bookGotten.toString()); } catch (e) { print(e); } await channel.shutdown(); } } main() { var client = Client(); client.main([]); }
We imported the grpc.dart
package and the book.pb.dart
and book.pbgrpc.dart
files. We created a class Client
class. We have a BookMethodsClient stub
; the stub
will hold the BookMethodsClient
instance, which is where we can call the BookMethods
service methods to invoke them in the server.
In the main
method, we created a ClientChannel
instance and also a BookMethodsClient
instance pass in the ClientChannel
instance to its constructor. BookMethodsClient
uses the instance to get config — for example, the port the gRPC server will be reached on. In our case, it’s 50051
and the timeout time.
Inside the try
statement body, we called our gPRC methods. First, we created a book with the title “Things Fall Apart” and assigned it an ID of 1
. We called the createBook
method in the stub
, passing in the Book
instance bookToAdd1
to the method as arg. This will call the createBook
method in the server with the addToAdd1
object.
Next, we created a new book instance, “No Longer at Ease,” with the ID 2
and called the createBook
method, passing in the book instance. This remotely invoked the createBook
method in the gRPC server and a new book was created.
We called the getAllBooks
method to get all books on the server.
Next, we set up a BooKId
object, setting its id to 2
. Then, we called the deleteBook
method,
passing in the BookId
object. This deletes the book with id 2
(“No Longer at Ease”) from the server.
Notice where we edit a book. We created a BookId
instance with an ID set to 1
and a title set to “Beware Soul Brother.” We want to edit the title of the book with ID 1
to say “Beware Soul Brother” instead of “Things Fall Apart.” So we called the editBook
method, passing in the BookId
instance.
Last, we retrieved a specific book using its ID. We created a BookId
instance with its id
set to 1
. This means we want to get the book with the ID of 1
, which represents the book “Beware Soul Brother.” So, we called the getBook
method, passing the BookId
instance. The return should be a Book
object with the title “Beware Soul Brother.”
After all this, the channel is shut down by calling the shutdown
method in ClientChannel
from its channel
instance.
Now it’s time to test everything. First, run the server:
âžś dart_grpc dart bin/dart_grpc.dart Server listening on port 50051...
Open another terminal and run the client:
âžś dart_grpc dart bin/client.dart Added a book: id: 1 title: Things Fall Apart Added a book: id: 2 title: No Longer at Ease [id: 1 title: Things Fall Apart , id: 2 title: No Longer at Ease ] Deleted Book with ID: 2 [id: 1 title: Things Fall Apart ] Book Id 1 gotten: id: 1 title: Beware Soul Brother âžś dart_grpc
That’s it — our gRPC server is working as intended!
The complete source code for this example is available on GitHub.
We covered a lot in this tutorial. We started by introducing gRPC generally and explaining how it works from the protocol buffers down to the client.
Next, we demonstrated how to install tools and plugins for the protocol buffer compiler. These are used to generate Dart source code from the proto definitions. After that, we walked through the process of creating an actual gRPC service in Dart, building a gRPC client, and calling the methods from the client. Finally, we tested everything and found that it works great.
gRPC is very powerful and there is a lot more you can discover by playing around with it yourself. The examples in this tutorial should leave you with a solid foundation.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]