4 Servers
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
instdlib
, for full details of the behaviour interface.4.1 Client-Server Principles
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 thatPerson
is at the locationPosition
.vshrl_XX:find(Person) -> {at, Position} | lost
asks the server wherePerson
is. The server responds{at, Position}
, wherePosition
is the last reported location, orlost
if it does not know where the person is.The client-server model can be illustrated in the following figure:
The Client-Server ModelThe 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 queryQ
. The server responds by sending a replyR
back to the client and changes its internal state toS'
. 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 usingserver
:-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:
- The functionality is divided between two different modules. All the code that deals with spawning processes and sending and receiving messages is contained in the module
server
. All the code that has to do with the implementation of the VSHLR is contained in the modulevshrl
. Note also that most of the functions invshrl
can be written in a pure, side effect free manner. This division of functionality is good programming practice.- The code in
server
can be re-used to build many different client-server applications.- The name of the server, in this example the atom
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.- We hide the details of the remote procedure call inside the
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.- We hide the details of the protocol used between the client and the server inside the
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.4.1.1 Extending 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 fromserver
.-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 ofserver
.If we use
server1
together withvshlr
, we get an improved version ofvshlr
, 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.4.1.2 The Generic Server
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 usinggen_server
.The Reference Manual,
stdlib
, modulegen_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:
- Start a new server by evaluation the expression
gen_server:start({local, xx2}, vshlr_2, Args, Opts).
This expression starts a local server with namexx2
on the local node. The handler modulevshlr_2
is called to initialize the server.
The generic server callsvshlr_2:init(Args)
which is expected to return{ok, S}
. The value ofS
is used as the initial value of the state of the server.
- The client routines use the following type of calls:
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.
The previous call corresponds to the clause:
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, andState1
is a new value for the state of the server.
- Stop the server by evaluating
gen_server:call(xx2, die, 10000).
This matches the following expression:
handle_call(die, _, State) -> {stop, normal, ok, State}.The return value tells the server to stop. The server first evaluatesvshlr_2:terminate(normal, State)
. The reply, which isok
in this example, is passed back to the client and the server stops.
4.1.3 Local Client-Server
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.4.1.4 Global Client-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.4.1.5 Anonymous Server
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.
4.2 Notes
- The server can be started with
gen_server:start
, orgen_server:start_link
. In the case ofstart_link
, the server is linked to the process which started the server.- The
start_link
function must be used if the server is supervised by a supervisor.- The server does not normally trap exits and will die if it receives an exit signal. If you wish the server to trap exits then you should evaluate
process_flag(trap_exit, true)
ininit/1
before returning{ok, State}
.- This section has only described the remote procedure call interface to the server. You can also send a cast to the server. In a cast, the client sends a message to the server and continues since no reply is sent by the server.
- The server can also handle exit messages and information requests from the management system. Refer to the Reference Manual,
stdlib
for more information.