Generic client-server classes

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

1 2 Page 1
Page 1 of 2