When developing Java network applications in a stable and controlled environment, it's easy to become complacent and ignore the possibility of network timeouts. After all, with the perfect client and server running over a local area network, timeouts won't occur to stall your application. But when your users run clients or servers over the Internet (an environment where networks can go down, badly written software can stall or communication sessions can deviate from the ideal path of a communications protocol), timeouts can cause problems if there isn't a mechanism to recognize and deal with it.
In this article, I'll present two approaches to dealing with network timeouts. The first approach, writing a multi-threaded application, is backwards compatible with JDK1.02, at the expense of increased complexity. The second approach is by far the easiest. In most cases it involves adding only a few lines of code to your application but requires a JDK1.1 virtual machine. As an example, I'll show how the two approaches can be applied to a simple finger client.
Multi-threaded Applications
Designing a multi-threaded client or server offers a solution to the problem of timeouts. When one thread becomes blocked, waiting for network input or to send more data, a second timer thread can still perform other operations when a timeout occurs, such as terminating the connection. If you've never written a multi-threaded application before, consult the sidebar "Overview of Threads."
Listing 1 introduces the Timer class which is a subclass of java.lang.Thread. When an application creates an instance of Timer, it specifies the number of milliseconds that can elapse before a network timeout occurs. Once the timer is started, it will begin to decrement an internal counter. When the counter has no more milliseconds remaining, the timeout action occurs which, by default, is to exit the application. If necessary, subclasses of Timer can override the timeout method to provide custom functionality.
For the sample finger client, we will use the Timer class to terminate the application if a network timeout occurs (for example, a server that stalls). Listing 2 shows a multi-threaded finger client and Listing 3 shows a sample finger server. The client and server will communicate via port 79 (defined in the public int FINGER_PORT). However, on some systems the server may be unable to bind to this port, in which case an ephemeral port in the range 1024 to 5000 should be used instead for both client and server.
The finger server reads data from files matching the format 'user-name.info' where name is a valid username. For testing purposes, you can create a single user file and have the finger client request information. To simulate timeouts, the server maintains a counter and stalls after the first line of input on every second connection.
The finger client starts by checking its command line arguments and creating a new instance of itself. The client then opens a socket connection and gets input and output streams. It sends the name of a user to the finger server and then begins reading in data.
At some point in the connection, our finger server will stop sending data and our client will be stalled while it waits for input. Without some means of reconciliation, our client would remain blocked - this is where our Timer class solves the problem.
It is the responsibility of the application to repeatedly reset the timer to prevent a "false" timeout from occurring. While it is looping, the timer is reset; if the loop stops for some reason, the timer can continue uninterrupted and generate a timeout. When this occurs, the program can terminate rather than being locked.
If we wanted our timer to have different properties (for example, re-establishing a connection), we could simply extend the timer to create a custom timer class and to override the timeout method. Were we to write a server that supported timeouts, we would need a different timer thread for every single connection (which could cause considerable overheads if a large number of concurrent connections were generated). A much more efficient method is to use the new socket features introduced in JDK1.1, as we'll see in the next section.
Timeouts Made Easy
While the multi-threaded approach works well and is backwards compatible with JDK1.02, it does introduce a great deal of complexity, even for simple applications such as a finger client. If it is important to run your application on older virtual machines, use multi-threading. However, if you have the luxury of a JDK1.1 platform, you can add timeout code to your application with ease.
As part of the new features of JDK1.1, support for socket options have been introduced. One of these allows developers to specify the number of milliseconds that must elapse before a timeout occurs, throwing a java.io.InterruptedIOException. For Socket connections, this means that operations reading from the socket input stream can timeout if no data is received within a given period of time. With only a few lines of code, we can introduce support for timeouts, without moving to a multi-threaded design in our clients and without launching a Timer thread for our servers.
Simple Finger Client
Listing 4 shows a simple finger client which can be run against the server from Listing 3. You'll notice how simple it is to set a timeout value. The setSoTimeout method accepts an integer value as a parameter, representing the number of milliseconds for a timeout.
// Set SO_TIMEOUT for five seconds
socket.setSoTimeout ( 5000 );
All you need to do now is catch the java.io.InterruptedIOException that is thrown when a timeout occurs while reading from a Socket's input stream and your application will handle timeouts.
Timeouts in Servers
Timeouts can occur in servers as well. If you're writing a multi-threaded server, it's important to set and detect timeouts in the socket connections to clients. Otherwise, badly behaved clients can use up memory by stalling and leaving threads open, or use up all your available connections if your server has a limit to the number of connections it supports. A good design would be to call the setSoTimeout method on every socket you accept and to catch InterruptedIOExceptions if thrown.
The most important case for using the setSoTimeout method is when your server uses DatagramSockets. DatagramSockets allow Java applications and applets to communicate via UDP, which doesn't offer guaranteed packet delivery or ordering. It's critical that applications using UDP recognize and provide for situations in which timeouts occur. Previously, this meant creating a separate thread to monitor packet arrivals (and often re-sending packets when no response was issued). Now, however, your application can easily cater to this situation with a few lines of code using the setSoTimeout method.
To illustrate this, I've written an example program that implements a stop-and-wait delivery mechanism. One application (the sender) sends small packets of data containing a sequence number and then waits for an acknowledgement packet to be sent by the receiver. If no acknowledgement arrives, then one of two situations could have occurred:
- The original packet never arrived at the receiver's end.
- The original packet arrived and was acknowledged, but the acknowledgement never arrived at the sender.
Listing 5 shows the code for the sender and Listing 6 shows the code for the receiver. The sender uses a DatagramSocket to send packets, and receive acknowledgements. Likewise, the receiver uses a DatagramSocket to receive packets and send acknowledgements. Both sender and receiver should check for timeouts and respond accordingly.
Timeouts can occur for a variety of reasons; there may be times when the sender is forced to send more than one instance of a data packet, or when the receiver sends more than one acknowledgement. It is important in this situation for both sender and receiver to check the sequence number and handle inappropriate packets gracefully. It is even more important when your application sends many packets and expects the receiver to maintain sequence integrity.
Figure 1: Lost packet causes timeout
Conclusion
The decision to use a timer thread, or to use socket options instead, is largely up to the individual circumstances of the application. Some applications will require backwards compatibility with JDK1.02, in which case timers may be the only option. Wherever possible though, due consideration should be given to setting socket options for timeouts.
Setting socket options for timeouts and catching InterruptedIOExceptions allows developers to place code close to the point at which input and output takes place. This reduces complexity and saves placing control of your connections in an external class. For clients that are not already multi-threaded, setting a socket option timeout is by far the better choice and with only a few lines your application can support network timeouts.
About the Author
David Reilly has worked on network protocols and Web-related programming at Bond University,
Australia. Since his conversion to Java in 1996, he has worked almost exclusively with the language, finding it both a joy to use and the most productive way to produce portable applications. David can be contacted at java@davidreilly.com
Listing 1.
// Timer - thread which counts down to a network timeout,
// which prompts a call to timeout() method
//
// Written by : David Reilly
// Last modification date : March 8, 1998
//
//
public class Timer extends Thread
{
// Public consts
public static final int REFRESH_RATE = 100;
// Private member variables
private int m_timeoutValue; // Length of timeout
private int m_remainingTime; // Remaining time
private boolean m_timeReset; // time reset
// Constructor
public Timer ( int milliseconds )
{
// Assign to member variable
m_timeoutValue = milliseconds;
m_remainingTime = milliseconds;
m_timeReset = false;
}
public synchronized void resetTimer()
{
m_remainingTime = m_timeoutValue;
}
// Thread run method
public void run()
{
// Repeat forever, until program terminates
for (;;)
{
try
{
// Sleep for the default time refresh rate
Thread.sleep(REFRESH_RATE);
// Check to see if timer was reset while sleeping
if (m_timeReset == true)
{
// Timer has been reset, so clear our flag
m_timeReset = false;
}
else
{
// Decrease remaining time
m_remainingTime -= REFRESH_RATE;
// Has a timeout occured
if (m_remainingTime < 0)
{
// Trigger a timeout
timeout();
}
}
}
catch (InterruptedException interrupted)
{
// Disregard - if we are interupted, then
// we will simply sleep on the next loop
}
}
}
// Override this to provide custom functionality
public void timeout()
{
System.err.println ("Network timeout occured.... terminating");
System.exit(1);
}
}
Listing 2.
import java.net.*;
import java.io.*;
// MultiThreadedFingerClient
//
// Written by : David Reilly
// Last modification date : March 1, 1998
//
// Syntax : hostname username
//
//
public class MultiThreadedFingerClient extends Thread
{
// Public constants
public static final int FINGER_PORT = 79;
// may need to set this to an ephemeral port
// Private member variables
private String m_hostname;
private String m_username;
private Timer m_timer;
// Constructor :
public MultiThreadedFingerClient(String hostname, String username)
{
// Assign to member variables
m_hostname = hostname;
m_username = username;
// Five seconds timeout
m_timer = new Timer ( 5000 );
}
// MAIN :
public static void main(String args[])
{
if (args.length != 2)
{
System.out.println ("MultiThreadedFingerClient hostname user");
System.exit(1);
}
// Create a new
new MultiThreadedFingerClient(args[0], args[1]).start();
}
// connect to server : thread run method
public void run()
{
// Local variables
Socket socket = null;
int data;
// Prompt user
System.out.println ("Connecting to " + m_hostname);
// Start the timer
m_timer.start();
try
{
// Create a socket to the server
socket = new Socket ( m_hostname , FINGER_PORT );
// Only proceed if connection was established
if (socket == null)
return;
// Get input and output streams to socket
BufferedInputStream bufin = new BufferedInputStream
( socket.getInputStream() );
PrintStream pout = new PrintStream( socket.getOutputStream() );
// deprecated in JDK1.1
// Print user name
pout.println (m_username);
// Read in information from server
for (;;)
{
// Read a byte of data
data = bufin.read();
// Check for end of input stream
if (data != -1)
{
// Cast data to a character and display it
System.out.print ( (char) data );
}
else
break; // no more data to be read
// Reset our timer
m_timer.resetTimer();
}
}
catch (IOException e)
{
System.err.println ("I/O - " + e);
}
}
}
Listing 3.
import java.net.*;
import java.io.*;
// FingerTimeoutServer
//
// Written by : David Reilly
// Last modification date : March 8, 1998
//
//
public class FingerTimeoutServer extends Thread
{
public static final int FINGER_PORT = 79;
// may need to set this to an ephemeral port
// Private member vars
private Socket m_socket;
// Instance count
private static int instanceCount = 0;
public FingerTimeoutServer ( Socket sock )
{
// Assign to member variable
m_socket = sock;
// Increment instanceCount
instanceCount++;
}
public static void main( String args[] )
{
System.out.println ("Finger Server with simulated timeout");
try
{
// Create a server socket, bound to FINGER_PORT
ServerSocket serverSocket = new ServerSocket ( FINGER_PORT );
// Loop forever, accepting new connecitons
for (;;)
{
Socket sock = serverSocket.accept();
// Create an instance of server to process socket
FingerTimeoutServer connection = new FingerTimeoutServer ( sock );
// Start connection thread
connection.start();
}
}
catch (IOException ie)
{
System.err.println ("I/O Error - " + ie);
System.exit(0);
}
}
// Main 'work' method
public void run()
{
try
{
DataInputStream din = new DataInputStream ( m_socket.getInputStream() );
PrintStream pout = new PrintStream ( m_socket.getOutputStream() );
// deprecated in JDK1.1
// Read a single line of text
String username = din.readLine();
// Check for presence of user-name.info
File userFile = new File ("user-" + username + ".info");
// Check if user exists
if (userFile.exists())
{
FileInputStream fin = new FileInputStream( userFile );
DataInputStream dfin = new DataInputStream( fin );
// Read first line, and send to client
String line = dfin.readLine();
pout.println (line);
// Check instance count (if not even, then send data and terminate)
if (instanceCount % 2 != 0)
{
for (;;)
{
line = dfin.readLine();
if (line != null)
pout.println (line);
else
break;
}
// Close socket
m_socket.close();
}
else
{
// Simulate timeout by not closing socket
pout.println ("Simulating timeout....
if your client doesn't handle timeouts it will stall");
}
}
else
{
pout.println ("Invalid username");
// Close socket
m_socket.close();
}
}
catch (IOException ie)
{
System.out.println ("I/O Error - " + ie);
}
}
}
Listing 4.
import java.net.*;
import java.io.*;
// FingerClient - demonstrates new JDK1.1 networking features
//
// Written by : David Reilly
// Last modification date : March 1, 1998
//
// Syntax : hostname username
//
//
public class FingerClient
{
// Public constants
public static final int FINGER_PORT = 79;
// may need to change to an ephemeral port
// Private member variables
private String m_hostname;
private String m_username;
// Constructor :
public FingerClient(String hostname, String username)
{
// Assign to member variables
m_hostname = hostname;
m_username = username;
// Make a connection
connect();
}
// MAIN :
public static void main(String args[])
{
if (args.length != 2)
{
System.out.println ("FingerClient hostname user");
System.exit(1);
}
new FingerClient(args[0], args[1]);
}
// connect to server :
private void connect()
{
// Local vars
Socket socket = null;
int data;
// Prompt user
System.out.println ("Connecting to " + m_hostname);
try
{
// Create a socket to the server
socket = new Socket ( m_hostname , FINGER_PORT );
// Only proceed if connection was established
if (socket == null)
return;
// Set SO_TIMEOUT for five seconds
socket.setSoTimeout ( 5000 );
// Get input and output streams to socket
BufferedInputStream bufin = new BufferedInputStream
( socket.getInputStream() );
PrintStream pout = new PrintStream( socket.getOutputStream() );
// deprecated in JDK1.1
// Print user name
pout.println (m_username);
// Read in information from server
for (;;)
{
// Read a byte of data
data = bufin.read();
// Check for end of input stream
if (data != -1)
{
// Cast data to a character and display it
System.out.print ( (char) data );
}
else
break; // no more data to be read
}
}
catch (IOException e)
{
System.err.println ("I/O - " + e);
}
}
}
Listing 5.
import java.net.*;
import java.io.*;
import java.util.Random;
// Sender - demonstrates timeouts on a stop-and-wait protocol
//
// Written by : David Reilly
// Last modification date : March 1, 1998
//
// Syntax : hostname port
//
//
public class Sender
{
// Priavet member variables
// Internet address for receiver
private InetAddress m_addr;
// Receiver port
private int m_port;
// Socket to receiver
private DatagramSocket m_datagramSocket = null;
// Psuedo-random number generator for random sleep interval
private Random random;
// Default packet size
public static final int packetSize = 20;
// Constructor for Sender
public Sender(String hostname, String port)
{
// Create a psuedo-random number generator
random = new Random();
// Obtain an InetAddress object for hostname
try
{
m_addr = InetAddress.getByName ( hostname );
}
catch (UnknownHostException unknownHost)
{
System.out.println ("Unknown hostname : " + hostname);
}
// Convert string into an int
try
{
// Create an integer object and get it's int value
m_port = new Integer(port).intValue();
}
catch (Exception e)
{
System.out.println ("Invalid port number : " + port);
System.exit(1);
}
}
// MAIN method
public static void main( String args[] )
{
if (args.length != 2)
{
System.out.println ("Sender hostname port");
System.exit(1);
}
// Create an instance of sender
Sender sender = new Sender(args[0], args[1]);
// Send packets to a receiver
sender.transmit();
}
// Transmit packets to a receiver
public void transmit()
{
// Sequence number
int sequenceNumber =0;
// Create a datagram socket
try
{
m_datagramSocket = new DatagramSocket();
// Set the timeout to two seconds
m_datagramSocket.setSoTimeout( 2000 );
}
catch (SocketException se)
{
System.err.println ("Error creating a datagram socket - " + se);
System.exit(1);
}
for (;;)
{
// Packets for receiving a response
DatagramPacket response = new DatagramPacket(new byte[packetSize], packetSize);
try
{
// Read a packet, and check if its an acknowledgement
m_datagramSocket.receive(response);
int responseNumber = getSequenceNumber(response);
// Does the response ACK match our sequence
if (responseNumber == sequenceNumber)
{
System.out.println ("ACK : " + sequenceNumber);
// Increment sequence number
sequenceNumber++;
System.out.println ("Sending packet " + sequenceNumber);
// Send next packet to receiver
sendPacket(sequenceNumber, m_addr, m_port);
}
else
System.out.println ("ACK : " + responseNumber
+ " (expecting" + sequenceNumber + ")");
// Sleep for a random length of time... so some timeouts are generated
randomSleep();
}
catch (InterruptedIOException iioe)
{
// Timeout occured... must resend packet
System.out.println ("TIMEOUT : " + sequenceNumber);
System.out.println ("Re-sending packet " + sequenceNumber);
// Send packet to receiver
sendPacket(sequenceNumber, m_addr, m_port);
}
catch (IOException ioe)
{
System.out.println ("I/O Error occured - " + ioe);
}
}
}
private int getSequenceNumber( DatagramPacket packet )
{
// Create a byte input stream to read the data
ByteArrayInputStream bin = new ByteArrayInputStream (packet.getData());
// Create a datainput stream to read sequence number
DataInputStream din = new DataInputStream (bin);
int sequenceNumber;
try
{
sequenceNumber = din.readInt();
}
catch (IOException ioe)
{
sequenceNumber = 0;
}
return sequenceNumber;
}
private void sendPacket(int sequenceNumber, InetAddress addr, int port)
{
try
{
// Byte array containing response
byte[] sendBuffer;
// Create output stream to a byte array
ByteArrayOutputStream bout = new ByteArrayOutputStream ();
DataOutputStream dout = new DataOutputStream (bout);
// Write sequence number to packet
dout.writeInt(sequenceNumber);
// Get data and put in packet
sendBuffer = bout.toByteArray();
// Create packet for response
DatagramPacket response = new DatagramPacket
(sendBuffer, sendBuffer.length, addr, port);
// Send packet through out datagram socket
m_datagramSocket.send(response);
}
catch (IOException ioe)
{
System.err.println ("Error sending packet - " + ioe);
}
}
private void randomSleep()
{
try
{
// Generate a random number between five hundred and
two thousand five hundred milliseconds
int randomValue = (random.nextInt() % 2000) + 500;
if (randomValue < 0 ) randomValue = -randomValue;
// Sleep for a random length of time
Thread.sleep( randomValue );
}
catch (InterruptedException interrupt)
{
// Ignore - sleeping is for a random length, so
// if we are interrupted that is fine
}
}
}
Listing 6.
import java.net.*;
import java.io.*;
// Receiver - demonstrates timeouts on a stop-and-wait protocol
//
// Written by : David Reilly
// Last modification date : March 1, 1998
//
//
public class Receiver
{
// Private member variables
// Internet address of sender
private InetAddress m_addr;
// Port of sender
private int m_port;
// DatagramSocket to communicate with sender
private DatagramSocket m_datagramSocket;
// Public constants
// Default packet size
public static final int packetSize = 20;
// Port for communication
public static final int port = 8000;
// Number of re-tries before terminating
public static final int maxRetry = 10;
// Constructor for receiver
public Receiver( )
{
try
{
// Create a datagram socket bound to a specific port
m_datagramSocket = new DatagramSocket ( port );
}
catch (IOException e)
{
System.err.println ("Error binding to socket - " + port);
System.exit(1);
}
}
public void receive()
{
// Local variables
int retryCount = 0;
int sequenceNumber = 0;
boolean messageArrived;
// Datagram packet for messages
DatagramPacket message = new DatagramPacket
( new byte[packetSize], packetSize);
try
{
// Receive the first message
m_datagramSocket.receive(message);
// Set a timeout for future communications
m_datagramSocket.setSoTimeout ( 5000 );
// Use the address and port of this packet's sender
m_addr = message.getAddress();
m_port = message.getPort();
System.out.println ("Packet received from " + m_addr.getHostName()
+":" + m_port);
}
catch (IOException e)
{
System.err.println ("Error reading packet - " + e);
System.exit(1);
}
// Wait for the first message
for (;;)
{
try
{
// Every twelfth packet, simulate a delay
// This will cause timeouts at sender, and
// packets will be re-sent by sender.
if ( sequenceNumber % 12 == 0)
try
{
// Sleep for ten seconds
Thread.sleep(10000);
}
catch (InterruptedException e)
{
// Ignore, as timeout is only for simulation purposes
}
// Send response
sendPacket(sequenceNumber, m_addr, m_port);
// Receive messages
m_datagramSocket.receive(message);
}
catch (InterruptedIOException iioe)
{
// Notify user
System.out.println ("TIMEOUT : " + sequenceNumber);
continue;
}
catch (IOException ioe)
{
// Notify user
System.err.println ("IOException -" + ioe);
// Terminate
System.exit(1);
}
// Get sequence number of message
int packetNumber = getSequenceNumber(message);
// Is it an older packet
if ( packetNumber < sequenceNumber )
{
sendPacket ( getSequenceNumber ( message ) , m_addr, m_port );
System.out.println ("Old packet (" + getSequenceNumber(message) + ")
acknowledged");
}
else
if ( packetNumber > sequenceNumber )
{
System.out.println ("Future packet (" + getSequenceNumber(message) + ")
received... ignoring");
}
// Packet number matches sequence number
else
{
// Print to screen
System.out.println ( "DATA : " + packetNumber );
// Increment sequence number
sequenceNumber++;
}
}
}
// Gets the sequence number from a packet
private int getSequenceNumber( DatagramPacket packet )
{
// Create a byte input stream to read the data
ByteArrayInputStream bin = new ByteArrayInputStream (packet.getData());
// Create a datainput stream to read sequence number
DataInputStream din = new DataInputStream (bin);
int sequenceNumber;
try
{
sequenceNumber = din.readInt();
}
catch (IOException ioe)
{
sequenceNumber = 0;
}
return sequenceNumber;
}
// Sends a packet
private void sendPacket(int sequenceNumber, InetAddress addr, int port)
{
try
{
// Byte array containing response
byte[] sendBuffer;
// Create output stream to a byte array
ByteArrayOutputStream bout = new ByteArrayOutputStream ();
DataOutputStream dout = new DataOutputStream (bout);
// Write sequence number to packet
dout.writeInt(sequenceNumber);
// Get data and put in packet
sendBuffer = bout.toByteArray();
// Create packet for response
DatagramPacket response = new DatagramPacket(sendBuffer, sendBuffer.length, addr, port);
// Send packet through out datagram socket
m_datagramSocket.send(response);
}
catch (IOException ioe)
{
System.err.println ("Error sending packet - " + ioe);
}
}
// MAIN method
public static void main (String args[])
{
Receiver receiver = new Receiver();
receiver.receive();
}
}
Download Source Code Assoicated Files (Zip file format - 8.54 KB)