by Rich Kadel

Generic client-server classes

news
Sep 1, 199616 mins
JavaJava SE

Develop your own applet-to-server protocols by subclassing from a simple client-server package

The Java API provides a nice set of libraries for initiating and communicating over TCP/IP sockets. I know a number of Unix programmers (myself included) that had, at one time or another, resorted to writing C or C++ wrappers over the existing BSD sockets API. The standard Unix API provides a very low-level interface, and requires a significant amount of error checking and handling. It is also very architecture-specific. Java, with its java.net package, has removed all of the portability issues, and most of the tedium of network I/O.

However, there is room for improvement. To develop a Java-based server application that listens for client connections and spawns off threads to handle them, a number of steps must be taken. In the following paragraphs, we will go over each of these steps, and will touch on several useful techniques including the use of Sockets, Threads, and basic Java data I/O. The result will be a package that can be used to develop almost any client-server protocol by subclassing the classes in the package. These classes can then be integrated into an applet, or a standalone application.

The architecture

A typical client-server architecture consists of a server application running on one machine, and one or more clients โ€” often, but not always, operating on different machines across a network. The server is modeled as a perpetual process that waits for clients to connect (request a service), and then processes the client requests. Java threads are ideal to the implementation of a server application, because a new thread can be started for each connecting client. This prevents one client request from holding up others if and when it enters a wait state, or some time-consuming task.

The example โ€“ RemoteFileInputStream

For the purposes of this article, we are going to be developing a client-server protocol to allow a client to retrieve a file from the server machine. We will use a familiar API on the client side by mimicking the

java.io.FileInputStream

class in creating the new class RemoteFileInputStream. The constructor will take the hostname of the server, the TCP/IP port number of the service, and the filename to retrieve. The user can then use the methods defined in

java.io.InputStream

to read the data from the file.

(Note: Java actually provides a very simple way to read a file from the server, as long as the file is in under the HTTP document root. Ordinarily you would want to use the openStream() method in java.net.URL. The RemoteFileInputStream example still provides a good example of client-server communication, and it can be molded to support just about any other protocol.)

The API

Now that we have a good idea about what we are trying to model, we need an API. On the server side, it should start out as simple as:

    new RemoteFileInputServer( portnum );

This could be the only line in the static main() function of a standalone Java application. As we will see later, we will create a subclass of a new class called NetPortToClient, which is a subclass of java.lang.Thread, to perform the server-side portion of the protocol.

The client side will use the constructor for a RemoteFileInputStream class via:

    InputStream stream = new RemoteFileInputStream( hostname, portnum, filename );

This constructor will, in turn, call something like:

    NetPortToServer port = new NetPortToServer( hostname, portnum );

to initiate the connection to the server. From there, the RemoteFileInputStream class will use the port object to communicate with its assigned thread in the server application.

The server

The serverโ€™s most unique responsibility is that it must listen for and accept new clients. We embody this responsibility in the abstract class NetServer. NetServerโ€™s constructor requires only one argument: the port number to which clients will attempt connections.

public abstract class NetServer extends Thread { private int portnum;

public NetServer ( int portnum ) { this.portnum = portnum; start (); }

NetServer is implemented as a subclass of Thread. This way, you can create a server object, and then go off and do other things while the server waits for and accepts clients. The constructor calls start(), which begins the thread of execution, and in turn calls run(), the main body of NetServer. Within run(), an instance of java.net.ServerSocket is constructed, using the port number we saved from the NetPort constructor. Then we begin an infinite loop to wait for and accept client connections.

public void run () { ServerSocket server_socket = null; try { server_socket = new ServerSocket( portnum );

while ( true ) { createClientPort ( server_socket.accept () ); } }

The thread will hang in the accept() function until a client connects, but thatโ€™s why we made NetServer a thread. The NetServer thread has no other responsibilities. If you want to do something else, you will need to do it in another thread (which, by the way, could be the thread from which the NetServer constructor was called).

We will, of course, need to handle any potential IOExceptions. (Anyone who has used the java.io or java.net packages should be familiar with this, since almost every method in these packages throws an IOException.) Finally, we should add a finally clause to the try block. This ensures that the close method will be called for the server_socket. Without it, some implementations may not release all system resources, which can cause some operating systems (such as Windows 95) to crash or hang.

        catch ( IOException ioerror ) {
            System.err.println ( "IO Error opening server socket" );
        }
        finally {
            if ( server_socket != null ) {
                try {
                    server_socket.close();
                }
                catch ( IOException ioe ) {
                }
            }
        }
    }

The loop in the run() method called a member function createClientPort(). This is an abstract method and must be implemented in the protocol-specific subclass of NetServer. (We will be doing this later.) The expectation here is that a new thread will be started through which the server and client will communicate, using the given client socket.

