This section describes a simple and powerful way of programming client-server applications. Client-server applications are programmed using the gen_server
behaviour.
Refer to the Reference Manual , the module gen_server
in stdlib
, for full details of the behaviour interface.
This section describes several solutions to one sample problem in order to illustrate how to write client-server applications.
The sample problem is a very simple server which acts as a Home Location Register (HLR). We will implement a small sub-set of an HLR which we call VSHLR (Very Simple HLR) in a number of different ways. The Erlang modules which implement our VSHLR will always be called something like vshlr_XX
. All these modules will export the following functions:
vshlr_XX:start() -> true
starts the server.
vshlr_XX:stop() -> true
stops the server.
vshlr_XX:i_am_at(Person, Position) -> ok
tells the server that Person
is at the location Position
.
vshrl_XX:find(Person) -> {at, Position} | lost
asks the server where Person
is. The server responds {at, Position}
, where Position
is the last reported location, or lost
if it does not know where the person is.
The client-server model can be illustrated in the following figure:
The client-server model is characterized by a central server and an arbitrary number of clients. The client-server model is generally used for resource management operations, where several different clients want to share a common resource. The server is responsible for managing this resource.
If we ignore how the server is started and stopped, and ignore all error cases, then it is possible to describe the server by means of a simple function f
.
Suppose that the internal state of the server is described by the state variable S
and that the server receives a query Q
. The server responds by sending a reply R
back to the client and changes its internal state to S'
. This can be described as follows:
{R, S'} = f(Q, S)
Given a function f
, we can write a very simple universal client server as follows:
-module(server). -copyright('Copyright (c) 1991-97 Ericsson Telecom AB'). -vsn('$Revision: /main/release/2 $'). -export([start/3, stop/1, loop/3, call/2]). start(Name, F, State) -> register(Name, spawn(server, loop, [Name, F, State])). stop(Name) -> exit(whereis(Name), kill). call(Name, Query) -> Name ! {self(), Query}, receive {Name, Reply} -> Reply end. loop(Name, F, State) -> receive {Pid, Query} -> {Reply, State1} = F(Query, State), Pid ! {Name, Reply}, loop(Name, F, State1) end.
vshlr
can be written using server
:
-module(vshlr_1). -copyright('Copyright (c) 1991-97 Ericsson Telecom AB'). -vsn('$Revision: /main/release/2 $'). -export([start/0, stop/0, i_am_at/2, find/1, handle_event/2]). start() -> server:start(xx1, fun(Event, State) -> handle_event(Event, State) end, []). stop() -> server:stop(xx1). i_am_at(Person, Position) -> server:call(xx1, {i_am_at, Person, Position}). find(Person) -> server:call(xx1, {find, Person}). handle_event({i_am_at, Person, Position}, State) -> State1 = update_position(Person, Position, State), {ok, State1}; handle_event({find, Person}, State) -> Location = lookup(Person, State), {Location, State}. update_position(Key, Value, [{Key, _}|T]) -> [{Key, Value}|T]; update_position(Key, Value, [H|T]) -> [H|update_position(Key, Value, T)]; update_position(Key, Value, []) -> [{Key,Value}]. lookup(Key, [{Key, Value}|_]) -> {at, Value}; lookup(Key, [_|T]) -> lookup(Key, T); lookup(Key, []) -> lost.
We can run this as follows:
1 > vshlr_1:start(). true 2> vshlr_1:i_am_at("joe", "home"). ok 3> vshlr_1:i_am_at("helen", "work"). ok 4> vshlr_1:find("joe"). {at,"home"} 5> vshlr_1:find("mike"). lost 6> vshlr_1:i_am_at("joe", {building,23}). ok 7> vshlr_1:find("helen"). {at,"work"} 8> vshlr_1:find("joe"). {at,{building,23}}
Even though our VSHLR program is extremely simple, it illustrates and provides simple solutions to a surprisingly large number of design issues.
The reader should note the following:
server
. All the code that has to do with the implementation of the VSHLR is contained in the module vshrl
. Note also that most of the functions in vshrl
can be written in a pure, side effect free manner. This division of functionality is good programming practice.
server
can be re-used to build many different client-server applications.
xx1
, is hidden from the users of the client functions. This means it can be changed without effecting the code that uses the client functions. This point has important consequences for writing distributed systems. Essentially, we can develop programs as non-distributed applications and then turn them into distributed applications by making very small changes to the client stub code. This point will be covered in more detail later.
server
module. This means that we can change how we do the remote procedure call at a later stage. This has consequences for error handling and the recovery from failures which may occur during a remote procedure call.
server
module. This is good programming practice and allows us to change the protocols without having to make any changes to the functions which use the server.
Splitting a server into two parts means that we can work on either of the parts without effecting the other. We can illustrate this by extending the server so that it logs the last ten requests and calls the error logger if something goes wrong. This version is called server1
to distinguish it from server
.
-module(server1). -copyright('Copyright (c) 1991-97 Ericsson Telecom AB'). -vsn('$Revision: /main/release/2 $'). -export([start/3, stop/1, loop/4, call/2]). start(Name, F, State) -> register(Name, spawn(server1, loop, [Name, F, State, []])). stop(Name) -> exit(whereis(Name), kill). call(Name, Query) -> Name ! {self(), Query}, receive {Name, error} -> exit(server_error); {Name, {ok, Reply}} -> Reply end. loop(Name, F, State, Buff) -> receive {Pid, Query} -> Buff1 = trim([Query|Buff]), case catch F(Query, State) of {'EXIT', Why} -> Pid ! {Name, error}, error_logger:error_msg({server_error, Name, Buff1}); {Reply, State1} -> Pid ! {Name, {ok, Reply}}, loop(Name, F, State1, Buff1) end end. trim([X1,X2,X3,X4,X5,X6,X7,X8,X9,X10|_]) -> [X1,X2,X3,X4,X5,X6,X7,X8,X9,X10]; trim(X) -> X.
server1
has exactly the same function interface as the previous version of server
.
If we use server1
together with vshlr
, we get an improved version of vshlr
, which has additional error handling facilities.
The improvement to VSHLR was made without any significant change to the code in the module vshlr
. This is a consequence of dividing the server into two parts, the generic part which is common to all servers, and the specific part which concerns the VSHLR problem.
The examples shown in the previous sections make it apparent that the server can be extended in a number of different ways. The module gen_server
provides a number of useful extensions to our simple server. In the following example, vshrl
is re-implemented using gen_server
.
The Reference Manual, stdlib
, module gen_server
has detailed information about the generic server.
-module(vshlr_2). -copyright('Copyright (c) 1991-97 Ericsson Telecom AB'). -vsn('$Revision: /main/release/2 $'). -export([start/0, start_link/0, stop/0, i_am_at/2, find/1]). -behaviour(gen_server). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). %% These are the interface routines start() -> gen_server:start({local, xx2}, vshlr_2, [], []). start_link() -> gen_server:start_link({local, xx2}, vshlr_2, [], []). stop() -> gen_server:call(xx2, die, 10000). i_am_at(Person, Location) -> gen_server:call(xx2, {i_am_at, Person, Location}, 10000). find(Person) -> gen_server:call(xx2, {find, Person}, 10000). %% These Routine MUST be exported since they are called by gen_server init(_) -> {ok, []}. handle_call({i_am_at, Person, Location}, _, State) -> State1 = update_location(Person, Location, State), {reply, ok, State1}; handle_call({find, Person}, _, State) -> Location = lookup(Person, State), {reply, Location, State}; handle_call(die, _, State) -> %% ok goes back to the user and terminate(normal, State) %% will be called {stop, normal, ok, State}. handle_cast(Request, State) -> {noreply, State}. handle_info(Request, State) -> {noreply, State}. terminate(Reason, State) -> ok. %% sub-functions update_location(Key, Value, [{Key, _}|T]) -> [{Key, Value}|T]; update_location(Key, Value, [H|T]) -> [H|update_location(Key, Value, T)]; update_location(Key, Value, []) -> [{Key,Value}]. lookup(Key, [{Key, Value}|_]) -> {at, Value}; lookup(Key, [_|T]) -> lookup(Key, T); lookup(Key, []) -> lost.
The flow of control in the example shown above is as follows:
gen_server:start({local, xx2}, vshlr_2, Args, Opts).
xx2
on the local node. The handler module vshlr_2
is called to initialize the server.vshlr_2:init(Args)
which is expected to return {ok, S}
. The value of S
is used as the initial value of the state of the server.
gen_server:call(xx2, {i_am_at, Person, Location}, 10000)For communication purposes,
xx2
is the name of the server and must agree with the name used to start the server. {i_am_at, Person, Location}
is a command which is passed to the server, and 10000 is a timeout value. If the server does not respond within 10000 milliseconds, the call to the server is aborted.handle_call({i_am_at, Person, Location}, _, State) -> State1 = update_Location(Person, Location, State), {reply, ok, State1};
handle_call
returns a tuple of the form{reply, Reply, State1}
. In this tuple, Reply
is the reply which should be sent back to the client, and State1
is a new value for the state of the server.gen_server:call(xx2, die, 10000).
handle_call(die, _, State) -> {stop, normal, ok, State}.The return value tells the server to stop. The server first evaluates
vshlr_2:terminate(normal, State)
. The reply, which is ok
in this example, is passed back to the client and the server stops.
The example shown in the previous section was a local server. The main points to note were:
gen_server:start({local, xx2}, ...)
starts the server.
gen_server:call(xx2, ...)
calls the server.
To make a global server, the following small changes are made to the access routines:
gen_server:start({global, xx2}, ...)
starts the server.
gen_server:call({global, xx2}, ...)
calls the server.
With these changes, the client-server model will work in a network of distributed nodes. All nodes in the system are assumed to evaluate identical copies of the code. The server will be placed on the first node which evaluates gen_server:start
. All other nodes will be coupled to this node automatically.
The following calls will start an anonymous server:
gen_server:start(Mod, ...) -> {ok, Pid}
starts an anonymous server. All calls to the server must include an explicit reference to the Pid of the server.
gen_server:call(Pid, ...)
calls the server.
The user must ensure that the Pid of the server is communicated to all clients which make use of the server.
gen_server:start
, or gen_server:start_link
. In the case of start_link
, the server is linked to the process which started the server.
start_link
function must be used if the server is supervised by a supervisor.
process_flag(trap_exit, true)
in init/1
before returning {ok, State}
.
stdlib
for more information.