Учебное пособие по каналу Java NIO

Каналы являются вторым крупным дополнением к java.nio после буферов, которые мы подробно изучили в моем предыдущем уроке. Каналы обеспечивают прямое подключение к службам ввода-вывода.

Канал — это среда, которая эффективно передает данные между байтовыми буферами и объектом на другом конце канала(обычно файлом или сокетом).

Обычно каналы имеют отношение один к одному с файловыми дескрипторами операционной системы. Классы каналов предоставляют абстракцию, необходимую для поддержания независимости платформы, но при этом моделируют собственные возможности ввода-вывода современных операционных систем.

Каналы — это шлюзы, через которые можно получить доступ к собственным службам ввода-вывода операционной системы с минимальными накладными расходами, а буферы — это внутренние конечные точки, используемые каналами для отправки и получения данных.

1. Канал Java NIO

На вершине иерархии находится интерфейс канала, который выглядит следующим образом:

package java.nio.channels;public interface Channel{public boolean isOpen();public void close() throws IOException;}

Реализации каналов радикально различаются в разных операционных системах из-за различных факторов, зависящих от базовой платформы, поэтому API каналов(или интерфейсы) просто описывают, что можно сделать.

Реализации каналов часто используют собственный код для выполнения фактической работы. Таким образом, интерфейсы каналов позволяют вам получить доступ к низкоуровневым службам ввода-вывода контролируемым и переносимым способом.

Как видно из интерфейса Channel верхнего уровня, для всех каналов существуют только две операции: проверка того, открыт ли канал(isOpen()), и закрытие открытого канала(close()).

1.1 Открытие канала

Как мы уже знаем, ввод-вывод делится на две основные категории:

  • Файловый ввод/вывод
  • Поток ввода-вывода

Поэтому неудивительно, что существует два типа каналов: файл и сокет. Класс FileChannel и классы SocketChannel используются для работы с этими двумя категориями.

Объект FileChannel можно получить только вызвав метод getChannel() для открытого объекта RandomAccessFile, FileInputStream или FileOutputStream. Вы не можете создать объект FileChannel напрямую.

Пример 1: Как получить FileChannel

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

В отличие от FileChannel, каналы сокетов имеют фабричные методы для непосредственного создания новых каналов сокетов.

Пример 2: Как создать SocketChannel

//How to open SocketChannelSocketChannel sc = SocketChannel.open();sc.connect(new InetSocketAddress("somehost", someport));//How to open ServerSocketChannelServerSocketChannel ssc = ServerSocketChannel.open();ssc.socket().bind(new InetSocketAddress(somelocalport));//How to open DatagramChannelDatagramChannel dc = DatagramChannel.open();

Вышеуказанные методы возвращают соответствующий объект канала сокета. Они не являются источниками новых каналов, как RandomAccessFile.getChannel(). Они возвращают канал, связанный с сокетом, если он уже существует; они никогда не создают новые каналы.

1.2 Использование каналов

Как мы уже узнали в учебнике по буферам, каналы передают данные в объекты ByteBuffer и из них. Большинство операций чтения/записи выполняются методами, реализованными из интерфейсов ниже.

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{}

Каналы могут быть однонаправленными или двунаправленными.

Данный класс канала может реализовать ReadableByteChannel, который определяет метод read(). Другой может реализовать WritableByteChannel для предоставления write().

Класс, реализующий один или другой из этих интерфейсов, является однонаправленным: он может передавать данные только в одном направлении. Если класс реализует оба интерфейса(или ByteChannel, который расширяет оба интерфейса), он является двунаправленным и может передавать данные в обоих направлениях.

Если вы пройдетесь по классам Channel, вы обнаружите, что каждый из каналов файлов и сокетов реализует все три этих интерфейса. С точки зрения определения класса это означает, что все объекты каналов файлов и сокетов являются двунаправленными.

Это не проблема для сокетов, поскольку они всегда двунаправлены, но это проблема для файлов. Объект FileChannel, полученный из метода getChannel() объекта FileInputStream, доступен только для чтения, но является двунаправленным с точки зрения деклараций интерфейсов, поскольку FileChannel реализует ByteChannel.

Вызов write() на таком канале выдаст неотмеченное исключение NonWritableChannelException, поскольку FileInputStream всегда открывает файлы с разрешением только на чтение. Поэтому помните, что когда канал подключается к определенной службе ввода-вывода, возможности экземпляра канала будут ограничены характеристиками службы, к которой он подключен.

Экземпляр канала, подключенный к файлу только для чтения, не может писать, даже если класс, к которому принадлежит этот экземпляр канала, может иметь метод write(). Программист должен знать, как был открыт канал, и не пытаться выполнить операцию, которую не позволит базовая служба ввода-вывода.

Пример 3: Мы не можем записать файл, доступный только для чтения, используя любой канал.

FileInputStream input = new FileInputStream("readOnlyFile.txt");FileChannel channel = input.getChannel();// This will compile but will throw an IOException// because the underlying file is read-onlychannel.write(buffer);

Методы read() и write() ByteChannel принимают объекты ByteBuffer в качестве аргументов. Каждый возвращает количество переданных байтов, которое может быть меньше количества байтов в буфере или даже равно нулю. Позиция буфера будет сдвинута на ту же величину.

Если была выполнена частичная передача, буфер может быть повторно отправлен в канал для продолжения передачи данных с того места, где он остановился. Повторяйте, пока метод hasRemaining() буфера не вернет false.

В приведенном ниже примере мы копируем данные из одного канала в другой канал(или из одного файла в другой файл).

Пример 4: Копирование данных из одного канала в другой канал в 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 drainedbuffer.flip();// Make sure that the buffer was fully drainedwhile(buffer.hasRemaining()){dest.write(buffer);}// Make the buffer empty, ready for fillingbuffer.clear();}}}

Каналы могут работать в блокирующем или неблокирующем режиме. Канал в неблокирующем режиме никогда не переводит вызывающий поток в спящий режим. Запрошенная операция либо завершается немедленно, либо возвращает результат, указывающий, что ничего не было сделано. Только потокоориентированные каналы, такие как сокеты и каналы, могут быть переведены в неблокирующий режим.

1.3 Закрытие канала

Чтобы закрыть канал, используйте его метод close(). В отличие от буферов, каналы не могут быть повторно использованы после закрытия. Открытый канал представляет собой определенное соединение с определенной службой ввода-вывода и инкапсулирует состояние этого соединения. Когда канал закрывается, это соединение теряется, и канал больше ни к чему не подключен.

Безвредно вызывать close() на канале несколько раз. Последующие вызовы close() на закрытом канале ничего не делают и возвращаются немедленно.

Каналы сокетов могут предположительно занять значительное время для закрытия в зависимости от сетевой реализации системы. Некоторые стеки сетевых протоколов могут блокировать закрытие, пока выход опустошается.

Состояние открытия канала можно проверить с помощью метода isOpen(). Если он возвращает true, канал можно использовать. Если false, канал закрыт и больше не может использоваться.

Попытка чтения, записи или выполнения любой другой операции, требующей открытого состояния канала, приведет к возникновению исключения ClosedChannelException.

Прокрутить вверх