Core concepts
View SourceThis page covers the concepts you need to work with livery_grpc: the service definition, the four call types, and how they map to Erlang.
Service definition
You start with a .proto file. It declares a service, its methods, and
the messages they exchange.
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }At build time, rebar3_gpb_plugin compiles this to an Erlang module using
gpb in maps mode. Messages are plain maps: #{name => <<"ada">>}.
livery_grpc reads the service's methods, message types, and call kinds
from that module at runtime, so there is no separate generated dispatch
layer to keep in sync.
How the module and names are set
The generated module name is the proto file name plus the
module_name_suffix from gpb_opts (_pb by convention): helloworld.proto
compiles to helloworld_pb, and route_guide.proto to route_guide_pb.
The file name sets the module; the package line does not.
For the helloworld.proto above:
| In the proto | In Erlang |
|---|---|
file helloworld.proto | module helloworld_pb |
service Greeter | service atom 'Greeter' |
rpc SayHello(...) | method atom 'SayHello' |
message HelloRequest | a map #{name => _} |
So you name a method with the generated module and the proto's service and method atoms:
{ok, Method} = livery_grpc_client:method(helloworld_pb, 'Greeter', 'SayHello').The wire path is built from the package and these names:
/helloworld.Greeter/SayHello. For the rebar3 and gpb_opts setup that
produces the module, see the
Erlang integration guide.
Two modules: generated messages and your handler
There are two modules per service, and they are different things:
- The generated
*_pbmodule (helloworld_pb, fromhelloworld.proto) holds the messages and their encode/decode. It is generated; you never edit it. - Your handler module holds the RPC functions (
say_hello/2, ...). You write it, and you choose its name; it does not have to match the proto. The RouteGuide example names its handlerroute_guide, alongside the generatedroute_guide_pb.
You connect the two when you start the server: proto is the generated
module, handler is yours.
livery_grpc:start_server(#{
services => [#{proto => route_guide_pb, %% generated messages
service => 'RouteGuide',
handler => route_guide}] %% your functions
}).The four call types
gRPC has four call types. livery_grpc supports all of them.
- Unary: one request, one reply. The most common shape.
- Server-streaming: one request, a stream of replies.
- Client-streaming: a stream of requests, one reply.
- Bidirectional: both sides stream, independently.
On the server, each method is one Erlang function whose name is the RPC name in snake_case. The shape of the function follows the call type:
%% unary: request and context in, reply out
say_hello(#{name := Name}, _Ctx) ->
{ok, #{message => <<"hello ", Name/binary>>}}.
%% server-streaming: a Send function pushes replies
say_hello_stream(#{name := Name}, Send, _Ctx) ->
Send(#{message => <<"hi ", Name/binary>>}),
ok.
%% client-streaming and bidirectional: a stream handle to recv (and send)
say_hello_collect(Stream, _Ctx) ->
{ok, Requests, _} = livery_grpc_stream:recv_all(Stream),
{ok, #{message => summarize(Requests)}}.See the streaming guide for each shape in full.
The context
Every callback receives a context map, Ctx. It carries the call
metadata, the method descriptor, the deadline, and the underlying request:
#{metadata := [{binary(), binary()}],
method := map(),
deadline := timeout(),
req := livery_req:req()}Metadata
Metadata is key-value pairs sent alongside a call, as HTTP/2 headers. A
client attaches it per call; a handler reads it from Ctx. Use it for
auth tokens, request ids, and tracing. See the
metadata guide.
Deadlines
A client sets a deadline; it travels on the wire as grpc-timeout. The
server exposes it in Ctx and aborts a unary handler that overruns. See
the deadlines guide.
Status
Every call ends with a status. Success is ok; an error is a status code
(such as not_found or invalid_argument) with an optional message and
details. A handler returns {error, Status} or {error, {Status, Msg}};
a client sees {error, {Status, Msg}}. See the
error handling and
status codes guides.
Multiple services
A server takes a list of services, so you run many on one listener. Each entry binds a generated module, a service name, and a handler:
livery_grpc:start_server(#{
port => 50051,
services => [
#{proto => greeter_pb, service => 'Greeter', handler => my_greeter},
#{proto => route_guide_pb, service => 'RouteGuide', handler => route_guide},
livery_grpc_health:service()
],
reflection => true
}).The dispatcher routes each call by its path (/package.Service/Method),
so the services coexist with no conflict. The built-in health and
reflection services are just entries in the same list. A single .proto
can also declare several service blocks; register one entry per service
(same module, different service atom).
Start a second server only when you need a separate port, TLS config, or isolation. The usual pattern is one server with many services.
Connections
A client opens a connection with livery_grpc_client:connect/2,3 and
makes many calls on it. A server is a dedicated listener started with
livery_grpc:start_server/1. Both speak gRPC over HTTP/2.