Java I/O and NIO.2

Socket programming in Java for scalable systems

From simple I/O to non-blocking asynchronous channels in the Java socket model

Java I/O and NIO.2

Show More
1 2 Page 2
Page 2 of 2
package com.geekcap.javaworld.simplesocketclient;

import java.io.BufferedReader;
import java.io.I/OException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class SimpleSocketServer extends Thread
{
    private ServerSocket serverSocket;
    private int port;
    private boolean running = false;

    public SimpleSocketServer( int port )
    {
        this.port = port;
    }

    public void startServer()
    {
        try
        {
            serverSocket = new ServerSocket( port );
            this.start();
        }
        catch (I/OException e)
        {
            e.printStackTrace();
        }
    }

    public void stopServer()
    {
        running = false;
        this.interrupt();
    }

    @Override
    public void run()
    {
        running = true;
        while( running )
        {
            try
            {
                System.out.println( "Listening for a connection" );

                // Call accept() to receive the next connection
                Socket socket = serverSocket.accept();

                // Pass the socket to the RequestHandler thread for processing
                RequestHandler requestHandler = new RequestHandler( socket );
                requestHandler.start();
            }
            catch (I/OException e)
            {
                e.printStackTrace();
            }
        }
    }

    public static void main( String[] args )
    {
        if( args.length == 0 )
        {
            System.out.println( "Usage: SimpleSocketServer <port>" );
            System.exit( 0 );
        }
        int port = Integer.parseInt( args[ 0 ] );
        System.out.println( "Start server on port: " + port );

        SimpleSocketServer server = new SimpleSocketServer( port );
        server.startServer();

        // Automatically shutdown in 1 minute
        try
        {
            Thread.sleep( 60000 );
        }
        catch( Exception e )
        {
            e.printStackTrace();
        }

        server.stopServer();
    }
}

class RequestHandler extends Thread
{
    private Socket socket;
    RequestHandler( Socket socket )
    {
        this.socket = socket;
    }

    @Override
    public void run()
    {
        try
        {
            System.out.println( "Received a connection" );

            // Get input and output streams
            BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream() ) );
            PrintWriter out = new PrintWriter( socket.getOutputStream() );

            // Write out our header to the client
            out.println( "Echo Server 1.0" );
            out.flush();

            // Echo lines back to the client until the client closes the connection or we receive an empty line
            String line = in.readLine();
            while( line != null && line.length() > 0 )
            {
                out.println( "Echo: " + line );
                out.flush();
                line = in.readLine();
            }

            // Close our connection
            in.close();
            out.close();
            socket.close();

            System.out.println( "Connection closed" );
        }
        catch( Exception e )
        {
            e.printStackTrace();
        }
    }
}

In Listing 2 we create a new SimpleSocketServer instance and start the server. This is required because the SimpleSocketServer extends Thread to create a new thread to handle the blocking accept() call that you see in the read() method. The run() method sits in a loop accepting client requests and creating RequestHandler threads to process the request. Again, this is relatively simple code, but also involves a fair amount of threaded programming.

Note too that the RequestHandler handles the client communication much like the code in Listing 1 did: it wraps the OutputStream with a PrintStream to facilitate easy writes and, similarly, wraps the InputStream with a BufferedReader for easy reads. As far as a server goes, it reads lines from the client and echoes them back to the client. If the client sends an empty line then the conversation is over and the RequestHandler closes the socket.

Java socket programming with NIO and NIO.2

For many applications, the base Java socket programming model that we've just explored is sufficient. For applications involving more intensive I/O or asynchronous input/output you will want to be familiar with the non-blocking APIs introduced in Java NIO and NIO.2.

The JDK 1.4 NIO package offers the following key features:

  • Channels are designed to support bulk transfers from one NIO buffer to another.
  • Buffers represent a contiguous block of memory interfaced by a simple set of operations.
  • Non-Blocking Input/Output is a set of classes that expose channels to common I/O sources like files and sockets.

