An Introduction To Java NIO – Client Side

An Introduction To Java Networking

What is NIO?

Standing for Non-Blocking IO, Java NIO was introduced in 1.4 update by Sun Microsystems. Traditional IO Networking waits for data to be available for reading before performing other operations while Non-Blocking IO does not. If there is not any data available for reading then the application will perform the next operation instead.

Creating A NIO Client Application.

public class Client extends Runnable{
    @Overrride
    public void run(){
        //This is the main entry point of your client implementation
    }
}

Your class should extend Runnable in order to be ran in a seperate thread other than the main UI thread. Additionally, you will need to override the run method and will be invoked upon execution of the thread. This method will contain your main thread loop.

public class Client extends Runnable{

    SocketChannel channel = null;
    Selector selector = null;
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead;

    @Overrride
    public void run(){
        //This is the main entry point of your client implementation
    }
}

Further, your runnable class will implement the, SocketChannel, Selector, and ByteBuffer classes:

  • SocketChannel – Entry point between the client and the server via a connection socket
  • Selector – Maintains and iterates multiple channel connections. This allows a single thread to manage multiple connections (image from: http://tutorials.jenkov.com/java-nio/selectors.html).

  • ByteBuffer – Entry point for data input and output (1024 bytes are allocated to the buffer).
  • bytesRead – Number of bytes read from the channel

Initiating A Connection.


    public void init(){

        channel = SocketChannel.open();

        channel.configureBlocking(false);

        try{

            channel.connect(new InetSocketAddress(ip, port);

        }catch(ConnectionPending Exception e){

        }

        selector = Selector.open();

    }

In order for the SocketChannel to connect to a remote server the channel must first be prepped for connection by calling the open() method from the SocketChannel class. Then the channel must be set to non-blocking mode using the configureBlocking() method and passing false as a parameter. This will ensure that the channel remains in non-blocking mode.

To connect the channel’s socket to a remote server, the connect method must be supplied with an ip and port number of which to connect. This is provided by the InetSocketAddress that provides an object of which the socket uses for binding to the server.

Finally, the Selector must be initiated. This is accomplished by calling the open() method from the Selector class(); this method employs the system’s selector provider to create a new selector.

The Main Loop.

while(true){
    try{
        selector.select()
    }catch(IOException e){}

    Iterator i = selector.selectedKeys().iterator();</pre>
<pre>    while( i.hasNext() ){

    }</pre>
<pre> }

Now that the channel has been prepped for connection, the main loop must be implemented. Selector.select() waits until a channel is ready for an IO operation and generates a set of keys that represents that operation (read, write, or connect). Then an iterator is created, corresponding to the keys provided by the selector. An inner loop will be iterate through each key of which operations can be performed depending on the key provided.

Additionally, the select() method registers the server with the selector

 

while(true){
    try{
        selector.select()
    }catch(IOException e){}

    Iterator i = selector.selectedKeys().iterator();</pre>
<pre>    while( i.hasNext() ){
        SelectionKey key = (SelectionKey)i.next();
        i.remove();</pre>
<pre>        if( key.isConnectable() ){
            connect();
        }else if( key.isReadable() ){
            read();
        }else if( key.isWritable() ){
            write();
        }</pre>
<pre>    }</pre>
<pre> }

Next, the corresponding key is then obtain from the iterator by calling the i.next() method and casted to SelectionKey followed by removing the current item from the iterator. Then, based upon the operation registered with the key, the connect, read, or write functions are called.

  • isConnectable tests wether is key’s channel has either finished, or failed to finish, its socket-connection operation
  • isReadable test whether the channel is ready for reading
  • isWritable tests whether the channel is ready for writing

Additionally, a key can be associated with a desired operation by calling the interestOps() method and pass it either SelectionKey.[OP_READ,OP_WRITE,OP_CONNECT].

The connect() method.

private void connect(){
    try{
        if(channel.finishConnect() ){
            key.interestOps(SelectionKey.OP_READ);
        }
    }catch(IOException e){
        key.cancel();
    }
}</pre>
<pre>

Once a key has been set to connect the finishConnect() method is called. If the channel is successful in connecting to the server then the key is flagged for reading using the interestOps method. If the channel is unsuccessful, then the key is cancelled.

The read() method.

private void read(){
    int numBytes;
    SocketChannel c = key.channel();

    buffer.clear();

    while( (numBytes = c.read(buffer)) != 0){
        if(numBytes== -1)
            break;
        else
            bytesRead = numBytes;
    }
}</pre>
<pre>

The read() method obtains the channel from the key (of which was obtained from the selector which manages channels) and
reads from the channel into the provided ByteBuffer until zero bytes have been read. The SocketChannel.read() methods
returns the number of bytes read from the channel. A negative one indicated that an error has occurred during the
operation. Although, before any data is read into the buffer, the buffer must be cleared of any bytes left as to avoid
contaminated data.

Once data has been read from the channel we set the number returned by the c.read() method to byteRead to be used in our write method;

The write() method.

private void read(){
    int bytesWritten;
    SocketChannel c = key.channel();

    while( (bytesWritten = c.write(temp)) != 0){
        if(bytesWritten == -1)
            break;

    }
}</pre>
<pre>

The write() method is quite similar to the read method. The channel is obtained from the key and bytes are written to the channel stream until there is no longer any data available in the buffer. When a byte is written using the SocketChannel.write method, the ByteBuffer internal position flag is incremented. When the position has reached the end of the buffer or has reached null then the write method returns zero.