Inspirel banner

Programming Distributed Systems with YAMI4

9.1.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 YAMI4 classes are imported:

import com.inspirel.yami.Agent;
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.Parameters class allows to build a proper message content, where the string value will be placed.

Some standard classes are also essential to read text from the standard input:

import java.io.BufferedReader;
import java.io.InputStreamReader;

The whole client program is implemented within the main function of the Client class. Initial actions in this procedure check whether there is one required argument describing server destination address:

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

        String serverAddress = args[0];

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

        try {
            Agent clientAgent = new Agent();

The whole logic of the client program is devoted to reading lines of text from standard input:

            // read lines of text from standard input
            // and post each one for transmission

            BufferedReader reader = new BufferedReader(
                new InputStreamReader(System.in));
            String inputLine;
            while (
                (inputLine = reader.readLine()) != null) {

For each line of text, a new message is sent to the server with parameters object that contains the given text. Once the parameters object is created, it can be filled with data - here, only one data entry is needed:

                Parameters params = new Parameters();

                // the "content" field name is arbitrary,
                // but needs to be recognized at the server side

                params.setString("content", inputLine);

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:

                clientAgent.sendOneWay(serverAddress,
                    "printer", "print", params);

The message is sent as one-way, which means that there is no feedback on its progress.

The arguments to the sendOneWay() function are:

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 agent object manages system resources and needs to be explicitly closed when no longer needed - here, after the last line of text is entered (and the EOF condition is found), the loop terminates and the agent is clean up:

            }

            clientAgent.close();

The program code concludes with rudimentary exception handling:

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

Another version of the client program (ClientSynchronous.java) 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 class needs to be imported in order to provide access to the outgoing message API:

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

Then the program is implemented as before:

import java.io.BufferedReader;
import java.io.InputStreamReader;

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

        String serverAddress = args[0];

        try {
            Agent clientAgent = new Agent();

            // read lines of text from standard input
            // and post each one for transmission

            BufferedReader reader = new BufferedReader(
                new InputStreamReader(System.in));
            String inputLine;
            while (
                (inputLine = reader.readLine()) != null) {

                Parameters params = new Parameters();

                // the "content" field name is arbitrary,
                // but needs to be recognized at the server side

                params.setString("content", inputLine);

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.

                OutgoingMessage message =
                    clientAgent.send(serverAddress,
                        "printer", "print", params);

The user code can then synchronize itself with the message delivery by waiting on its transmission condition:

                message.waitForTransmission();

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.

The OutgoingMessage object manages some internal resources and needs to be explicitly closed when no longer used:

                message.close();

As in the previous version, the agent is closed after the last line of text was processed:

            }

            clientAgent.close();

Program code concludes with simple exception handling:

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

The server program (Server.java) is also implemented in a single file and begins with appropriate import statements:

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

The com.inspirel.yami.IncomingMessage class allows the server program to use the operations of incoming message objects and is almost always used by server programs.

Another important server-side import is for com.inspirel.yami.IncomingMessageCallback, which is a callback interface for user-defined message handlers. In this example the message handler is defined as a nested class named Printer:

public class Server {
    private static class Printer
        implements IncomingMessageCallback {

The above type defines new stateless class - more elaborated servers with have stateful message handlers.

The message handler type needs to implement a single call() function that accepts the incoming message object:

        @Override
        public void call(IncomingMessage im) {
            Parameters params = im.getParameters();

            // extract the content field
            // and print it on standard output

            System.out.println(
                params.getString("content"));
        }
    }

In this example the call() function has to print the line of text that was sent by remote client on the screen - here this process is straightforward since the parameters object can be accessed as a ``subcomponent'' of the incoming message object. No other processing is performed and in particular no feedback - neither reply nor rejection - is sent back to the client.

In its main part the server program first verifies whether it was given a single required argument that is interpreted as a server target:

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

        String serverAddress = args[0];

Then the agent object is created and a listener is added to it with the given target name:

        try {
            Agent serverAgent = new Agent();

            String resolvedAddress =
                serverAgent.addListener(serverAddress);

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:

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

Then the ``printer'' object is registered with the new instance of the already defined message handler class:

            serverAgent.registerObject(
                "printer", new Printer());

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 main thread effectively stops by executing infinite loop. Such a loop is typical for servers that are entirely driven by remote messages.

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());
        }
    }
}