When programming with NIO, you open a channel to your destination and then read data into a buffer from the destination, write the data to a buffer, and send that to your destination. We'll dive into setting up a socket and obtaining a channel to it shortly, but first let's review the process of using a buffer:

  1. Write data into a buffer
  2. Call the buffer's flip() method to prepare it for reading
  3. Read data from the buffer
  4. Call the buffer's clear() or compact() method to prepare it to receive more data

When data is written into the buffer, the buffer knows the amount of data written into it. It maintains three properties, whose meanings differ if the buffer is in read mode or write mode:

  • Position: In write mode, the initial position is 0 and it holds the current position being written to in the buffer; after you flip a buffer to put it in read mode, it resets the position to 0 and holds the current position in the buffer being read from,
  • Capacity: The fixed size of the buffer
  • Limit: In write mode, the limit defines how much data can be written into the buffer; in read mode, the limit defines how much data can be read from the buffer.

Java I/O demo: Echo server with NIO.2

NIO.2, which was introduced in JDK 7, extends Java's non-blocking I/O libraries to add support for filesystem tasks, such as the java.nio.file package and java.nio.file.Path class and exposes a new File System API. With that background in mind, let's write a new Echo Server using NIO.2's AsynchronousServerSocketChannel.

The AsynchronousServerSocketChannel provides a non-blocking asynchronous channel for stream-oriented listening sockets. In order to use it, we first execute its static open() method and then bind() it to a specific port. Next, we'll execute its accept() method, passing to it a class that implements the CompletionHandler interface. Most often, you'll find that handler created as an anonymous inner class.

Listing 3 shows the source code for our new asynchronous Echo Server.

Listing 3. SimpleSocketServer.java

package com.geekcap.javaworld.nio2;

import java.io.I/OException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class NioSocketServer
{
    public NioSocketServer()
    {
        try
        {
            // Create an AsynchronousServerSocketChannel that will listen on port 5000
            final AsynchronousServerSocketChannel listener =
                    AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(5000));

            // Listen for a new request
            listener.accept( null, new CompletionHandler<AsynchronousSocketChannel,Void>() {

                @Override
                public void completed(AsynchronousSocketChannel ch, Void att)
                {
                    // Accept the next connection
                    listener.accept( null, this );

                    // Greet the client
                    ch.write( ByteBuffer.wrap( "Hello, I am Echo Server 2020, let's have an engaging conversation!\n".getBytes() ) );

                    // Allocate a byte buffer (4K) to read from the client
                    ByteBuffer byteBuffer = ByteBuffer.allocate( 4096 );
                    try
                    {
                        // Read the first line
                        int bytesRead = ch.read( byteBuffer ).get( 20, TimeUnit.SECONDS );

                        boolean running = true;
                        while( bytesRead != -1 && running )
                        {
                            System.out.println( "bytes read: " + bytesRead );

                            // Make sure that we have data to read
                            if( byteBuffer.position() > 2 )
                            {
                                // Make the buffer ready to read
                                byteBuffer.flip();

                                // Convert the buffer into a line
                                byte[] lineBytes = new byte[ bytesRead ];
                                byteBuffer.get( lineBytes, 0, bytesRead );
                                String line = new String( lineBytes );

                                // Debug
                                System.out.println( "Message: " + line );

                                // Echo back to the caller
                                ch.write( ByteBuffer.wrap( line.getBytes() ) );

                                // Make the buffer ready to write
                                byteBuffer.clear();

                                // Read the next line
                                bytesRead = ch.read( byteBuffer ).get( 20, TimeUnit.SECONDS );
                            }
                            else
                            {
                                // An empty line signifies the end of the conversation in our protocol
                                running = false;
                            }
                        }
                    }
                    catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    catch (ExecutionException e)
                    {
                        e.printStackTrace();
                    }
                    catch (TimeoutException e)
                    {
                        // The user exceeded the 20 second timeout, so close the connection
                        ch.write( ByteBuffer.wrap( "Good Bye\n".getBytes() ) );
                        System.out.println( "Connection timed out, closing connection" );
                    }

                    System.out.println( "End of conversation" );
                    try
                    {
                        // Close the connection if we need to
                        if( ch.isOpen() )
                        {
                            ch.close();
                        }
                    }
                    catch (I/OException e1)
                    {
                        e1.printStackTrace();
                    }
                }

                @Override
                public void failed(Throwable exc, Void att) {
                    ///...
                }
            });
        }
        catch (I/OException e)
        {
            e.printStackTrace();
        }
    }

    public static void main( String[] args )
    {
        NioSocketServer server = new NioSocketServer();
        try
        {
            Thread.sleep( 60000 );
        }
        catch( Exception e )
        {
            e.printStackTrace();
        }
    }
}

