Inspirel banner

Programming Distributed Systems with YAMI4

9.2.3 Java

The Java client and server can be compiled by running the following command, assuming that the Java library was already compiled:

$ javac -cp ../../../../lib/yami4.jar *.java

Similar classpath argument will be needed to run the programs. For example, the following is necessary to start the server program with the shortest possible target:

$ java -cp ../../../../lib/yami4.jar:. Server 'tcp://*:*'

The client program is entirely implemented in the Client.java file, which is dissected in detail below.

First the relevant client-side YAMI4 classes are imported:

import com.inspirel.yami.Agent;
import com.inspirel.yami.OutgoingMessage;
import com.inspirel.yami.Parameters;

The com.inspirel.yami.Agent class is needed so that the client program can create its own agent that will manage the communication.

The com.inspirel.yami.OutgoingMessage class encapsulates the functionality of the outgoing message and the ability to track its progress.

The com.inspirel.yami.Parameters class allows to build a proper message content, where the string value will be placed.

The whole client program is implemented within the main function of the Client class. Initial actions in this procedure check whether there are three required arguments describing server destination address and two integer values:

public class Client {
    public static void main(String[] args) {
        if (args.length != 3) {
            System.out.println(
                "expecting three parameters: " +
                "server destination and two integers");
            return;
        }

        String serverAddress = args[0];

        int a;
        int b;
        try {
            a = Integer.parseInt(args[1]);
            b = Integer.parseInt(args[2]);
        } catch (NumberFormatException ex) {
            System.out.println(
                "cannot parse the parameters");
            return;
        }

After the program arguments are processed, client can create its own YAMI agent.

        try {
            Agent clientAgent = new Agent();

Then the parameters object is prepared that will be used as a payload of the outgoing message - here it contains the two integer values that were read from the command-line with names ``a'' and ``b''. These names are recognized by the server.

            Parameters params = new Parameters();
            params.setInteger("a", a);
            params.setInteger("b", b);

When the parameters object is filled with data, the actual outgoing message is created:

            OutgoingMessage message =
                clientAgent.send(serverAddress,
                    "calculator", "calculate", params);

Above, the send() function is invoked with the following parameters:

The outgoing message object is created as a result of this operation.

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() function returns. In fact, the user thread might continue its work while the message is being processed by the background I/O thread.

In this particular example, the client program does not have anything to do except waiting for the response:

            message.waitForCompletion();

Depending on the message state, client either processes the result or prints appropriate report on the console.

In the most expected case the completion of the message is a result of receiving appropriate reply from the server - this can be checked with the REPLIED message state:

            OutgoingMessage.MessageState state =
                message.getState();
            if (state == OutgoingMessage.MessageState.REPLIED) {
                Parameters reply = message.getReply();

                int sum =
                    reply.getInteger("sum");
                int difference =
                    reply.getInteger("difference");
                int product =
                    reply.getInteger("product");
                int ratio = 0;

                Parameters.Entry ratioEntry =
                    reply.find("ratio");
                boolean ratioDefined = ratioEntry != null;
                if (ratioDefined) {
                    ratio = ratioEntry.getInteger();
                }

Above, the ratioDefined 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. This reflects the ``protocol'' of interaction between client and server that allows the server to send back the ratio only when it can be computed.

The code then prints all received values on the console:

                System.out.println("sum        = " + sum);
                System.out.println("difference = " +
                    difference);
                System.out.println("product    = " + product);

                System.out.print("ratio      = ");
                if (ratioDefined) {
                    System.out.println(ratio);
                } else {
                    System.out.println("<undefined>");
                }

Alternatively, the message might be completed due to rejection or being abandoned - these cases are reported as well:

            } else if (state ==
                OutgoingMessage.MessageState.REJECTED) {

                System.out.println(
                    "The message has been rejected: " +
                    message.getExceptionMsg());
            } else {
                System.out.println(
                    "The message has been abandoned.");
            }

The outgoing message manages some internal resources that need to be properly cleaned up. In this example, the whole program sends only one message, so both the agent and the message need to be cleaned up after they are no longer needed. In more complex systems a single agent will be used for processing many messages, so the clean-up pattern will be different as well.

            message.close();
            clientAgent.close();

The client program finishes with simple exception handling.

        } catch (Exception ex) {
            System.out.println(
                "error: " + ex.getMessage());
        }
    }
}

The server program is entirely implemented in the Server.java file, which begins with a typical set of server-side imports:

import com.inspirel.yami.Agent;
import com.inspirel.yami.IncomingMessage;
import com.inspirel.yami.IncomingMessageCallback;
import com.inspirel.yami.Parameters;

One of the most important server-side entities is a message handler, which has to be an instance of the class that implements the IncomingMessageCallback interface. This interface defined only one function, call(), which accepts the incoming message object.

In this example the message handler is implemented as a nested static class:

public class Server {
    private static class Calculator
        implements IncomingMessageCallback {

        @Override
        public void call(IncomingMessage im)
            throws Exception {

The handler has to extract the content of the message, which is a regular parameters object. In the calculator system the content is supposed to have two integer entries named ``a'' and ``b'':

            // extract the parameters for calculations

            Parameters params = im.getParameters();

            int a = params.getInteger("a");
            int b = params.getInteger("b");

The server computes the results of four basic calculations on these two numbers, with the possible omission of the ratio, which might be impossible to compute if the divisor is zero. In this case the ratio entry is simply not included in the resulting parameters object - note that the possibility of missing entry in the message response is properly handled by the client and this is part of the calculator ``protocol''.

            // prepare the answer
            // with results of four calculations

            Parameters replyParams = new Parameters();

            replyParams.setInteger("sum", a + b);
            replyParams.setInteger("difference", a - b);
            replyParams.setInteger("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) {
                replyParams.setInteger("ratio", a / b);
            }

Once the parameters object for the reply is prepared, the incoming message can be replied to:

            im.reply(replyParams);

The handler finishes with a report on the console.

            System.out.println("got message with parameters " +
                a + " and " + b +
                ", response has been sent back");
        }
    }

The main() function of the server program deals with command-line arguments, where the server target is expected.

    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println(
                "expecting one parameter: server destination");
            return;
        }

        String serverAddress = args[0];

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.

        try {
            Agent serverAgent = new Agent();

            String resolvedAddress =
                serverAgent.addListener(serverAddress);

            System.out.println(
                "The server is listening on " +
                resolvedAddress);

The previously defined message handler is instantiated and 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.

            serverAgent.registerObject(
                "calculator", new Calculator());

The server's main thread effectively stops in the infinite loop. In this state the whole activity of the server program is driven by incoming messages that are received in background.

It should be noted that the background threads created by the agent are of the ``daemon'' type, which means that they alone are not able to keep the process alive. If the main thread was allowed to quit at this point, the whole server program would finish immediately without giving the clients any chance for interaction.

            // block
            while (true) {
                Thread.sleep(10000);
            }

The server code finishes with simple exception handling:

        } catch (Exception ex) {
            System.out.println(
                "error: " + ex.getMessage());
        }
    }
}