Gem #84: The Distributed Systems Annex 1 - Simple client/server
by Thomas Quinot —AdaCore
Let's get started…
Many aspects of software engineering require, or can benefit from, distributed technology:
- Load balancing
- Fault tolerance
- Interconnection between multiple agents
... among others.
In each of these instances, it is useful to enlist the contribution of multiple computers to achieve a certain goal in a coordinated fashion. In a distributed application design, parts of the processing are thus assigned to distinct hosts which communicate in order to provide a given service. In Ada parlance, the fraction of the complete application that is assigned to each host is called a partition.
A distributed design can be implemented using direct calls to communication services provided by the environment, allowing the exchange of data between partitions. However, this is extremely cumbersome and error-prone. Distribution models have therefore been defined, which are sets of high-level abstractions allowing the programmer to express the interactions between components of a distributed application -- possibly located on different partitions -- in convenient high-level terms.
Distribution models support various communication patterns. The simplest ones support simple message passing. More elaborate models also provide more structured patterns, such as remote subprogram calls (based on the natural abstraction boundaries represented by subprograms) and distributed (remote) objects, extending remote subprogram calls to the case of method calls in an object-oriented design.
The services afforded by distribution middleware (i.e., the implementation of a distribution model) can be made available to the programmer in different ways. Explicit distribution APIs can be used. Alternatively, distribution may be included in the facilities provided by a programming language. Ada 95 and Ada 2005 include such features as part of the optional Annex E of the Reference Manual.
In this first introductory example, we consider a simple application managing a public bulletin board, which we want to make available for posting from several partitions. The DSA allows a service to be offered in a very simple way: you just write a package declaration:
package Bulletin_Board is pragma Remote_Call_Interface; -- This makes the package a Remote Call Interface (RCI), so the subprograms -- below are remotely callable. -- This pragma enforces some restrictions on the unit to ensure that any -- visible subprogram can actually be called remotely, and in particular -- that the types of the parameters are suitable for transport over a -- communication link from one partition to another. subtype Length is Natural range 0 .. 100; type News_Item (Author_Length, Message_Length : Length := 0) is record Author : String (1 .. Author_Length); Message : String (1 .. Message_Length); end record; type News_Items is array (Positive range <>) of News_Item; procedure Post (Item : News_Item); function Whats_Up return News_Items; end Bulletin_Board;
A simple client can then be written that will just make calls to these subprograms. The fact that these calls may be executed remotely is completely transparent in the code.
with Ada.Text_IO; use Ada.Text_IO; with Bulletin_Board; use Bulletin_Board; procedure Post_Message is Author, Message : String (1 .. 140); Author_Length, Message_Length : Natural; begin Put ("Author name: "); Get_Line (Author, Author_Length); Put ("Message : "); Get_Line (Message, Message_Length); Post (News_Item' (Author_Length => Author_Length, Message_Length => Message_Length, Author => Author (1 .. Author_Length), Message => Message (1 .. Message_Length))); -- This subprogram call may be remote, but we write it exactly in the -- usual way. end Post_Message;
Similarly, a procedure that displays all messages can be written as follows:
with Ada.Text_IO; use Ada.Text_IO; with Bulletin_Board; use Bulletin_Board; procedure Display_Messages is begin loop Put_Line ("----- all messages -----"); declare Contents : constant News_Items := Whats_Up; begin for J in Contents'Range loop Put_Line (Contents (J).Author & " says:"); Put_Line (Contents (J).Message); New_Line; end loop; delay 2.0; end; end loop; end Display_Messages;
This procedure can run on the same partition as the one where Bulletin_Board
is located (the language requires that each Remote_Call_Interface unit is assigned to exactly one partition). However, since it only uses a visible subprogram declared in Bulletin_Board
(Whats_Up
), it could also very well run in another partition.
The assignment of units to partitions need not be apparent in sources. The same set of sources can even be used for different partitioning configurations (or used without partitioning to build a monolithic version of the application, in which case there is no distribution overhead at all).
The process of partitioning a DSA application is implementation defined. In GNAT, this is done using the gnatdist
tool, and a po_gnatdist
configuration file. The syntax for this file is documented in the PolyORB User's Guide.
Here is an example configuration for the bulletin board application:
configuration Dist_App is pragma Starter (None); -- User starts each partition manually ServerP : Partition := (Bulletin_Board); -- RCI package Bulletin_Board is on partition ServerP ClientP : Partition := (); -- Partition ClientP has no RCI packages for ClientP'Termination use Local_Termination; -- No global termination procedure Display_Messages is in ServerP; -- Main subprogram of master partition procedure Post_Message; for ClientP'Main use Post_Message; -- Main subprogram of slave partition end Dist_App;
After running po_gnatdist
on this configuration file, two executables are produced: serverp
and clientp
. Serverp
will loop, displaying all posted messages, and clientp
will allow sending a message to the server. This example thus shows how a simple client/server design can be implemented in Ada without any network programming.
In a future Gem we will discuss remote object designs, which allow flexible dynamic communication across partitions.