Статьи

Учебник по Java NIO

1. Введение

Java NIO — это библиотека, представленная в Java 1.4. Java NIO с момента своего запуска предоставила альтернативный способ обработки операций ввода-вывода и сетевых транзакций. Он считается альтернативой библиотекам Java Networking и Java IO. Java NIO была разработана с целью сделать транзакции для ввода и вывода асинхронными и неблокирующими. Концепция блокирующего и неблокирующего ввода-вывода объясняется в следующих разделах.

2. Терминологии в Java NIO

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

2.1 Блокировка ввода и вывода

С символьными потоками и байтовыми потоками файл был загружен в память JVM и заблокирован для чтения или записи. Это привело к ситуации блокировки для других программ, пытающихся прочитать тот же файл. Такой сценарий приводит к потере мощности процессора, поскольку программы вынуждены были ждать, несмотря на доступную вычислительную мощность. Такая схема чтения или записи данных называется блокированием ввода и вывода.

2.2 Неблокирующий ввод и вывод

С появлением неблокирующего входа и выхода данные начали считываться в каналы. В неблокирующем устройстве ввода и вывода JVM использует каналы для буферизации данных. Каналы позволяют читать данные динамически, не блокируя файл для внешнего использования. Каналы являются буферизованной частью памяти, которая заполняется, как только предыдущие данные буфера были прочитаны. Это гарантирует, что файл не будет заблокирован во время полного цикла чтения, и другие программы будут иметь к нему необходимый доступ.

Java NIO в основном состоит из трех терминов:

  • каналы
  • Селекторы
  • Буферы

Эти условия будут объявлены далее в этой статье.

3. Java NIO — Терминологии

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

3.1 Буферы

Буфер — это фиксированная часть памяти, используемая для хранения этих данных перед их чтением в канал. Буфер обеспечивает предварительную загрузку данных определенного размера для ускорения чтения файлов, входных данных и потоков данных. Размер буфера настраивается в блоках от 2 до степени n.

Различные типы буферов, которые могут использоваться в зависимости от типа ввода:

  • ByteBuffer: используется для чтения потоков символов или файлов в байтовом выражении
  • CharBuffer: используется для чтения символов в полном наборе ASCII
  • DoubleBuffer: используется специально для двойных значений данных, таких как показания датчиков
  • FloatBuffer: используется для чтения постоянных потоков данных для таких целей, как аналитика
  • LongBuffer: используется для чтения значений типа данных long
  • IntBuffer: используется для чтения целочисленных значений для результатов или результатов.
  • ShortBuffer: используется для чтения коротких целочисленных значений

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

