Recent top five:
Let's talk about exceptions ...
How do you handle exceptions? Do you think upfront about the type of exceptions that you want to catch or do you just let
the outside world handle it?
-- Jeroen van Bergen in JW Blogs
| Enterprise AJAX - Transcend the Hype |
| Memory Analysis in Eclipse |
| Oracle Compatibility Developer's Guide |
| Memory Analysis in Eclipse |
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.
(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.)
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.
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 ); }
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();
}
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 ) );
}
}
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/".