|
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.Outgoing_Messages; with YAMI.Parameters;
The above packages are the most commonly used client-side packages that allow the program to create the agent and to send outgoing messages with nontrivial payloads.
Some standard packages are needed to process command line arguments, handle exceptions and to report the current activity on the console:
with Ada.Command_Line; with Ada.Exceptions; with Ada.Text_IO;
The whole client program is implemented in a single Client
procedure, which initial actions focus on argument retrieval and parsing - in addition to the requested server target, two integer numbers are read:
procedure Client is A : YAMI.Parameters.YAMI_Integer; B : YAMI.Parameters.YAMI_Integer; begin if Ada.Command_Line.Argument_Count /= 3 then Ada.Text_IO.Put_Line ("expecting three parameters: " & "server destination and two integers"); Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure); return; end if; begin A := YAMI.Parameters.YAMI_Integer'Value (Ada.Command_Line.Argument (2)); B := YAMI.Parameters.YAMI_Integer'Value (Ada.Command_Line.Argument (3)); exception when Constraint_Error => Ada.Text_IO.Put_Line ("cannot parse the second or third parameter"); Ada.Command_Line.Set_Exit_Status (Ada.Command_Line.Failure); return; end; declare Server_Address : constant String := Ada.Command_Line.Argument (1);
In order to send the message three entities are needed at the client side: agent to handle the communication, parameters object to carry the payload (the two integer values) and the outgoing message object for progress tracking:
Client_Agent : YAMI.Agents.Agent := YAMI.Agents.Make_Agent; Params : YAMI.Parameters.Parameters_Collection := YAMI.Parameters.Make_Parameters; Message : aliased YAMI.Outgoing_Messages.Outgoing_Message; State : YAMI.Outgoing_Messages.Message_State;
There is also a local procedure that is used to process the content of the reply that will be received from the server. This procedure extracts four values from the response content, which correspond to the results of four basic calculations performed by the server.
An important feature of the communication ``protocol'' in this example is that one of the components in the reply content is optional - that is, it might or might not be included in the content. The processing procedure takes care of that possibility with appropriate logic.
procedure Process_Reply (Content : in out YAMI.Parameters.Parameters_Collection) is Sum : constant YAMI.Parameters.YAMI_Integer := Content.Get_Integer ("sum"); Difference : constant YAMI.Parameters.YAMI_Integer := Content.Get_Integer ("difference"); Product : constant YAMI.Parameters.YAMI_Integer := Content.Get_Integer ("product"); Ratio : YAMI.Parameters.YAMI_Integer; Ratio_Entry : YAMI.Parameters.Parameter_Entry; Ratio_Defined : Boolean; begin Content.Find ("ratio", Ratio_Entry, Ratio_Defined); if Ratio_Defined then Ratio := YAMI.Parameters.Get_Integer (Ratio_Entry); end if;
Above, the Ratio_Defined
variable is True
when the reply content has the entry for the result of division (and then the Ratio
variable will get that value), and False
otherwise.
The processing procedure prints all received values on the console:
Ada.Text_IO.Put_Line ("sum = " & YAMI.Parameters.YAMI_Integer'Image (Sum)); Ada.Text_IO.Put_Line ("difference = " & YAMI.Parameters.YAMI_Integer'Image (Difference)); Ada.Text_IO.Put_Line ("product = " & YAMI.Parameters.YAMI_Integer'Image (Product)); Ada.Text_IO.Put ("ratio = "); if Ratio_Defined then Ada.Text_IO.Put_Line (YAMI.Parameters.YAMI_Integer'Image (Ratio)); else Ada.Text_IO.Put_Line ("<undefined>"); end if; end Process_Reply;
The main part of the client program composes and sends the outgoing message.
First, the message payload is formed to contain two parameters from the command-line:
use type YAMI.Outgoing_Messages.Message_State; begin Params.Set_Integer ("a", A); Params.Set_Integer ("b", B);
Then, the message is physically sent:
Client_Agent.Send (Server_Address, "calculator", "calculate", Params, Message'Unchecked_Access);
Above, the Send
operation is invoked with the following parameters:
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 first argument."calculator"
- 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."calculate"
- 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. For example, the calculator system might be restructured to allow separate message for each basic operation (``add'', ``subtract'', ``multiply'' and ``divide'') - then, the message name would be a very natural mean for expressing the request.Params
- The payload to be sent, which here contains two integer numbers from the command-line.Message'Unchecked_Access
- The access value to the user-provided outgoing message object. In this example the access value is obtained as ``unchecked'', because the object was created in the scope that is deeper than the definition of the access type. This usage is safe here, because the agent guarantees that the message object will not be referred after the interaction is completed and proper program structure ensures that the object will be still alive till that time. Creating the outgoing message at the library level (or dynamically) might be a valid alternative approach.
It is important to note that the actual message is transmitted to the server in background and there is no provision for it to be already transmitted when the Send
operation returns. In fact, the user task might continue its work while the message is being processed by the background I/O task.
In this particular example, the client program does not have anything to do except waiting for the response:
Message.Wait_For_Completion;
Depending on the message state, client either processes the result with the previously defined local procedure or prints appropriate report on the console:
State := Message.State; if State = YAMI.Outgoing_Messages.Replied then Message.Process_Reply_Content (Process_Reply'Access); elsif State = YAMI.Outgoing_Messages.Rejected then Ada.Text_IO.Put_Line ("The message has been rejected: " & Message.Exception_Message); else Ada.Text_IO.Put_Line ("The message has been abandoned."); end if;
Client code finishes with basic error handling:
end; exception when E : others => Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Message (E)); end Client;
The server program is entirely implemented in the server.adb
file.
First the relevant server-side YAMI4 packages are withed:
with YAMI.Agents; with YAMI.Incoming_Messages; with YAMI.Parameters;
The YAMI.Incoming_Messages
package defines the API allowing to interact with messages sent by clients.
Some standard packages will be needed as well:
with Ada.Command_Line; with Ada.Exceptions; with Ada.Text_IO;
The server is implemented in the Server
procedure.
One of the most important server-side entities is a message handler, which has to be a user-provided instance of the type that implements the Message_Handler
interface.
This interface has only one primitive operation that has to be overridden and redefined by the server code:
procedure Server is type Incoming_Message_Handler is new YAMI.Incoming_Messages.Message_Handler with null record; overriding procedure Call (H : in out Incoming_Message_Handler; Message : in out YAMI.Incoming_Messages.Incoming_Message'Class) is
The message handler in this example uses a local procedure to process the content of incoming message. This local procedure is similar in its form to the one used in the printer example.
First, appropriate data values (two integers named ``a'' and ``b'') are extracted from the content:
procedure Process (Content : in out YAMI.Parameters.Parameters_Collection) is A : YAMI.Parameters.YAMI_Integer; B : YAMI.Parameters.YAMI_Integer; Reply_Params : YAMI.Parameters.Parameters_Collection := YAMI.Parameters.Make_Parameters; use type YAMI.Parameters.YAMI_Integer; begin -- extract the parameters for calculations A := Content.Get_Integer ("a"); B := Content.Get_Integer ("b");
Then the reply content is prepared with results of the four basic computations. As was already explained with the client code, the ``protocol'' for request-response interaction in this example is that one of the entries in the result is optional - that is, it is present or not depending on message processing.
In this particular case the result of division is included in the reply content only when it was possible to compute it - if the divisor is zero, the result cannot be computed and the server expresses that fact by not including the ratio in the reply content.
-- prepare the answer -- with results of four calculations Reply_Params.Set_Integer ("sum", A + B); Reply_Params.Set_Integer ("difference", A - B); Reply_Params.Set_Integer ("product", A * B); -- if the ratio cannot be computed, -- it is not included in the response -- the client will interpret that fact properly if B /= 0 then Reply_Params.Set_Integer ("ratio", A / B); end if;
Once the reply content is prepared, the message can be replied to:
Message.Reply (Reply_Params);
The above operation can be performed only once on the given incoming message, which reflects the function-call analogies of remote invocations. As a result of the above, the reply is put into the outgoing queue of the relevant channel (the one on which the originating incoming message was received) where it is immediately ready for transmission. The I/O activity can take place in background, so that the main server code is not tied during this process.
The processing procedure finishes with a report on the console.
Ada.Text_IO.Put_Line ("got message with parameters " & YAMI.Parameters.YAMI_Integer'Image (A) & " and " & YAMI.Parameters.YAMI_Integer'Image (B) & ", response has been sent back"); end Process;
The message handler's Call
operation only delegates to the processing local procedure without performing any other action. After the processing procedure is done with its work (that includes preparing and posting the reply), the message handler can complete as well - this is completely independent on when exactly the reply might be actually transmitted to the client.
begin Message.Process_Content (Process'Access); end Call;
The message handler has to be instantiated before it is given to the agent. The handler is instantiated as an aliased object so that the agent can refer to it by by its base interface's access value.
My_Handler : aliased Incoming_Message_Handler;
The main part of the server program deals with command-line arguments, where the server target is expected.
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);
Once the server target is known, it is passed to the freshly constructed agent in order to add a new listener. The resolved target that results from this operation is printed on the console.
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); Ada.Text_IO.Put_Line ("The server is listening on " & Resolved_Server_Address (1 .. Resolved_Server_Address_Last));
The previously defined message handler is registered as an implementation of the logical object named ``calculator''. This object name has to be known by clients so that they can properly send their messages.
Server_Agent.Register_Object ("calculator", My_Handler'Unchecked_Access);
Above, the access value of the message handler is obtained as unchecked, because the handler is defined in the deeper scope than that of the access type definition. This construct is safe, because the handler will anyway live longer than the agent and therefore there is no risk of referring to it after its finalization. In more elaborate servers the message handler might be defined at the library level, in which case the unchecked access would not be needed.
The server's environment task effectively blocks forever by falling into an infinite loop. From that time all of the server's activity is driven by incoming messages:
loop delay 10.0; end loop;
The code concludes with rudimentary exception handling:
end; exception when E : others => Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Message (E)); end Server;