Java NIO Channel Tutorial

Channels are the second major addition to java.nio after buffers which we have learned in my previous tutorial in detail. Channels provide direct connection to the I/O services.

A Channel is a medium that transports data efficiently between byte buffers and the entity on the other end of the channel (usually a file or socket).

Usually, channels have a one-to-one relationship with operating-system file descriptors. The Channel classes provide the abstraction needed to maintain platform independence but still model the native I/O capabilities of modern operating systems.

Channels are gateways through which the native I/O services of the operating system can be accessed with a minimum of overhead, and buffers are the internal endpoints used by channels to send and receive data.

1. Java NIO Channel

At the top of hirarchy, there is Channel interface which looks like this:

package java.nio.channels;

public interface Channel
{
	public boolean isOpen();
	public void close() throws IOException;
}

Channel implementations vary radically between operating systems due to various factors that depend on the underlying platform, so the channel APIs (or interfaces) simply describe what can be done.

Channel implementations often use native code to perform actual work. In this way, the channel interfaces allow you to gain access to low-level I/O services in a controlled and portable way.

As you can see by the top-level Channel interface, there are only two operations common to all channels: checking to see if a channel is open (isOpen()) and closing an open channel (close()).

1.1. Opening a Channel

As we already know that I/O falls into two broad categories:

  • File I/O
  • Stream I/O

So it’s no surprise that there are two types of channels: file and socket. FileChannel class and SocketChannel classes are used to deal with these two categories.

FileChannel object can be obtained only by calling the getChannel() method on an open RandomAccessFile, FileInputStream, or FileOutputStream object. You cannot create a FileChannel object directly.

Example 1: How to get a FileChannel

RandomAccessFile raf = new RandomAccessFile ("somefile", "r");
FileChannel fc = raf.getChannel();

Opposite to FileChannel, socket channels have factory methods to create new socket channels directly.

Example 2: How to create a SocketChannel

//How to open SocketChannel
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("somehost", someport));

//How to open ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind (new InetSocketAddress (somelocalport));

//How to open DatagramChannel
DatagramChannel dc = DatagramChannel.open();

The above methods return a corresponding socket channel object. They are not sources of new channels as RandomAccessFile.getChannel() is. They return the channel associated with a socket if one already exists; they never create new channels.

1.2. Using the Channels

As we already learned into buffers tutorial that channels transfer data to and from ByteBuffer objects. Most of the read/write operations are performed by methods implemented from below interfaces.

public interface ReadableByteChannel extends Channel
{
        public int read (ByteBuffer dst) throws IOException;
}

public interface WritableByteChannel extends Channel
{
        public int write (ByteBuffer src) throws IOException;
}

public interface ByteChannel extends ReadableByteChannel, WritableByteChannel
{
}

Channels can be unidirectional or bidirectional.

A given channel class might implement ReadableByteChannel, which defines the read() method. Another might implement WritableByteChannel to provide write().

A class implementing one or the other of these interfaces is unidirectional: it can transfer data in only one direction. If a class implements both interfaces (or ByteChannel which extends both interfaces), it is bidirectional and can transfer data in both directions.

If you go through Channel classes, you’ll find that each of the file and socket channels implement all three of these interfaces. In terms of class definition, this means that all the file and socket channel objects are bidirectional.

This is not a problem for sockets because they’re always bidirectional, but it is an issue for files. A FileChannel object obtained from the getChannel() method of a FileInputStream object is read-only but is bidirectional in terms of interface declarations because FileChannel implements ByteChannel.

Invoking write() on such a channel will throw the unchecked NonWritableChannelException because FileInputStream always opens files with read-only permission. So remember that when a channel connects to a specific I/O service, the capabilities of a channel instance will be constrained by the characteristics of the service to which it’s connected.

A Channel instance connected to a read-only file cannot write, even though the class to which that channel instance belongs may have a write() method. It falls to the programmer to know how the channel was opened and not to attempt an operation the underlying I/O service won’t allow.

Example 3: We cannot write to a read-only file using any channel

FileInputStream input = new FileInputStream ("readOnlyFile.txt");
FileChannel channel = input.getChannel();

// This will compile but will throw an IOException 
// because the underlying file is read-only
channel.write (buffer);

The read() and write() methods of ByteChannel take ByteBuffer objects as arguments. Each returns the number of bytes transferred, which can be less than the number of bytes in the buffer, or even zero. The position of the buffer will have been advanced by the same amount.

If a partial transfer was performed, the buffer can be resubmitted to the channel to continue transferring data where it left off. Repeat until the buffer’s hasRemaining() method returns false.

In the below example, we are copying data from one channel to another channel (or from one file to another file).

Example 4: Copying data from one channel to another channel in Java

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

public class ChannelCopyExample
{
	public static void main(String args[]) throws IOException 
	{
		FileInputStream input = new FileInputStream ("testIn.txt");
		ReadableByteChannel source = input.getChannel();
		
		FileOutputStream output = new FileOutputStream ("testOut.txt");
		WritableByteChannel dest = output.getChannel();

		copyData(source, dest);

		source.close();
		dest.close();
	}

	private static void copyData(ReadableByteChannel src, WritableByteChannel dest) throws IOException 
	{
		ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);

		while (src.read(buffer) != -1) 
		{
			// Prepare the buffer to be drained
			buffer.flip();

			// Make sure that the buffer was fully drained
			while (buffer.hasRemaining()) 
			{
				dest.write(buffer);
			}

			// Make the buffer empty, ready for filling
			buffer.clear();
		}
	}
}

Channels can operate in blocking or non-blocking modes. A channel in non-blocking mode never puts the invoking thread to sleep. The requested operation either completes immediately or returns a result indicating that nothing was done. Only stream-oriented channels, such as sockets and pipes, can be placed in non-blocking mode.

1.3. Closing a Channel

To close a channel, use it’s close() method. Unlike buffers, channels cannot be reused after closing it. An open channel represents a specific connection to a specific I/O service and encapsulates the state of that connection. When a channel is closed, that connection is lost, and the channel is no longer connected to anything.

It’s harmless to call close() on a channel multiple times. Subsequent calls to close() on the closed channel do nothing and return immediately.

Socket channels could conceivably take a significant amount of time to close depending on the system’s networking implementation. Some network protocol stacks may block a close while the output is drained.

The open state of a channel can be tested with the isOpen() method. If it returns true, the channel can be used. If false, the channel has been closed and can no longer be used.

Attempting to read, write, or perform any other operation that requires the channel to be in an open state will result in a ClosedChannelException.

Happy Learning !!

Was this post helpful?

Join 7000+ Fellow Programmers

Subscribe to get new post notifications, industry updates, best practices, and much more. Directly into your inbox, for free.

HowToDoInJava

A blog about Java and its related technologies, the best practices, algorithms, interview questions, scripting languages, and Python.