|
The Ada client and server can be compiled by running make
, assuming that the core library was already compiled.
The client program is entirely implemented in the client.adb
file, which is dissected in detail below.
First the relevant YAMI4 packages are withed:
with YAMI.Agents; with YAMI.Parameters;
The YAMI.Agents
package is needed so that the client program can create its own agent that will manage the communication. The YAMI.Parameters
package allows to build a proper message content, where the string value will be placed.
Some standard packages are also essential to manage program arguments, handle exceptions and read text from the standard input:
with Ada.Command_Line; with Ada.Exceptions; with Ada.Text_IO;
The whole client program is implemented within a single main procedure named Client
.
Initial actions in this procedure check whether there is one required argument describing server destination address:
procedure Client is begin if Ada.Command_Line.Argument_Count /= 1 then Ada.Text_IO.Put_Line ("expecting one parameter: server destination"); Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure); return; end if; declare Server_Address : constant String := Ada.Command_Line.Argument (1);
After the program arguments are processed, client can create its own YAMI agent.
The newly created agent has to be initialized with proper constructor call:
Client_Agent : YAMI.Agents.Agent := YAMI.Agents.Make_Agent;
The whole logic of the client program is devoted to reading lines of text from standard input:
begin -- read lines of text from standard input -- and post each one for transmission while not Ada.Text_IO.End_Of_File loop declare Input_Line : constant String := Ada.Text_IO.Get_Line;
For each line of text, a new message is sent to the server with parameters object that contains the given text. The parameters object also needs to be created with initialization by dedicated constructor function:
Params : YAMI.Parameters.Parameters_Collection := YAMI.Parameters.Make_Parameters;
Once the parameters object is created, it can be filled with data - here, only one data entry is needed:
begin -- the "content" field name is arbitrary, -- but needs to be recognized at the server side Params.Set_String ("content", Input_Line);
The data entry is named "content"
, but the name does not matter as long as it is properly recognized by the server.
In other words, the choice of entry names forms a kind of contract between client and server and can be compared to the naming of method parameters in object-oriented programming.
Everything is now ready for sending the message:
Client_Agent.Send_One_Way (Server_Address, "printer", "print", Params);
The message is sent as one-way, which means that there is no feedback on its progress.
The arguments to the Send_One_Way
operation are:
Server_Address
- The server target, as it was resolved on the server side when the server's listener was created; this is the string that was printed by the server just after starting and passed to the client program as its only argument."printer"
- The logical destination object name; this name has to be known by the server agent, which is true if the message handler was registered there with this name. This name can be interpreted as the object name within the namespace of the server's agent."print"
- The message name, which is simply the name of the requested operation that the server should perform. In this particular example there is only one type of message that is sent from client to server, but in bigger systems this name will be certainly used by the server to figure out what to do.Params
- The payload to be sent, which here contains the line of text that was just read from the standard input.It should be noted that the message is being sent in background, so the user task can immediately continue and read another line of text.
The program code concludes with rudimentary exception handling:
end; end loop; end; exception when E : others => Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Message (E)); end Client;
Another version of the client program (client_synchronous.adb
) synchronizes each message separately and allows to guarantee that all lines of text are really delivered to the server, so that when the client program finishes there are no messages pending in its outgoing queue.
The code of that alternative client is very similar and there are only two differences.
The first is that yet another YAMI4 package needs to be withed in order to provide access to the outgoing message API:
with YAMI.Agents; with YAMI.Outgoing_Messages; with YAMI.Parameters;
Then the program is implemented as before:
with Ada.Command_Line; with Ada.Exceptions; with Ada.Text_IO; procedure Client_Synchronous is begin if Ada.Command_Line.Argument_Count /= 1 then Ada.Text_IO.Put_Line ("expecting one parameter: server destination"); Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure); return; end if; declare Server_Address : constant String := Ada.Command_Line.Argument (1); Client_Agent : YAMI.Agents.Agent := YAMI.Agents.Make_Agent; begin -- read lines of text from standard input -- and post each one for transmission while not Ada.Text_IO.End_Of_File loop declare Input_Line : constant String := Ada.Text_IO.Get_Line; Params : YAMI.Parameters.Parameters_Collection := YAMI.Parameters.Make_Parameters;
Another implementation difference is the part that sends the message - instead of posting a one-way message to the queue, a regular message object is created and later used for synchronization with its delivery.
The message object needs to be initialized with appropriate constructor call:
Message : aliased YAMI.Outgoing_Messages.Outgoing_Message; begin -- the "content" field name is arbitrary, -- but needs to be recognized at the server side Params.Set_String ("content", Input_Line);
In order to send this message, its access value needs to be given to the agent, so that further activity and message progress can be reflected in the internal state of the message object:
Client_Agent.Send (Server_Address, "printer", "print", Params, Message'Unchecked_Access);
The user code can then synchronize itself with the message delivery by waiting on its transmission condition:
Message.Wait_For_Transmission;
The above call ensures that the user code will not proceed until the line of text is sent to the server - if this was the last line, the program can finish immediately by exiting the main loop without any risk of losing pending messages.
Program code concludes with simple exception handling:
end; end loop; end; exception when E : others => Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Message (E)); end Client_Synchronous;
The server program (server.adb
) is also implemented in a single file and begins with appropriate context clause:
with YAMI.Agents; with YAMI.Incoming_Messages; with YAMI.Parameters;
The YAMI.Incoming_Messages
package allows the server program to use the operations of incoming message objects and is almost always used by server programs.
Some standard packages are also needed:
with Ada.Command_Line; with Ada.Exceptions; with Ada.Text_IO; procedure Server is
An important part of every server is the definition of its message handlers. In this example there is only one such handler:
type Incoming_Message_Handler is new YAMI.Incoming_Messages.Message_Handler with null record;
The above type defines new tagged message handler type without any extension part - more elaborated servers with stateful message handlers will use the extension to define their state components.
The message handler type needs to implement a single Call
procedure that accepts the incoming message object:
overriding procedure Call (H : in out Incoming_Message_Handler; Message : in out YAMI.Incoming_Messages.Incoming_Message'Class) is procedure Process (Content : in out YAMI.Parameters.Parameters_Collection) is begin -- extract the content field -- and print it on standard output Ada.Text_IO.Put_Line (Content.Get_String ("content")); end Process; begin Message.Process_Content (Process'Access); end Call;
In this example the Call
procedure has to print the line of text that was sent by remote client on the screen - here a local Process
procedure is used to inspect the message content (this is similar to the idioms of standard containers). No other processing is performed and in particular no feedback - neither reply nor rejection - is sent back to the client.
The message handler has to be instantiated in order to be used:
My_Handler : aliased Incoming_Message_Handler;
In its main part the server program first verifies whether it was given a single required argument that is interpreted as a server target:
begin if Ada.Command_Line.Argument_Count /= 1 then Ada.Text_IO.Put_Line ("expecting one parameter: server destination"); Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure); return; end if; declare Server_Address : constant String := Ada.Command_Line.Argument (1);
Then the agent object is created and a listener is added to it with the given target name:
Server_Agent : YAMI.Agents.Agent := YAMI.Agents.Make_Agent; Resolved_Server_Address : String (1 .. YAMI.Agents.Max_Target_Length); Resolved_Server_Address_Last : Natural; begin Server_Agent.Add_Listener (Server_Address, Resolved_Server_Address, Resolved_Server_Address_Last);
The resolved server address is printed on the screen to show how it was expanded and to allow the user to pass it properly to the client program:
Ada.Text_IO.Put_Line ("The server is listening on " & Resolved_Server_Address (1 .. Resolved_Server_Address_Last));
Then the ``printer'' object is registered with the already defined message handler:
Server_Agent.Register_Object ("printer", My_Handler'Unchecked_Access);
After creating the listener and registering the object the server is ready for operation - every message that will arrive from remote clients will be routed to the registered message handler and the line of text that was sent by client will be printed on the screen.
In order to allow the agent to operate in background the environment task effectively stops by executing infinite loop. Such a loop is typical for servers that are entirely driven by remote messages:
loop delay 10.0; end loop;
The server code finishes with simple exception handling:
end; exception when E : others => Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Message (E)); end Server;