In Listing 3 we first create a new AsynchronousServerSocketChannel and then bind it to port 5000:

        final AsynchronousServerSocketChannel listener =
              AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(5000));	

From this AsynchronousServerSocketChannel, we invoke accept() to tell it to start listening for connections, passing to it a custom CompletionHandler instance. When we invoke accept(), it returns immediately. Note that this example is different from the ServerSocket class in Listing 1; whereas the accept() method blocked until a client connected to it, the AsynchronousServerSocketChannel accept() method handles it for us.

The completion handler

Our next responsibility is to create a CompletionHandler class and provide an implementation of the completed() and failed() methods. The completed() method is called when the AsynchronousServerSocketChannel receives a connection from a client and it includes an AsynchronousSocketChannel to the client. The completed() method first accepts the connection from the AsynchronousServerSocketChannel and then starts communicating with the client. The first thing that it does is write out a "Hello" message: It builds a string, converts it to a byte array, and then passes it to ByteBuffer.wrap() to construct a ByteBuffer. The ByteBuffer can then be passed AsynchronousSocketChannel's write() method.

To read from the client, we create a new ByteBuffer by invoking its allocate(4096) (which creates a 4K buffer), then we invoke the AsynchronousSocketChannel's read() method. The read() returns a Future<Integer> on which we can invoke get() to retrieve the number of bytes read from the client. In this example, we pass get() a timeout value of 20 seconds: if we do not get a response in 20 seconds then the get() method will throw a TimeoutException. Our rule for this echo server is that if we observe 20 seconds of silence then we terminate the conversation.

Next we check the position of the buffer, which will be the location of the last byte received from the client. If the client sends an empty line then we receive two bytes: a carriage return and a line feed. The check ensures that if the client sends a blank line that we take it as an indicator that the client is finished with the conversation. If we have meaningful data then we call the ByteBuffer's flip() method to prepare it for reading. We create a temporary byte array to hold the number of bytes read from the client and then invoke the ByteBuffer's get() to load data into that byte array. Finally, we convert the byte array to a string by creating a new String instance. We echo the line back to the client by converting the string to a byte array, passing that to the ByteBuffer.wrap() method and invoking the AsynchronousSocketChannel's write() method. Now we clear() the ByteBuffer, which recall means that it repositions the position to zero and puts the ByteBuffer into write mode, and then we read the next line from the client.

The only thing to be aware of is that the main() method, which creates the server, also sets up a 60 second timer to keep the application running. Because the AsynchronousSocketChannel's accept() method returns immediately, if we don't have the Thread.sleep() then our application will stop immediately.

To test this out, launch the server and connect to it using a telnet client:

telnet localhost 5000

Send a few strings to the server, observe that they are echoed back to you, and then send an empty line to terminate the conversation.

In conclusion

In this article I've presented two approaches to sockets programming with Java: the traditional approach introduced with Java 1.0 and the newer, non-blocking NIO and NIO.2 approaches introduced in Java 1.4 and Java 7, respectively. I used several iterations of a Java socket client and a Java socket server example to demonstrate both the utility of basic Java I/O and some scenarios where non-blocking I/O improves and simplifies the Java socket programming model. Using non-blocking I/O, you can program Java networked applications to handle multiple simultaneous connections without having to manage multiple thread collections. You can also take advantage of the new server scalability that is built in to NIO and NIO.2.

1 2 Page 2
Page 2 of 2