This tutorial provides a basic Erlang programmer's introduction to working with gRPC.
This is an Erlang version of the tutorial that is available for several other languages on grpc.io, see for example the C version.
The tutorial covers both the client and the server side of the Erlang gRPC implementation. The implementations are in different repositories:
- Client: grpc_client
- Server: grpc
By walking through this example you'll learn how to:
- Define a service in a .proto file.
- Generate server and client code using the protocol buffer compiler.
- Use the Erlang gRPC API to write a simple client and server for your service.
It assumes that you have read the Overview on gRPC.io and are familiar with protocol buffers. Note that the example in this tutorial uses the proto3 version of the protocol buffers language, which is currently in beta release: you can find out more in the proto3 language guide and see the release notes for the new version in the protocol buffers Github repository.
Our example is a simple route mapping application that lets clients get information about features on their route, create a summary of their route, and exchange route information such as traffic updates with the server and other clients.
With gRPC we can define our service once in a .proto file and implement clients and servers in any of gRPC's supported languages, which in turn can be run in environments ranging from servers inside Google to your own tablet - all the complexity of communication between different languages and environments is handled for you by gRPC. We also get all the advantages of working with protocol buffers, including efficient serialization, a simple IDL, and easy interface updating.
The example code for our tutorial is in /examples/route_guide.
To try the examples it is easiest to start an erlang shell using:
$ make shell
And then in this shell cd to the examples/routeguide directory:
1> cd("examples/route_guide").
Our first step is to
define the gRPC service and the method request and response types using
protocol buffers.
You can see the complete .proto file in
examples/route_guide/route_guide.proto
.
To define a service, you specify a named service
in your .proto file:
service RouteGuide {
...
}
Then you define rpc
methods inside your service definition, specifying their
request and response types. gRPC lets you define four kinds of service method,
all of which are used in the RouteGuide
service:
- A simple RPC where the client sends a request to the server using the stub and waits for a response to come back, just like a normal function call.
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
- A server-side streaming RPC where the client sends a request to the server
and gets a stream to read a sequence of messages back. The client reads from
the returned stream until there are no more messages. As you can see in our
example, you specify a server-side streaming method by placing the
stream
keyword before the response type.
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- A client-side streaming RPC where the client writes a sequence of messages
and sends them to the server, again using a provided stream. Once the client
has finished writing the messages, it waits for the server to read them all
and return its response. You specify a client-side streaming method by placing
the
stream
keyword before the request type.
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- A bidirectional streaming RPC where both sides send a sequence of messages
using a read-write stream. The two streams operate independently, so clients
and servers can read and write in whatever order they like: for example, the
server could wait to receive all the client messages before writing its
responses, or it could alternately read a message then write a message, or
some other combination of reads and writes. The order of messages in each
stream is preserved. You specify this type of method by placing the
stream
keyword before both the request and the response.
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
Our .proto file also contains protocol buffer message type definitions for all
the request and response types used in our service methods - for example, here's
the Point
message type:
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Next we need to generate the gRPC client and server interfaces from our .proto service definition. A large part of the work is done by the exellent Erlang protobuf implementation gpb. Some additional code is generated to deal with the gRPC specifics.
Generating all required files can easily be done from the Erlang shell:
1> grpc:compile("route_guide.proto").
Running this command generates the following files in your current directory:
route_guide.erl
, the code to encode and decode the protobuf messagesroute_guide_server.erl
, which contains skeleton functions for a server
These contain:
- All the protocol buffer code to populate, serialize, and retrieve our request and response message types
- a set of ("empty") functions (or skeleton functions) in which you have
to provide your implementation of the server side of the methods
defined in the
RouteGuide
service.
First let's look at how we create a RouteGuide
server. If you're only
interested in creating gRPC clients, you can skip this section and go straight
to Creating the client (though you might find it interesting
anyway!).
There are two parts to making our RouteGuide
service do its job:
- Providing the required code to make the generated skeleton functions do the actual "work" of our service.
- Running a gRPC server to listen for requests from clients and return the service responses.
You can find our example RouteGuide
server in
examples/route_guide/server/route_guide_server_1.erl.
Let's take a closer look at how it works.
As you can see, our example is a copy of the generated
route_guide_server.erl
module with bits added to do the actual work.
In this case we're implementing a synchronous version of RouteGuide
.
This means that the flow of control is determined by the client. The server
waits for a request by the client, does its processing, possibly sends back
one or more reply messages, and then waits for the next request. In general
this fits well for simple RPC requests, but perhaps less so for
server-side streaming RPCs or bidirectional streaming RPCs. Using
standard Erlang concepts it is not difficult to implement an a-synchonous
version of the service, but we won't consider that in this tutorial.
route_guide_server_1.erl
contains a set of type specifications for the
messages (they will be translated to and from Erlang maps), and functions
for all our service methods. Let's look at the simplest
type first, GetFeature
, which just gets a Point
from the client and returns
the corresponding feature information from its database in a Feature
.
The "empty" function that is in the generated module looks like this:
-spec 'GetFeature'(Message::'Point'(), Stream::grpc:stream(), State::any()) ->
{'Feature'(), grpc:stream()} | grpc:error_response().
%% This is a unary RPC
'GetFeature'(_Message, Stream, _State) ->
{#{}, Stream}.
In our example implementation, the completed function looks like this:
-spec 'GetFeature'(Message::'Point'(), Stream::grpc:stream(), State::any()) ->
{'Feature'(), grpc:stream()} | grpc:error_response().
%% This is a unary RPC
'GetFeature'(Message, Stream, _State) ->
Feature = #{name => find_point(Message, data()),
location => Message},
{Feature, Stream}.
The function is passed the client's Point
protocol
buffer request (decoded to a map), a data structure that contains context
information about the request (or "Stream"), and some State information.
In this simple example we don't use the Stream and State information, so we'll ignore it for now. The Stream must be passed back in the response, because it is possible to modify it (for example by adding headers).
As the function spec indicates, the function must return a Feature
, which
is again a map. The fields that are expected in the map can be found in the
type specification for Feature()
, which can be found in the generated module.
Now let's look at something a bit more complicated - a streaming RPC.
ListFeatures
is a server-side streaming RPC, so we need to send back multiple
Features
to our client.
-spec 'ListFeatures'(Message::'Rectangle'(), Stream::grpc:stream(), State::any()) ->
{['Feature'()], grpc:stream()} | grpc:error_response().
%% This is a server-to-client streaming RPC
'ListFeatures'(_Message, Stream, _State) ->
Stream2 = grpc:send(Stream,
#{name => "Louvre",
location => #{latitude => 4,
longitude => 5}}),
{[#{name => "Tour Eiffel",
location => #{latitude => 3,
longitude => 5}}], Stream2}.
As you can see, this is not a "proper" implementation. It does not consider
the input at all, but it always returns the same result, which consists of
two Features
. Note that it uses two different ways to do this: The first
Feature
is sent to the client explictly, using grpc:send/2
, and the
second point is passed in the return value of the function. Which way you
want to use is up to you, it would also have been possible to send all
responses using grpc:send/2
or in the return value (but that would not
be very logical for a streaming RPC, since the response messages will then
be sent all together when all the work is done).
Note that the new Stream
value that is returned by grpc:send/2
must
be used from that moment on, and the final Stream
value must be passed back
in the return value of the function.
If you look at the client-side streaming method RecordRoute
you may
recognise that this follows a kind of 'folding' pattern, as known from
lists:foldl/3
or maps:fold/3
. The RecordRoute
function will be called
for each Point
that is received. With it you receive the result of the
previous invocation (the accumulator). You return the new value for the
accumulator.
Regarding the input parameters there are three cases to consider:
-
the first invocation receives a starting state that is specified when the server is started. In this case that value is
undefined
. For more information on how to specify this value, see starting the server. -
All following invocations receive the result of the previous invocation as the value for the last parameter.
-
to signal that the last message was received from the client, the special value
eof
is passed (in stead of a message).
Regarding the return value there are two cases to consider:
-
If the server considers that the answer must be sent to the client, it returns a tuple
{ResponseValue, Stream}
. Typically this should happen after receiving theeof
input value, see above. -
If the server expects more values, it returns
{continue, AccumulatorValue, Stream}
.
-spec 'RecordRoute'(Message::'Point'() | eof, Stream::grpc:stream(), State::any()) ->
{continue, grpc:stream(), any()} |
{'RouteSummary'(), grpc:stream()} |
grpc:error_response().
%% This is a client-to-server streaming RPC. After the client has sent the last message,
%% this function will be called a final time with 'eof' as the first argument. This last
%% invocation must return the response message.
'RecordRoute'(FirstPoint, Stream, undefined) ->
%% The fact that State == undefined tells us that this is the
%% first point. Set the starting state, and return 'continue' to
%% indicate that we are not done yet.
{continue, Stream, #{t_start => erlang:system_time(1),
acc => [FirstPoint]}};
'RecordRoute'(eof, Stream, #{start_time := T0, acc := Points}) ->
%% receiving 'eof' tells us that we need to return a result.
{#{elapsed_time => erlang:system_time(1) - T0,
point_count => length(Points),
feature_count => count_features(Points),
distance => distance(Points)}, Stream};
'RecordRoute'(Point, Stream, #{acc := Acc} = State) ->
{continue, Stream, State#{acc => [Point | Acc]}}.
Finally, let's look at our bidirectional streaming RPC RouteChat()
.
-spec 'RouteChat'(Message::'RouteNote'() | eof, Stream::grpc:stream(), State::any()) ->
{['RouteNote'()], grpc:stream(), any()} |
{['RouteNote'()], grpc:stream()} |
grpc:error_response().
%% This is a bidirectional streaming RPC. If the client terminates the stream
%% this function will be called a final time with 'eof' as the first argument.
'RouteChat'(In, Stream, undefined) ->
'RouteChat'(In, Stream, []);
'RouteChat'(eof, Stream, _) ->
{[], Stream};
'RouteChat'(#{location := Location} = P, Stream, Data) ->
Messages = proplists:get_all_values(Location, Data),
{Messages, Stream, [{Location, P} | Data]}.
The conventions for the invocation are exactly the same as for the client-side streaming case, see above.
For the return value the situation is different, because we can now send zero or more return values after every invocation.
This time we include a list of zero or more response messages in the return
value, rather than continue
. If we want the stream to continue we return
a 3-tuple {[Value], Stream, NewState}
(NewState
will be included as the
new value for the third parameter at the next invocation). If we want it to
end, we send a 2-tuple {[Value], Stream}
.
Once we've implemented all our methods, we also need to start up a gRPC server
so that clients can actually use our service. The following snippet shows how we
do this for our RouteGuide
service. Note that it will only work if both
the route_guide_server_1
module and the generated encoder/decoder module
route_guide
are on the code path.
5> {ok, Server} = grpc:start_server(grpc, tcp, 10000, #{'RouteGuide' => #{handler => route_guide_server_1}}).
The first argument is the name of the server (strictly speaking: the Cowboy
listener). It can be any term. The second argument specifies the transport
layer. It can be tcp
or ssl
. The third argument specifies the port and
the fourth argument links the gRPC service to our handler module. As an optional fifth argument
some options can be provided, for example to enable compression or
authentication, details about that are provided in separate sections below.
In this section, we'll look at creating an Erlang client for our RouteGuide
service. Note that the Erlang client is in a different repository:
grpc_client.
As for the server, a client module can be generated by compile the .proto file:
1> grpc_client:compile("route_guide.proto").
The module route_guide_client.erl
that was created when we compiled
route_guide.proto
contains the types for the messages that are exchanged
between client and server. It also contains a number of stubs: functions
that can be called locally by the server, which will result in a remote
call to the server. Especially in the case of simple RPCs it may be
convenient to use these stubs. As we will see below it may be more
convenient (or even necessary) for streaming RPCs to use more general
functions to send and receive messages.
First we need to create a gRPC connection, specifying the server address and port we want to connect to and optionally some additional data for the connection, such as TLS related options. In our case we'll use the simple form without TLS or other options.
1> {ok, Connection} = grpc_client:connect(tcp, "localhost", 10000).
Now we can use the connection to create a "stream". The stream is specific
for the RPC that we are calling. Each invocation needs its own stream,
which may be used to exchange many messages between the client and the
server (if it is a streaming service). Like the server, also the client
needs to have the encoder/decoder module route_guide
on the code path.
2> {ok, Stream} = grpc_client:new_stream(Connection, 'RouteGuide', 'ListFeatures', route_guide).
Now let's look at how we call our service methods.
Simple RPCs follow a fixed pattern of messages that are exchanged
between the client and the server: essentially the client sends a request and the
server sends back a response (on the wire you would also see some
headers being exchanged). Since this is such simple pattern, the whole
exchange of messages can be handled by one function call:
grpc_client:unary/6
. We don't even
have to bother about establishing (and stopping) a stream, this will all be handled by
unary
.
3> Point = #{latitude => 409146138, longitude => -746188906}.
4> grpc_client:unary(Connection, Point, 'RouteGuide', 'GetFeature', route_guide, []).
{ok,#{grpc_status => 0,
headers => #{<<":status">> => <<"200">>},
http_status => 200,
result =>
#{location =>
#{latitude => 409146138,longitude => -746188906},
name =>
"Berkshire Valley Management Area Trail, Jefferson, NJ, USA"},
status_message => <<>>,
trailers => #{<<"grpc-status">> => <<"0">>}}}
As you can see, we call the function with an argument of type Point()
,
that is: a map, and the main result is Feature()
: another map. The
translation of these maps to and from protobufs is handled for us.
Note that in this example the function that we are calling is blocking/synchronous: this means that the RPC call waits for the server to respond (you can specify an optional timeout). Below we will see a more general approach to exchange messages over a stream. This gives a lot of control, and it can also be used to implement simple RPCs, for example if you want to have an a-synchronous implementation or if you want have control over the processing of headers.
To simplify things even more for simple RPCs the compilation step
generates stubs for these functions. The generated file
"route_guide_client.erl" contains a function 'GetFeature'/3
that can be
called as follows:
5> route_guide_client:'GetFeature'(Connection, Point, []).
Now let's look at our streaming methods. The interaction between client and server is more complex in these cases: we cannot simply send one request and receive an answer, but we need to establish a "Stream" to exchange messages in both ways.
The process that we will use is not specific for any of the different types of streaming RPCs (client-streaming, server-streaming, bidirectional streaming). In fact, you can also use this method for simple RPCs, if you want to call a simple RPC in an a-synchronous way.
In all cases we start by initiating a connection, and on that connection a stream. Let's start one for the 'RouteChat' RPC, which is a bidirectional streaming RPC.
{ok, Connection} = grpc_client:connect(tcp, "localhost", 10000),
{ok, Stream} = grpc_client:new_stream(Connection, 'RouteGuide', 'RouteChat', route_guide).
This stream can now be used to send and receive messages. It always has to start with a message from the client (in fact, the stream wil only be created 'on the wire' when that happens). Let's send 2 Points.
P1 = #{latitude => 1, longitude => 2},
P2 = #{latitude => 3, longitude => 5},
grpc_client:send(Stream, #{location => P1, message => "something about P1"}),
grpc_client:send(Stream, #{location => P2, message => "something about P2"}).
Now we can see whether we have already received and responses:
grpc_client:get(Stream).
This returns empty
- we did not yet receive anything. This is not
surprising, because the server will only send a message if it receives
information about a point that it knows something about.
We can also check using the blocking rcv
function. Let's specify a
timeout to avoid getting stuck.
grpc_client:rcv(Stream, 1000).
This returns {error,timeout}
after one second - still no messages.
When we send some additional information for P1, we should get a response message with the information that we sent earlier.
grpc_client:send(Stream, #{location => P1, message => "more about P1"}).
Now try to get a message:
grpc_client:get(Stream).
Now we get:
{headers,#{<<":status">>,<<"200">>}}`.
Calling it again gives:
{data,#{location => #{latitude => 1,longitude => 2},
message => "something about P1"}}
So we do not only get the messages this way, but also the headers.
Bi-directional streaming RPCs such as RouteChat
have no specific end,
they can go on forever and there is no particular way to stop them, apart
from stopping the stream (using grpc_client:stop_stream(Stream).
) This is
different for the other RPC types: there the client has to signal that it
has sent its last message, and the server may send a trailer which may
contain additional metadata.
To show how this works let's look at an example for our server-streaming
RPC ListFeatures
. We'll reuse P1 and P2 as the corner points for the area
for which we would like to know the features. This is shown below as a sequence
of commands and return values in an Erlang shell.
2> {ok, S} = grpc_client:new_stream(Connection, 'RouteGuide', 'ListFeatures', route_guide).
{ok,<0.155.0>}
3> grpc_client:get(S).
empty
4> grpc_client:send_last(S, #{hi => P1, lo => P2}).
ok
5> grpc_client:get(S).
{headers,[{<<":status">>,<<"200">>}]}
6> grpc_client:rcv(S).
{data,#{location => #{latitude => 4,longitude => 5},name => "Louvre"}}
7> grpc_client:rcv(S).
{data,#{location => #{latitude => 3,longitude => 5},name => "Tour Eiffel"}}
8> grpc_client:rcv(S).
{headers,[{<<"grpc-status">>,<<"0">>}]}
9> grpc_client:get(S).
eof
10> grpc_client:get(S).
eof
11> grpc_client:rcv(S).
eof
You will notice a few things:
send_last/2
is used to indicate that this message is the last one. For theListFeatures
we now expect to receive a number of response messages.- As long as no anwer has been received,
get/1
returnsempty
. (Usingrcv/1
would block). - Once messages have arrived,
get/1
andrcv/1
can be used to retrieve information that was sent by the server. get/1
andrcv/1
do not only return messages, but also headers. In this example they do not contain anything special, but they may contain metadata that may be relevant.- The server sends headers before the first message and after the last
message. These headers are returned by
get/1
andrcv/1
in the same way, that is, separately and in the right order. - After the last set of headers has been received the stream is considered
closed, and both
get/1
andrcv/1
returneof
.
gRPC has SSL/TLS integration and promotes the use of SSL/TLS to authenticate the server, and to encrypt all the data exchanged between the client and the server. Optional mechanisms are available for clients to provide certificates for mutual authentication.
This is the simplest authentication scenario, where a client just wants to authenticate the server and encrypt all data.
The server has to be started so that it expects to use TLS. In this example
we want it to authenticate itself as "localhost", so it needs certificates for that
domain. Note that you must ensure that the required modules
(route_guide_server
and also route_guide
, the decoding module) are on
the erlang code path, and the location of the certificate files must also
be correct. The example certificates can be found in
test/certificates.
grpc:start_server(grpc, ssl, 10000,
#{'RouteGuide' => #{handler => route_guide_server_1}},
[{transport_options, [{certfile, "certificates/localhost.crt"},
{keyfile, "certificates/localhost.key"},
{cacertfile, "certificates/My_Root_CA.crt"}]}]).
On the client side we must now also use TLS:
1> application:ensure_all_started(ssl).
2> {ok, Connection} = grpc_client:connect(ssl, "localhost", 10000, []).
3> {ok, S} = grpc_client:new_stream(Connection, 'RouteGuide', 'ListFeatures', route_guide).
3> grpc_client:send_last(S, #{hi => P1, lo => P2}).
ok
4> grpc_client:get(S).
{headers,[{<<":status">>,<<"200">>}]}
5> grpc_client:rcv(S).
{data,#{location => #{latitude => 4,longitude => 5},name => "Louvre"}}
...
To ensure that the client checks the identity of the serve, provide a few extra options to enforce the exchange of certificates and to tell the client to perform the check:
6> SslOptions = [{cacertfile, "certificates/My_Root_CA.crt"},{fail_if_no_peer_cert, true}].
7> Options = [{verify_server_identity, true}, {transport_options, SslOptions}].
8> {ok, C} = grpc_client:connect(ssl, "localhost", 10000, Options).
This will check the subject of the certificate against the host name ("localhost", in this case). If you know that the certifcate will have another subject, the option 'server_host_override' can be used to provide that name.
To tell the server that it must authenticate the client, provide an extra option 'client_cert_dir' which will indicate where the required certificates are, and we must pass a few extra transport_options to ensure that the authentication is enforced. So the server will be started like this:
grpc:start_server(grpc, ssl, 10000,
#{'RouteGuide' => #{handler => route_guide_server_1}},
[{client_cert_dir, "certificates"},
{transport_options, [{certfile, "certificates/localhost.crt"},
{keyfile, "certificates/localhost.key"},
{cacertfile, "certificates/My_Root_CA.crt"},
{fail_if_no_peer_cert, true},
{verify, verify_peer}]}]).
The client must now pass its certificate:
1> SslOptions = [{certfile, "certificates/127.0.0.1.crt"},
{keyfile, "certificates/127.0.0.1.key"},
{cacertfile, "certificates/My_Root_CA.crt"}].
2> {ok, Connection} = grpc_client:connect(ssl, "localhost", 10000,
[{transport_options, SslOptions}]).
3> {ok, S} = grpc_client:new_stream(Connection, 'RouteGuide', 'ListFeatures', route_guide).
4> grpc_client:send_last(S, #{hi => P1, lo => P2}).
...
Metadata is information about a particular RPC call (such as authentication details) in the form of a list of key-value pairs, where the keys are strings and the values are typically strings (but can be binary data). Metadata is opaque to gRPC itself - it lets the client provide information associated with the call to the server and vice versa.
To add metadata to a request (from the client to the server), specify the key-value pairs when creating the stream:
Metadata = #{<<"password">> => <<"secret">>},
{ok, Stream} = grpc_client:new_stream(Connection, 'RouteGuide', 'RouteChat', route_guide, [{metadata, Metadata}]).
On the server side, the metadata that was sent by the client can be
retrieved from the Stream
parameter that is passed to every service
implementation. In order to limit access to the 'GetFeature' service
to clients that provide the password "secret" in the metadata, we could do
this:
-spec 'GetFeature'(Message::'Point'(), Stream::grpc:stream(), State::any()) ->
{'Feature'(), grpc:stream()} | grpc::error_response().
%% This shows how to access metadata that was sent by the client,
%% and how to send an error response.
'GetFeature'(Message, Stream, _State) ->
case grpc:metadata(Stream) of
#{<<"password">> := <<"secret">>} ->
Feature = #{name => find_point(Message, data()),
location => Message},
{Feature, Stream};
_ ->
{error, 7, <<"permission denied">>, Stream}
end.
The server can add metadata to the initial header that is sent to the
client, and/or to the final trailer using grpc:set_headers/2
or
grpc:set_trailers/2
. A simplistic example that uses both
variants:
-spec 'ListFeatures'(Message::'Rectangle'(), Stream::grpc:stream(), State::any()) ->
{['Feature'()], grpc:stream()} | grpc::error_response().
%% This adds metadata to the header and to the trailer that are sent to the client.
'ListFeatures'(_Message, Stream, _State) ->
Stream2 = grpc:send(grpc:set_headers(Stream,
#{<<"info">> => <<"this is a test-implementation">>}),
#{name => "Louvre",
location => #{latitude => 4,
longitude => 5}}),
Stream3 = grpc:set_trailers(Stream2, #{<<"nr_of_points_sent">> => <<"2">>}),
{[#{name => "Tour Eiffel",
location => #{latitude => 3,
longitude => 5}}], Stream3}.
The values will be accessible for the client in the header and trailer messages that it will receive (also the "standard" HTTP/2 and grpc headers such as ":status" and "grpc-status" are available in this way).
1> {ok, C} = grpc_client:connect(tcp, "localhost", 10000).
...
2> {ok, Stream} = grpc_client:new_stream(C, 'RouteGuide', 'ListFeatures', route_guide),
...
3> P1 = #{latitude => 1, longitude => 2}, P2 = #{latitude => 3, longitude => 5}.
...
4> grpc_client:send_last(Stream, #{hi => P1, lo => P2}).
...
5> grpc_client:rcv(Stream).
{headers,#{<<":status">> => <<"200">>,
<<"info">> => <<"this is a test-implementation">>}}
6> grpc_client:rcv(Stream).
{data,#{location => #{latitude => 4,longitude => 5},name => "Louvre"}}
7> grpc_client:rcv(Stream).
{data,#{location => #{latitude => 3,longitude => 5},name => "Tour Eiffel"}}
8> grpc_client:rcv(Stream).
{headers,#{<<"grpc-status">> => <<"0">>,
<<"nr_of_points_sent">> => <<"2">>}}
8> grpc_client:rcv(Stream).
eof
gRPC supports compression of messages. The standard specifies several poossible methods: gzip, deflate or snappy, and it also allows the use of custom methods. This implementation currently only supports gzip.
On the client side, a message that is sent to the server can be compressed
by specifying the option {compression, gzip}
when creating the stream (or
as an option to a simple rpc in a generated client module).
2> {ok, Stream} = grpc_client:new_stream(C, 'RouteGuide', 'ListFeatures',
route_guide,
[{compression, gzip}]).
Or:
3> route_guide_client:'GetFeature'(Connection, Point, [{compression, gzip}]).
On the server side, a message that is sent to the client can be compressed
by applying grpc:set_compression/2
on the stream, similar to the way
metadata can be added using grpc:set_headers/2
or grpc:set_trailers/2
.
You can implement middleware as described in https://ninenines.eu/docs/en/cowboy/2.0/guide/middlewares/. The list of middlewares will need to be specified in order of execution in the middlewares
option when starting the server. The list must include cowboy_router
and cowboy_handler
.
TODO:
- Timeout.
- Description and improvement of the actual implementation of error handling.
- Client side middleware support