Basics tutorial

View Source

This tutorial builds RouteGuide, the canonical gRPC example, in Erlang. It uses all four call types, so by the end you have seen every shape. The complete code is in examples/route_guide.erl and proto/route_guide.proto.

Define the service

syntax = "proto3";
package routeguide;

service RouteGuide {
  rpc GetFeature(Point) returns (Feature);
  rpc ListFeatures(Rectangle) returns (stream Feature);
  rpc RecordRoute(stream Point) returns (RouteSummary);
  rpc RouteChat(stream RouteNote) returns (stream RouteNote);
}

message Point     { int32 latitude = 1; int32 longitude = 2; }
message Rectangle { Point lo = 1; Point hi = 2; }
message Feature   { string name = 1; Point location = 2; }
message RouteNote { Point location = 1; string message = 2; }
message RouteSummary { int32 point_count = 1; int32 feature_count = 2; int32 distance = 3; }

rebar3 compile generates route_guide_pb. Nested messages are nested maps: a Feature is #{name => <<...>>, location => #{latitude => _, longitude => _}}.

Implement the four methods

The handler is a module you write, separate from the generated route_guide_pb (which holds the messages). This example names it route_guide. It exports one snake_case function per RPC.

Unary: GetFeature

One request in, one reply out.

get_feature(Point, _Ctx) ->
    {ok, #{name => feature_name(Point), location => Point}}.

Server-streaming: ListFeatures

One request, many replies. A Send function pushes each reply; return ok when done.

list_features(#{lo := Lo, hi := Hi}, Send, _Ctx) ->
    lists:foreach(
        fun(#{location := Loc} = Feature) ->
            case in_rectangle(Loc, Lo, Hi) of
                true  -> Send(Feature);
                false -> ok
            end
        end,
        features()),
    ok.

Client-streaming: RecordRoute

Many requests, one reply. Read the stream with recv_all/1, then return a summary.

record_route(Stream, _Ctx) ->
    {ok, Points, _} = livery_grpc_stream:recv_all(Stream),
    Named = [P || P <- Points, feature_name(P) =/= <<>>],
    {ok, #{point_count   => length(Points),
           feature_count => length(Named),
           distance      => distance(Points)}}.

feature_name/1, in_rectangle/3, distance/1, and features/0 are small helpers in examples/route_guide.erl.

Bidirectional: RouteChat

Both sides stream. Read with recv/1, reply with send/2, interleaved. Here, each incoming note is echoed with any earlier note left at the same point.

route_chat(Stream, _Ctx) ->
    chat_loop(Stream, #{}).

chat_loop(Stream, Seen) ->
    case livery_grpc_stream:recv(Stream) of
        {ok, #{location := Loc, message := Msg}, Stream1} ->
            Prior = maps:get(Loc, Seen, []),
            [livery_grpc_stream:send(Stream1, #{location => Loc, message => M}) || M <- Prior],
            chat_loop(Stream1, Seen#{Loc => Prior ++ [Msg]});
        {eof, _} ->
            ok
    end.

Run the server

{ok, Server} = livery_grpc:start_server(#{
    port     => 50051,
    services => [#{proto => route_guide_pb, service => 'RouteGuide', handler => route_guide}]
}).

Call it

Unary and server-streaming go through call/3:

{ok, Conn} = livery_grpc_client:connect("localhost", 50051),
{ok, GF}   = livery_grpc_client:method(route_guide_pb, 'RouteGuide', 'GetFeature'),
{ok, Feature} = livery_grpc_client:call(Conn, GF, #{latitude => 1, longitude => 1}),

{ok, LF}   = livery_grpc_client:method(route_guide_pb, 'RouteGuide', 'ListFeatures'),
{ok, Features} = livery_grpc_client:call(Conn, LF, #{
    lo => #{latitude => 0, longitude => 0},
    hi => #{latitude => 10, longitude => 10}
}).

Client-streaming sends a list and gets one reply:

{ok, RR} = livery_grpc_client:method(route_guide_pb, 'RouteGuide', 'RecordRoute'),
{ok, Summary} = livery_grpc_client:client_stream(Conn, RR, [Point1, Point2, Point3]).

Bidirectional opens a stream you drive with send/2 and recv/1:

{ok, RC}   = livery_grpc_client:method(route_guide_pb, 'RouteGuide', 'RouteChat'),
{ok, Call} = livery_grpc_client:open(Conn, RC),
ok = livery_grpc_client:send(Call, #{location => Loc, message => <<"hi">>}),
{ok, Note, Call1} = livery_grpc_client:recv(Call).

Try it end to end

route_guide:run/0 starts a server, calls all four methods, and prints the results:

$ rebar3 as examples shell
1> route_guide:run().

With reflection on, grpcurl works without a .proto:

$ grpcurl -plaintext localhost:50051 list
$ grpcurl -plaintext -d '{"latitude":1,"longitude":1}' \
    localhost:50051 routeguide.RouteGuide/GetFeature