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.