Writing Distributed Applications in Java, Part 1

by Qusay H. Mahmoud

Welcome to part 1 in a series of articles on writing distributed applications in Java. In each article of this series, we'll discuss a different approach to writing distributed applications. Also, each article will present a short tutorial, a sample distributed application, and some hints and guidelines to programming techniques that work well with distributed applications. Using these articles as guides, you should be able to build a substantial, safe, distributed application that works correctly and efficiently. Basically, there are four ways of writing distributed applications in Java: using low-level sockets, Remote Method Invocation (RMI), a CORBA implementation (such as JavaIDL, VisiBroker from Visigenic, OrbixWeb from IONA Technologies, etc.), or Mobile Agents. In this article, we'll consider each of these methods in detail.

Java's distributed applications support The Java Virtual Machine (JVM) enhances the portability of software across a heterogeneous network. Since Java is architecturally neutral, an application that's written in it can run on any system with a Java interpreter. This is a very important feature since it allows network-based applications to run on all of the different platforms on the Internet. The following are some of the tools and APIs that Java supports for writing distributed applications:

Sockets: Java supports connection-oriented (TCP) and connectionless (UDP) protocols over which sockets communicate.

RMI: Remote Method Invocation enables programmers to write distributed applications in which the methods of remote objects can be invoked from other Java Virtual Machines, possibly running on different hosts.

JavaIDL: JavaIDL (an implementation of CORBA) enables the programmer to define remote interfaces using the Interface Definition Language (IDL)--an industry standard defined by the Object Management Group (OMG).

Mobile Agents: While Java doesn't have a set of APIs for programming mobile agents, its introduction has led to an explosive interest in mobile agents. Only a couple of years ago, the most popular environment for writing mobile agents was Telescript from General Magic. However, a number of environments are now based on Java. Mobile Agents represent a new paradigm for writing intelligent distributed applications. Their approach is attractive because, unlike all other approaches with Mobile Agents, the reliability of a continuous network connection isn't crucial.

Sockets programming in Java You can implement distributed applications using three models: Client/Server based, object-based (Distributed Objects), and, of course, the new mobile agent-based model. The Client/Server model remains an important model for writing distributed applications. In this model, there's a set of server processes, each acting as a resource manager for a collection of a given type of resources. This model also has a set of client processes, each performing a task that requires access to some shared hardware and software resources. Resource managers themselves may need to access resources shared by another process--hence some processes are both client and server processes.

In the Client/Server model, all shared resources are held and managed by server processes. If a server receives a valid request, it performs the action and sends a reply to the client process. In the Client/Server model, both the client and the server usually speak the same language--known as the protocol that the client and server must understand to be able to communicate. You'll typically implement the Client/Server model using low-level sockets or remote procedure calls. Communication protocols You can use two common communication protocols for TCP/IP socket programming: datagram communication and stream communication. Let's look at each one in detail.

Datagram communication

The datagram communication protocol, also known as User Datagram Protocol (UDP), is a connectionless protocol. This means that each time you send a message in a datagram, you'll also need to send the local socket descriptor and the receiving socket's address. So, some extra data must be sent each time that a communication is made between a pair of sockets.

Stream communication

The stream communication protocol, also known as Transfer Control Protocol (TCP), is a connection-oriented protocol. That means if you want a pair of sockets to communicate, then you must first make a connection between them. One of the sockets, normally known as the "server," will be listening for connection requests, while the other socket, known as the "client," asks for connection. Once a connection is established between a pair of sockets, they can be used to transmit data in both directions. When to choose UDP or TCP The following discussion should help you decide whether to use UDP or TCP for your distributed application. In UDP, every time you send a datagram, you also need to send the local descriptor and the socket address of the receiving socket along with it. On the other hand, since TCP is a connection-oriented protocol, you must establish a connection before communications between the pair of sockets start. There's connection setup time involved in TCP. In UDP, there's a 64 KB size limit on the datagrams you can send to a specified location, while in TCP there's no limit. In TCP, once a connection is established, the pair of sockets behaves like streams--all available data is read immediately in the same order in which it's received. Now, it's clear that TCP is a reliable protocol, because it guarantees that the packets you send will be received in the order in which you sent them.

Conversely, UDP is an unreliable protocol, since there's no guarantee that the datagrams you send will be received in the same order by the receiving socket. In short, it's fair to say that TCP is suitable for implementing network services such as a remote login (rlogin, telnet), file transfer (FTP), and Web server, which require data of indefinite length to be transferred.

UDP is less complex and incurs less overhead, so it's often used in implementing Client/Server applications in distributed systems built over local area networks. However, we think you'll become a TCP convert when you see what it can do for you, so we'll use TCP in the examples in this article.

Programming sockets in Java Programming sockets in Java is easy. Basically, there are four steps to programming a client or a server using sockets: opening a socket, creating a data input stream, creating a data output stream, and closing a socket. And, of course, you have to develop the protocol (or language) so the client and server can communicate with each other. Now, let's look at each of the steps involved in programming a socket. Opening a socket If you were programming a client, you'd open a socket like this:


Socket MyClient = null;
try {
  MyClient = new 
    Socket("Machine_name", 
	PortNumber);
} 
catch(Unknown HostException uhe) {
  uhe.printStackTrace();
}

If you were programming a server, you'd open a socket as follows:


ServerSocket MyService = null;
try {
  MyService = new ServerSocket
	(PortNumber);
} 
catch(UnknownHostException uhe {
  uhe.printStackTrace();
}

When implementing a server, you also need to create a socket object from the ServerSocket in order to listen for and accept connections from clients. You do that as follows:



Socket serviceSocket = null;
try {
  serviceSocket = MyService.accept();
}
Creating an input stream On the client side, you can use the BufferedReader class (note this is in JDK1.1.) to receive response from the server


BufferedReader is = null;
try {
  is = new BufferedReader(new 
    InputStreamReader(
      MyClient.getInputStream());
} 
catch(IOException ioe) {
  ioe.printStackTrace();
}

On the server side, you can use the BufferedReader to receive input from the client


BufferedReader is = null;
try {
  is = new BufferedReader(new
    InputStreamReader(
      serviceClient.
	getInputStream());
} 
catch(IOException ioe) {
  ioe.printStackTrace();
}

Please refer to the documentation on BufferedReader to see all the handy methods it provides for reading lines of text and Java primitive data types.

Creating an output stream On the client side, you can create an output stream to send data to the server socket using the class DataOutputStream


DataOutputStream os = null;
try {
  os = new DataOutputStream(
    MyClient.getOutputStream());
} 
catch(IOException ix) {
  ix.printStackTrace();
}
On the server side, you can use the class DataOutputStream to send data to the client

DataOutputStream os = null;
try {
  os = new DataOutputStream(
    serviceClient.
	getOutputStream());
} 
catch(IOException ie) {
  ie.printStackTrace();
}
Closing sockets It's important to note that you should always close the output and input streams before closing the sockets. So, on the client side, you do the following:

try {
  os.close();
  is.close();
  MyClient.close();
} 
catch(IOException io) {
  io.printStackTrace();
}
and, on the server side, you write

try {
  os.close();
  is.close();
  serviceSocket.close();
  MyService.close();
} 
catch(IOException ic) {
  ic.printStackTrace();
}
Example: math Client/Server Now that we've covered the basics of writing socket programs, let's look at a completely functional Client/Server application. We'll look at a math application. In this distributed application, the client will send two arrays of integers to the math server. The math server will then add the two arrays and return the result (as an array) to the client. Now, the client will iterate through the result array and print it out. The first thing we noted while developing this application was that there's no method to write an array of integers to a socket. So our first step was to write a new class with a method capable of writing an array of integers to a socket. Also, no method was available for reading an array of integers from a socket. So our new class, ArrayIO, has two methods--one for reading an array of integers from a socket and one for writing an array of integers to a socket. We display the complete code in Listing A.

Listing A: Our complete math code


import java.io.*;

class ArrayIO {
  public ArrayIO() {}

/**
 * write an array of integers to a 
	socket
 */ 
  public void 
  writeArray(DataOutputStream out, 
	int arr[]) 
    throws Exception {
      for (int i=0; i<arr.
	length; i++) {
        out.write(arr[i]);
      }
    }

/**
 * read an array of integers
	 from a socket
 */
  public int[] readArray
	(BufferedReader br) 
    throws Exception {
      int c[] = new int[10];
      for (int h=0; h<10; h++) {
        try {  
          c[h] = (int) br.read();
        } 
        catch(IOException il) {
        }
 	}
      return c;
   }
}    

Both the client and the server will use the ArrayIO class for reading and writing arrays of integers to sockets. Once a client sends two arrays of integers to the server, the server will read them and call a method that sums these two arrays. In order to do this, we wrote a simple class, ArrayMath, to sum two arrays of integers. We show the source code for the ArrayMath class in Listing B. Since we have the classes ArrayIO and ArrayMath, we can develop our Client/Server application now.

Listing B: Our method to sum two arrays


import java.io.*;

class ArrayMath {
  public ArrayMath() {}
   
/** simple method to add two 
 * arrays ofintegers 
 */
  public int[] addArray
	(int a[], int b[]) {
    int result[] = new int[10];
    for (int s=0; s<result.
	length; s++) {
      result[s] = a[s] + b[s];   
    }
    return result;
  }
}
The math client The code for the client is really simple. All it does is open a socket--an input stream and an output stream. Once this is done, the client uses the method writeArray() from the ArrayIO class to send two arrays of integers to the server. Then the client waits for the server to return the result array. Once the client receives the result array, it iterates through the array and prints each element in the array. After that, it closes the IO streams and the socket. You can see the source code for the math client in Listing C.

Listing C: Our math client code


import java.io.*;
import java.net.*;

public class client {    
  public final static 
	int REMOTE_PORT = 3333;
  static int a[] = 
    {1, 2, 3, 4, 5, 6, 7,
	 8, 9, 10};
  static int b[] = 
    {11, 12, 13, 14, 15, 
	16, 17, 18, 19, 20};

  public static void main
	(String argv[]) 
    throws Exception {
	Socket cl = null, cl2=null;
	BufferedReader is = null;
	DataOutputStream os = null;
     ArrayIO aio = new ArrayIO();

     // Open connection to the 
	compute 
     // engine on port 5555	
	try {
	  cl = new Socket
	("leo",REMOTE_PORT);
       is = new BufferedReader(new
         InputStreamReader(
           cl.getInputStream()));
	  os = new 
         DataOutputStream(
           cl.getOutputStream());
	} 
     catch
	(UnknownHostException e1) {
	  System.out.println
	("Unknown Host: "
         +e1);
	} 
     catch (IOException e2) {
	  System.out.println
	("Erorr io: "+e2);
	}
	try {	    
	  aio.writeArray(os, a);
       aio.writeArray(os, b);
	} 
     catch (IOException ex) {
	  System.out.println(
         "error writing to 
	server..."+ex);
	}	
	// receive results from 
	the math server
	int result[] = new int[10];
	try {
       result = aio.readArray(is);
	} 
     catch(Exception e) {
	  e.printStackTrace();
	}
	System.out.println
	("The sum of the 
	two arrays is: ");
		for (int j=0; 
			j<result.
	length; j++) {
	  System.out.print
	(result[j]+" ");
	}
     System.out.println("");
	// close input stream,
	 output 
     // stream and connection
	try {
       is.close();
       os.close();
	  cl.close();
	} 
     catch (IOException x) {
	  System.out.println(
         "Error writing...."+x);
	}
  }
}
The math server Programming the math server isn't as bad as you might expect. However, we want the server to be able to serve simultaneous clients' requests--in other words, it must be a multi-thread server. Programming with threads in Java is easy. You just need to subclass the Thread class and implement the run() method, and that's where all the actions that the server will perform actually go. However, note that the thread won't start executing until you call the start() method, which in turn, calls the run() method. If you look at the server code shown in Listing D, you'll notice that all the actions are being performed in the run() method of the Connection class. In the run() method, we actually read two arrays from the client socket and add them before sending the result array back to the client.

Listing D: Our math server code


import java.io.*;
import java.net.*;
import java.util.*;

public class server extends 
	Thread {
  // The port number on which 
	the server 
  // will be listening on
  public static final int 
	HTTP_PORT = 3333;
  protected ServerSocket listen;

  // constructor.
  public server() {
    try {
      listen = new ServerSocket
	(HTTP_PORT);
    } 
    catch(IOException ex) {
      System.out.println
	("Exception..."+ex);
    }
    this.start();
  }
  // multi-threading -- create a 
  // new connection for 
	each request
  public void run() {
    try {
      while(true) {
        Socket client = listen.
	accept();
        Connects cc = new Connects
	(client);
      }
    } 
    catch(IOException e) {
      System.out.println
	("Exception..."+e);
    }
  }

  // main program
  public static void main
	(String argv[]) 
    throws IOException {
      new server();
  }
}

class Connects extends Thread {
  Socket client;
  BufferedReader is;
  DataOutputStream os;
  ArrayIO aio = new ArrayIO();
  ArrayMath am = new ArrayMath();

  public Connects(Socket s) 
	{ // constructor
    client = s;
    try {
      is = new BufferedReader(new
        InputStreamReader(
          client.get
	InputStream()));
      os = new DataOutputStream(
        client.getOutputStream());
    } 
    catch (IOException e) {
      try {
        client.close();
      } 
      catch (IOException ex) {
        System.out.println(
        "Error getting socket 
	streams.."+ex);
      }
      return;
    }
    this.start(); // 
	Thread starts here...          
                  //
	 start() will call run()
  }
 
  public void run() {
    int a1[] = new int[10];
    int a2[] = new int[10];
    try {
      a1 = aio.readArray(is);
	 a2 = aio.readArray(is);
    } 
    catch(Exception ioe) {}
    int r[] = new int[10];
    r = am.addArray(a1, a2);
    try {
      aio.writeArray(os, r);
    } 
    catch(Exception e) {
      e.printStackTrace();
    }
  }
}
Where to go from here If you've read this far, then you're really interested in developing distributed applications in Java. If you've never programmed sockets before, try reading and running the code in this article. Then, experiment with it. To make it your own, add new functionality to the code (for example, the ability to subtract, multiply and divide arrays). Good luck!

Qusay H. Mahmoud is a senior software engineer at The School of Computer Science at Carleton University, Ottawa, Canada. Before joining Carleton University, he worked as a software designer for Newbridge Networks. Qusay holds a B.Sc. in data analysis and a Masters degree in computer science, both from the University of New Brunswick, Canada. You may reach Qusay at: dejavu@acm.org.