    protected abstract void createClientPort( Socket socket );
}

The NetPort superclass

Once the client and server are connected, both sides perform almost identical tasks. Each must send and/or receive data, over a network port, based on a predetermined protocol. We can implement a common class, NetPort, that simplifies the task of reading and writing data over a socket.

public class NetPort
    extends Thread
    implements DataInput, DataOutput

NetPort is a subclass of java.lang.Thread. This does not mandate that a NetPort object run as a thread, but it simplifies the task of starting it up as one. It also implements java.io.DataInput and java.io.DataOutput interfaces. This makes using a NetPort object very natural. It allows expressions such as: port.writeInt( 2000 ) and String str = port.readUTF().

The constructor for a NetPort is passed a live java.net.Socket object. On the server side, this socket came from the server_socket.accept() method call. (Client-side socket creation is shown later.) Input and output streams are obtained from the socket, and all three are saved in member variables of the NetPort.

{ private Socket socket; private DataOutputStream outstream; private DataInputStream instream;

protected NetPort( Socket socket ) throws IOException { this.socket = socket;

instream = new DataInputStream( socket.getInputStream() ); outstream = new DataOutputStream( socket.getOutputStream() ); }

We will provide access to the input and output streams directly through โ€œgetโ€ functions, but these will rarely be necessary since most input and output functions can be accessed directly through the NetPort objectโ€™s implementation of the DataInput and DataOutput Interfaces.

public DataInputStream getInputStream () { return ( instream ); }

public DataOutputStream getOutputStream () { return ( outstream ); }

As with the finally clause in the NetServer run() method, we need to be sure we release all system resources once we are done with them. We can use the finalize() method defined in java.lang.Object. This method will eventually be called before the NetPort object is removed from memory, during the process exit or garbage collection. The method will call the close() method, which will close the input and output streams, and then the socket itself.

public void close() { try { instream.close(); outstream.close(); socket.close(); } catch( IOException ioerror ) { System.err.prinln( "Error closing input stream." ); } }

protected void finalize() { close(); }

Since the NetPort is a subclass of Thread, we can start the thread by calling start() on the NetPort object. This spawns the thread and invokes the required run() method. You can override this method in a subclass of NetPort if you like, but the most likely reason for creating the thread is to avoid hanging up other processing while you wait for input. For this reason, the NetPort run() method is already designed to start an infinite loop to read input. Since there is no way to know what it is supposed to read, it calls a method you should override called readInput().

public void run () {

while ( true ) { try { readInput (); } catch ( IOException ioerror ) { System.err.println( "Error reading input stream." ); break; } } }

Finally, all of the methods declared in the java.io.DataInput and java.io.DataOutput interfaces must be defined. For this, we simply delegate the call to the NetPortโ€™s input stream or output stream, as appropriate. Additionally, we should define the flush() method, required for the DataOutputStream class but not defined by the DataOutput interface. Without it, there is no guarantee that data written to the port will actually go out right away, due to internal buffering. Here are a few example function implementations:

public void write( int byteval ) throws IOException { outstream.write( byteval ); }

public void readFully(byte b[]) throws IOException { instream.readFully( b ); }

public void flush() throws IOException { outstream.flush(); }

Subclassing NetPort for the server and client

The API of the NetPort varies slightly between the server and client. We can characterize these differences in two subclasses of NetPort: NetPortToClient on the server side, and NetPortToServer on the client side. Since the socket for the serverโ€™s port to the client has already been obtained from the

accept()

method, we pass it directly into the constructor. On the client-side, however, the socket is not already created, so we do this in the NetPortToServer subclass constructors. These constructors are passed the machine location (hostname or Internet address) and port number.

public class NetPortToClient extends NetPort { public NetPortToClient( NetServer server, Socket socket ) throws IOException { super ( socket ); // might also save a reference to the server if needed } }

public class NetPortToServer extends NetPort { public NetPortToServer ( String host, int port ) throws IOException { super ( new Socket ( host, port ) ); }

public NetPortToServer ( InetAddress address, int port ) throws IOException { super ( new Socket ( address, port ) ); } }

The new net package

Believe it or not, all of the generic classes have now been defined. All thatโ€™s left to do is to define the protocol-specific functionality. Before we do that, we should place these new utility classes in a common package using the recommended naming guidelines (that is, using your domain name, backwards, capitalizing the first word). Appropriately enough, I will name the subpackage โ€œnetโ€, so for my companyโ€™s domain,

dtai.com

, the package should be called COM.dtai.net.

This package should be specified, by adding โ€œpackage COM.dtai.net;โ€ at the top of each of the four generic class files we created: NetServer.java, NetPort.java, NetPortToClient.java, and NetPortToServer.java. If you do not already have a directory for your own generic class files, you need to create one; for example, โ€œโ€ฆ;/homedir/classesโ€, and add it to your CLASSPATH. The new classes (and usually their corresponding .java source files) should be placed in subdirectories that correspond exactly to the package name. According to this example, they would be placed in โ€œโ€ฆ/homedir/classes/COM/dtai/net/โ€.

Implementing the RemoteFileInputServer

The RemoteFileInputServer is a subclass of NetServer. For simplicity, we will add a

static main()

function so that we can run RemoteFileInputServer as a standalone application, usually by executing โ€œjava RemoteFileInputServerโ€. The constructor requires the server-side port number to which to listen for client connections. We will use a somewhat random port number of 9005. Last, we need to override the abstract method

createClientPort()

. Given the clientโ€™s socket connection, we need to create a handler for it. The obvious choice is a NetPortToClient object, which we subclass in our private class ClientPort.

public class RemoteFileInputServer extends NetServer { public static void main ( String argv[] ) { new RemoteFileInputServer ( 9005 ); }

RemoteFileInputServer( int portnum ) { super ( portnum ); }

protected void createClientPort( Socket socket ) { try { new ClientPort ( this, socket ); } catch( IOException ioe ) { } } }

ClientPort overrides readInput(), which is designed to attempt to read a filename (in the form of a standard Unicode UTF string) from the client, or hang there until one is available. If the client disconnects, an IOException is thrown and caught, and the thread is stopped. When a String is read, we call the separate function process() to process the clientโ€™s request.

class ClientPort extends NetPortToClient { public ClientPort( RemoteFileInputServer server, Socket socket ) throws IOException { super( server, socket ); start(); // as a thread }

protected void readInput() throws IOException {

try { String filename = readUTF(); process ( filename.trim() ); } catch ( IOException ioe ) { System.err.println ( "Error reading filename from the client.nClient probably disconnected" ); stop(); } }

The process() method tries to open the filename request by the client. This would be a file local to the server application, of course. If it is successful, the ClientPort writes to the client the total number of bytes in the file, and then it reads and transmits the entire file, byte by byte. If an IOException occurs, it writes a -1. (Most likely the IOException would happen while opening the file, so the -1 would be read by the client in place of the file size, indicating to the client that an error has occurred.)

There is a very large security hole here! If you run this application on your system, you are giving the world the ability to read any file on your system that the processโ€™s effective user id can read (possibly including password files)! In a full implementation, you would want to do something like restrict the directories from which a file can be requested.

    private synchronized void process( String filename ) {
        try {
            InputStream file = new BufferedInputStream(
                                        new FileInputStream( filename ) );
            writeInt( file.available() );
            int byteread;
            while ( ( byteread = file.read() ) >= 0 ) {
                writeByte( byteread );
            }
            flush();
        }
        catch ( IOException ioe ) {
            System.err.println ( "Error reading file "" + filename + "" for the client." );
            writeInt( -1 );
            flush();
        }
    }
}

Note that we made process() a synchronized method. This pretty much guarantees that the request will be processed completely before anything else can happen to the object from another thread. There is not too much to worry about in this simple application, but with a little more complexity, your server might support inter-ClientPort interaction, and then this guarantee becomes much more important.

You should not make readInput synchronized because it has a strong likelihood of entering a prolonged wait state (in the read function). If it were synchronized and another thread tried to execute another synchronized method on the object, that other thread would be held up as well, waiting for an unrelated client to send data to free up the read! Thatโ€™s why we created the separate synchronized method process().

RemoteFileInputStream extends InputStream

On the client side, we are going to imbed a NetPortToServer object, and the client-side of the protocol, in a new class RemoteFileInputStream. If we extend

java.io.InputStream

(just as

java.io.FileInputStream

), and override all of the methods to implement them using the NetPort, the RemoteFileInputStream can be used anywhere an InputStream is expected.

The constructor for RemoteFileInputStream takes a filename, just like a FileInputStream would, but it also takes a hostname and port number. (Remember, if you are running this from within an applet, you probably will be restricted from connecting to any host other than the host from which the applet was loaded.) The connection to the server is established by creating an instance of NetPortToServer, and the filename is transmitted. The constructor immediately reads one integer from the port, and throws an IOException to the caller if the server returns -1. Otherwise, RemoteFileInputStream uses the returned number of bytes in the file, and the file data that should now be available on the port, to implement the InputStream methods. (Only a couple of these methods are shown below.)

public class RemoteFileInputStream extends InputStream { NetPortToServer port; DataInputStream is; int bytesavail;

RemoteFileInputStream( String host, int portnum, String filename ) throws IOException { port = new NetPortToServer( host, portnum ); is = port.getInputStream(); port.writeUTF( filename ); port.flush(); bytesavail = port.readInt(); if ( bytesavail < 0 ) { throw new IOException(); } }

public int available() throws IOException { return bytesavail; }

public int read() throws IOException { bytesavail--; if ( bytesavail 0 ) { return -1; } return is.read(); }

Testing the RemoteFileInputStream

To test the RemoteFileInputStream, we need a client. A simple applet client might read a remote file, and show the contents in a TextArea as follows. This application assumes that the file returned is a text file. Keep in mind that the RemoteFileInputStream has no such restriction.

public class RemoteFileTest extents Applet { public void init () {

TextArea text = new TextArea( 24, 80 ); add( text );

try { InputStream file = new RemoteFileInputStream( getCodeBase().getHost(), 9005, "RemoteFileInputServer.java" ); // file to read

int available = file.available(); int bytes_to_read = available; byte bytes[] = new byte[available]; while ( false || bytes_to_read > 0 ) { bytes_to_read -= file.read( bytes, ( available - bytes_to_read ), bytes_to_read ); } text.setText( new String( bytes, 0 ) ); } catch ( Exception e ) { text.setText( "Error reading file "+filename+" for input." ); } } }

Conclusion

If you have already started developing a Java class library for company or corporate use, these client-server tools might be a useful addition to your arsenal. If you are just getting started, you may want to implement the client-server package as a jumping-off point to developing and packaging other useful classes you generate from time to time.

Java applet programming is somewhat restrictive. Most browser-based SecurityManagers will restrict you from reading and writing information on the client. Client-server computing is a useful tool for overcoming these data storage restrictions. But even if you never need client-server functionality, you should be able to use this package as an example in the use of Threads, Sockets, and Java I/O.

Rich Kadel is a Senior Software Engineer and the lead engineer in charge of Java development at DTAI Incorporated of San Diego, California. He was the primary developer of PAL++, a large C++ software library with almost 300 C++ classes for building data- and graphically-oriented applications. DTAI Inc. provides software products and services including Internet development, training, and programming of Java, C++, client/server architectures, RDBMS, and X11/Motif applications for Unix and Microsoft Windows platforms.