1
2
3
RandomAccessFile aFile = new RandomAccessFile("src/data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);

В приведенном выше коде создается буфер из 48 байтов. Размер буфера является обязательным для указания. 3-я строка выделяет область памяти 48 байтов для буфера buf. Это гарантирует, что необходимая память предварительно выделена для буфера. Процесс чтения и записи в буфер необходимо понять, прежде чем мы перейдем к их использованию для чтения и записи канала. На рисунке ниже показан типичный буфер чтения байтов из файла.

Java NIO - чтение буфера

Чтение буфера

Как видно, буфер читает и толкает первый байт влево. Буфер является распределением памяти типа «Последний пришел — первый вышел». Следовательно, когда вы хотите прочитать файл с помощью буфера, необходимо перевернуть его, прежде чем вы сможете прочитать файл. Без переворота данные вышли бы в обратном порядке. Чтобы перевернуть буфер, необходимо выполнить простую строку кода, как показано ниже:

1
buf.flip();

Java NIO - buffer.flip ()

buffer.flip ()

Как только данные были прочитаны в буфер, пришло время фактически получить данные, которые были прочитаны. Для чтения данных используется функция buf.get() . Этот вызов функции читает один байт / символ / пакет при каждом вызове в зависимости от типа буфера. После того, как мы прочитали доступные данные, также необходимо очистить буфер перед следующим чтением. Очистка необходима, чтобы освободить место для чтения дополнительных данных. Чтобы очистить буфер, есть два возможных способа — очистить буфер или сжать буфер.

Чтобы очистить буфер, выполните команду buf.clear() . Чтобы buf.compact() буфер, используйте команду buf.compact() . Обе эти команды в конечном итоге делают одно и то же. Однако compact() очищает только те данные, которые были прочитаны с использованием вызова функции buf.get() . Таким образом, он используется, когда нам нужно продолжать оптимизировать объем памяти, используемой буфером.

3.1.1 Свойства буфера

Буфер по сути имеет 3 свойства:

  1. Буферная позиция
  2. Лимит буфера
  3. Буферная емкость

Во время записи в буфер позиция буфера — это позиция, в которой записывается текущий байт. Во время процесса чтения из буфера позиция буфера — это позиция, из которой читается байт. Положение буфера продолжает динамически меняться, когда мы продолжаем чтение или запись.

Во время записи в буфер предел буфера — это максимальный размер данных, которые могут быть записаны в буфер. По существу, ограничение буфера и емкость буфера являются синонимами во время записи в буфер. Однако во время чтения из буфера предел буфера — это количество доступных байтов для чтения из буфера. Следовательно, ограничение буфера продолжает уменьшаться по мере выталкивания байтов.

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

3.1.2 Чтение и запись в буфер

Буфер, по сути, является средой для хранения данных, пока поток не прочитает данные из него и не запросит новые данные. Основным шагом к чтению данных из источника ввода является чтение данных в буфер. Для считывания данных в буфер используется фрагмент кода, показанный ниже.

1
2
3
4
FileChannel inChannel = aFile.getChannel();
System.out.println("Created file Channel..");
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

В приведенном выше коде мы создаем FileChannel для чтения данных из файла и буфер buf для хранения данных. Затем буфер используется для чтения данных для канала с помощью оператора inChannel.read(buf) . При выполнении этого оператора буфер теперь будет содержать до 48 байт данных как доступные.
Чтобы начать читать данные, вы используете простое утверждение buf.get() .

3.1.3 Пометить и сбросить буфер

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

1
2
3
4
5
6
7
8
9
buffer.mark();
 
char x = buffer.get();
char y = buffer.get();
char z = buffer.get();
char a = buffer.get();
//Do something with above data
 
buffer.reset();  //set position back to mark.

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

3.2 Каналы

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

Каналы имеют несколько реализаций в зависимости от данных, которые нужно прочитать. Стандартные реализации, доступные для Каналов, перечислены ниже:

  • FileChannel: используется для чтения и записи данных из файлов и в файлы
  • DatagramChannel: используется для обмена данными по сети с использованием пакетов UDP.
  • SocketChannel: канал TCP для обмена данными через сокеты TCP
  • ServerSocketChannel: реализация, аналогичная веб-серверу, который прослушивает запросы через определенный порт TCP. Создает новый экземпляр SocketChannel для каждого нового соединения.

Как можно понять из названий каналов, они также охватывают сетевой IO-трафик UDP + TCP в дополнение к IO-файлу. В отличие от потоков, которые могут либо читать, либо записывать в определенный момент, один и тот же канал может беспрепятственно читать и записывать ресурсы. Каналы поддерживают асинхронное чтение и запись, что обеспечивает чтение данных без ущерба для выполнения кода. Буферы, рассмотренные выше, поддерживают эту асинхронную работу каналов.

3.2.1 Канал Scatter и Gather

Java NIO по своей природе поддерживает рассеяние и сбор данных для чтения и записи данных в несколько буферов. Java NIO достаточно умен, чтобы иметь возможность управлять чтением и записью в нескольких буферах.
Разброс Java NIO используется для разбивки чтения из канала на несколько буферов. Реализация кода довольно проста. Все, что вам нужно сделать, это добавить массив буферов в качестве аргумента для чтения. Фрагмент кода для того же самого был показан ниже.

1
2
3
4
5
ByteBuffer buffer1 = ByteBuffer.allocate(128);
ByteBuffer buffer2   = ByteBuffer.allocate(128);
ByteBuffer[] buffers = {buffer1,buffer2};
 
channel.read(buffers);

В приведенном выше коде мы создали два буфера по 128 байт каждый. Обратите внимание, что мы создали массив из двух буферов. Этот массив затем передается в качестве аргумента в канал для чтения. Канал считывает данные в первый буфер, пока не будет достигнута емкость буфера. Как только емкость буфера достигнута, канал автоматически переключается на следующий буфер. Таким образом, чтение канала рассеивается без какого-либо влияния на поток.
Сбор Java NIO также работает аналогичным образом. Данные, считанные в несколько буферов, также могут быть собраны и записаны в один канал. Ниже приведен фрагмент кода.

1
2
3
4
5
ByteBuffer buffer1 = ByteBuffer.allocate(128);
ByteBuffer buffer2   = ByteBuffer.allocate(128);
ByteBuffer[] buffers = {buffer1,buffer2};
 
channel.write(buffers);

Код похож на рассеяние чтения. Что нужно понять, так это порядок записи информации. Информация записывается в канал, начиная с первого буфера. Канал автоматически переключается на следующий буфер при достижении предела для первого. Важно отметить, что во время этой записи переворот не происходит. Если вам нужно перевернуть буфер перед записью, это нужно сделать перед назначением их в массив.

3.2.2 Канальные передачи

Передача канала, как следует из названия, представляет собой процесс передачи данных из одного канала в другой. Передача канала может осуществляться из определенной позиции буфера канала. Однако при значении позиции, равном нулю, можно копировать или реплицировать полный источник ввода в указанное место назначения вывода. Например, установление канала между ключевым словом и текстовым редактором позволит вам непрерывно переносить ввод с клавиатуры в текстовый редактор.
Чтобы облегчить передачу канала, Java NIO оснащен двумя функциями, а именно — transferFrom() и transferTo() . Назначение этих функций довольно ясно из их идентификаторов. Давайте возьмем пример, чтобы лучше понять эти функции.
Мы будем использовать файл data.txt, созданный выше, в качестве источника ввода. Мы перенесем данные из этого файла в новый файл output.txt . Код ниже делает то же самое, используя вызов метода TransferFrom transferFrom() .

ChannelTransfer.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
 
public class ChannelTransfer {
    public static void main(String[] args) {
        try {
            RandomAccessFile copyFrom = new RandomAccessFile("src/data.txt", "rw");
            FileChannel fromChannel = copyFrom.getChannel();
 
            RandomAccessFile copyTo = new RandomAccessFile("src/output.txt", "rw");
            FileChannel toChannel = copyTo.getChannel();
 
            long count = fromChannel.size();
 
            toChannel.transferFrom(fromChannel, 0, count);
        } catch (Exception e) {
            System.out.println("Error: " + e);
        }
    }
}

Как видно из приведенного выше кода, канал fromChannel используется для чтения данных из data.txt. toChannel используется для получения данных из fromChannel начиная с позиции 0. Важно отметить, что весь файл копируется с использованием FileChannel . Однако в некоторых реализациях SocketChannel это может быть не так. В таком случае копируются только те данные, которые доступны для чтения в буфере во время передачи. Такое поведение обусловлено динамической природой реализаций SocketChannel .

Реализация transferTo довольно похожа. Единственное изменение, которое потребуется, — это то, что вызов метода будет выполняться с использованием исходного объекта, а объект канала назначения будет аргументом в вызове метода.

3.3 Селекторы

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

Java NIO - селекторы NIO

Селекторы NIO

Создание селектора довольно просто. Приведенный ниже фрагмент кода объясняет, как создать селектор и как зарегистрировать канал в селекторе.

1
2
3
4
Selector myFirstSelector = Selector.open(); //opens the selector
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
SelectionKey selectorKey = channel.register(myFirstSelector, SelectionKey.OP_READ);

Приведенный выше код создает SocketChannel , настраивает его для неблокирования и регистрирует его с помощью селектора. Обратите внимание, что мы использовали SocketChannel . Для селектора требуется канал, который можно настроить как неблокирующий. Следовательно, селектор не может использоваться с FileChannel .
Еще один момент, на который следует обратить внимание — это второй аргумент при регистрации SocketChannel . Аргумент указывает события канала, который мы заинтересованы в мониторинге. Селектор ожидает события и меняет свой статус по мере запуска событий. События перечислены ниже:

  1. Connect
  2. принимать
  3. Читать
  4. Напишите

Эти события могут быть настроены с использованием статических констант, показанных ниже:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

Селектор предоставляет нам предопределенные функции для проверки возникновения этих событий. Приведенные ниже вызовы методов говорят сами за себя и могут использоваться для отслеживания возникновения этих событий.

1
2
3
4
key.isAcceptable();
key.isConnectable();
key.isReadable();
key.isWritable();

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

4. NIO Каналы

4.1 FileChannel

Этот тип канала обычно используется для чтения и записи файлов. Пример кода для создания канала и его использования для чтения и записи показан ниже

ChannelRW.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
 
public class ChannelRW {
    public static void main(String[] args) {
        System.out.println("Starting the file read..");
        try {
        RandomAccessFile aFile = new RandomAccessFile("src/data.txt", "rw");
        FileChannel inChannel = aFile.getChannel();
        System.out.println("Created file Channel..");
        ByteBuffer buf = ByteBuffer.allocate(48);
 
        int bytesRead = inChannel.read(buf);
        while (bytesRead != -1) {
 
          System.out.println("\nRead " + bytesRead);
          buf.flip();
 
          while(buf.hasRemaining()){
              System.out.print((char) buf.get());
          }
 
          buf.clear();
          bytesRead = inChannel.read(buf);
        }
        aFile.close();
        }catch(Exception e) {
            System.out.println("Error:"+e);
        }
    }
}

data.txt

1
2
3
Hello,
This is my first NIO read. I am reading multiple lines.
The code is implemented for Javacodegeeks by Abhishek Kothari

В приведенном выше коде мы пытаемся прочитать файл data.txt созданный, как показано выше. Файл читается с использованием библиотек Java NIO. Процесс чтения файла включает в себя несколько этапов. Пошаговое объяснение кода для того же самого показано ниже.

  1. Откройте файл с помощью объекта RandomAccessFile . Это гарантирует, что файл не заблокирован для доступа. Как следует из названия класса, доступ к файлу разрешен случайным образом только при необходимости. Следовательно, IO является неблокирующим по своей природе
  2. Получить объект FileChannel из созданного выше объекта. Канал помогает в чтении данных из файла по мере необходимости. Канал — это своего рода туннель между файлом и буфером.
  3. Создайте буфер для хранения байтов, прочитанных из канала. Обратите внимание, что мы указали размер буфера. Таким образом, данные будут считываться кусками по 48 байт всегда.
  4. Читайте данные из буфера, пока количество прочитанных байтов не станет отрицательным.
  5. Распечатайте данные, которые были прочитаны после щелчка буфера. Причина переворачивания буфера уже объяснена выше.
  6. Закройте файловые объекты, чтобы предотвратить утечку памяти любого рода

Вывод приведенного выше кода, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
Starting the file read..
Created file Channel..
 
Read 48
Hello,
This is my first NIO read. I am reading m
Read 48
ultiple lines.
The code is implemented for Javac
Read 28
odegeeks by Abhishek Kothari

Как видно, данные читаются порциями по 48 байтов, и каждый раз, когда появляется оператор Read 48 отображается количество прочитанных байтов. Как можно видеть, когда он достигает конца файла, доступно только 28 байтов для чтения и, следовательно, он возвращает счетчик 28.

4.2 SocketChannel

Этот тип канала используется для подключения к http-сокетам. Простой код для подключения к сокету показан ниже:

1
2
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("https://javacodegeeks.com", 8080));

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

1
sc.close();

Процесс чтения данных из SocketChannel аналогичен FileChannel .

4.3 ServerSocketChannel

Канал сокета сервера используется для чтения данных сокета, поступающих от клиентов сокета. Этот канал можно настроить для чтения с использованием приведенного ниже фрагмента кода.

1
2
3
4
5
6
7
8
9
ServerSocketChannel ssc = ServerSocketChannel.open();
 
ssc.socket().bind(new InetSocketAddress(8080));
 
while(true){
    SocketChannel sc =
            ssc.accept();
    //do something with sc...
}

Приведенный выше код открывает сокет на стороне сервера и разрешает подключение к сокету от внешних клиентов SocketChannel . Как можно понять из приведенного выше кода, для ServerSocketChannel требуется только номер порта для запуска сокет-сервера. После запуска он может создать собственный SocketChannel для приема данных из других сокетов. Вот как неблокирующее соединение Socket IO было бы возможно при использовании Java NIO.

5. Заключение

В статье подробно обсуждаются различные термины Java NIO. В нем объясняется процесс чтения и записи данных неблокирующим образом с использованием различных каналов NIO. Существует еще больше возможностей для изучения с точки зрения библиотек Java NIO, таких как DatagramChannel, Pipes, Async каналы и другие.