gRPC a comprehensive introduction
Hi everyone, the intent of this article is to be a comprehensive introduction to gRPC, their motivations, and how it works while introducing examples in Haskell (more precisely using the Mu-Haskell framework).
Also notice that for this article we are only going to focus on the server due to familiar tools like insomnia already supporting gRPC clients the implementation of them will be less crucial but Mu-Haskell has a section about client implementations in case you want to take a look.
What's gRPC
gRPC is an RPC framework, and it's a multi-machine communication standard like Rest and GraphQL, the main idea of gRPC is performance due to it does not transfer data like text-based protocols like rest, instead it uses buffers and HTTP2 to get performance and low latency improvements, of course, it limits us on how we can use due to use a gRPC we need to share a “.proto” file with the API contract to both server and clients.
Giving a look at the proto file
It all starts with the definition of the API contract in a “.proto” file, for this article I'm going to use the Mu-Haskell “.proto” file example:
syntax = "proto3";
import "google/protobuf/empty.proto";
package grpc;
service Service {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }
Explaining its contents in the “syntax” field, we have the version of proto we are going to use in this case version 3, “import” does a basic import of a base proto file, “package” is the namespace of our project it works similar to a module name the “service” is a wrapper for our endpoints.
Now defining the endpoints we start with “rpc” and then the name of the endpoint in the parenthesis we list the parameters this endpoint is going to receive, and then the return result is informed after the “returns” and finishes with {}.
Defining parameters and responses is done by using the keyword “message” and after it the identifier of our parameter, in this case, HelloRequest and HelloReply and after it, we open braces and list what that data structure will contain like this:
type identifierName = orderIndex;
Also, notice that if we have more fields, the orderIndex field should increase by one like this:
message Task {
string storyId = 1;
string taskId = 2;
string description = 3;
bool finished = 4;
}
It may seem a bit confusing at first about how it is used, actually this contract is compiled by the application to ensure everyone follows the same conventions, but I believe it helps to understand by translating it to JSON and pretending the JSON will be the data used by a node.js server to define routes for example:
{
"syntax": "proto3",
"import": "google/protobuf/empty.proto",
"package": "grpc",
"service": {
"name": "Service",
"endpoints": {
"SayHello": {
"type": "rpc",
"params": "HelloRequest"
"returns": "HelloReply"
}
}
}
"HelloRequest": {
"name": {
"type": "string",
"index": 1
}
}
"HelloReply": {
"message": {
"type": "string",
"index": 1
}
}
}
Reflecting the proto file in the schema
After it, we have to reflect the proto file in our Schema.hs(in case you are using Haskell):
grpc "GrpcSchema" id "grpc.proto"
data HelloRequestMessage = HelloRequestMessage { name :: T.Text }
deriving ( Eq, Show, Generic
, ToSchema GrpcSchema "HelloRequest"
, FromSchema GrpcSchema "HelloRequest" )
data HelloReplyMessage = HelloReplyMessage { message :: T.Text }
deriving ( Eq, Show, Generic
, ToSchema GrpcSchema "HelloReply"
, FromSchema GrpcSchema "HelloReply" )
It's basically reflecting the data structures in a proto file to our Haskell code and an endpoint reflects a server like this:
sayHello (HelloRequestMessage name) = do
pure $ HelloReplyMessage ("Hi, " <> name)
Error Handling and Error conventions
Also notice that gRPC also has error handling and that it differs from the Rest HTTP error codes convention, for gRPC it works like this:
- OK 0
- CANCELLED 1
- UNKNOWN 2
- INVALID_ARGUMENT 3
- DEADLINE_EXCEEDED 4
- NOT_FOUND 5
- ALREADY_EXISTS 6
- PERMISSION_DENIED 7
- RESOURCE_EXHAUSTED 8
- FAILED_PRECONDITION 9
- ABORTED 10
- OUT_OF_RANGE 11
- UNIMPLEMENTED 12
- INTERNAL 13
- UNAVAILABLE 14
- DATA_LOSS 15
- UNAUTHENTICATED 16
And using it in Haskell seems very similar to:
sayHello (HelloRequestMessage name) = do
if name /= "Kevin" then
serverError $ ServerError Invalid "Invalid name"
else
pure $ HelloReplyMessage ("Hi, " <> name)
Other topics and resources
And here we come to the end of the first article of this year, if you want to learn more about the gRPC I would recommend books like grpc up and running and the documentation of the framework of your language of choice and of course put your knowledge into practice.
And also I have implemented a gRPC to-do list in Haskell as an example in case you are looking for references.
Hope it was useful to you, happy new year!