1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/turnon-javacore

Клонировать/Скачать
02.JavaNIO.md 27 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 06.06.2025 23:59 8aaa26d
title: Java NIO
date: 2020-02-19 18:54:21
categories:
  - Java
  - JavaSE
  - IO
tags:
  - Java
  - JavaSE
  - IO
  - NIO
permalink: /pages/6912a8/

Java NIO

Ключевые слова: Channel, Buffer, Selector, несинхронный, множественный выбор

Введение в NIO

NIO — это модель несинхронного ввода-вывода, введенная в Java 1.4. Она соответствует пакету java.nio, который предоставляет абстракции Channel, Selector и Buffer.

В NIO буква N может означать как Non-blocking (несинхронный), так и New. NIO поддерживает методы ввода-вывода, ориентированные на буферы и каналы. NIO предоставляет два различных реализации каналов, соответствующих Socket и ServerSocket из традиционной модели BIO: SocketChannel и ServerSocketChannel. Оба канала поддерживают как синхронный, так и несинхронный режимы. В синхронном режиме каналы работают так же, как в традиционной модели, но их производительность и надежность невысоки. В несинхронном режиме ситуация прямо противоположная. Для приложений с низкой нагрузкой и низкой конкуренцией можно использовать синхронный ввод-вывод для ускорения разработки и улучшения поддержки. Для приложений с высокой нагрузкой и высокой конкуренцией следует использовать несинхронный режим NIO.

Различия между NIO и BIO

Несинхронный ввод-вывод (Non-blocking IO)

BIO синхронен, а NIO несинхронен.Все потоки BIO являются синхронными. Это означает, что когда поток вызывает read() или write(), он блокируется до тех пор, пока не будет прочитана какая-либо информация или информация не будет полностью записана. В это время поток не может выполнять другие задачи.NIO позволяет нам выполнять асинхронные операции ввода-вывода. Например, в одном потоке данные могут быть прочитаны из канала в буфер, а затем поток может продолжить выполнение других задач. Аналогично, при записи данных поток может продолжить выполнение других задач, не дожидаясь завершения записи.

Буфер (Buffer)

BIO ориентирован на потоки (Stream oriented), а NIO ориентирован на буферы (Buffer oriented).

Буфер — это объект, содержащий данные для записи или чтения. В NIO-библиотеке введение объекта Buffer представляет собой важное отличие от BIO. В модели BIO с потоками данные могут быть напрямую записаны или прочитаны из объекта Stream. Хотя в Stream есть расширения с префиксом Buffer, они являются просто обертками для потока, и данные все равно читаются в буфер, а в NIO данные напрямую записываются или читаются из буферов. В NIO все данные обрабатываются с помощью буферов. При чтении данных они напрямую читаются из буферов; при записи данных они записываются в буферы. В любое время доступ к данным в NIO осуществляется через буферы.

Наиболее часто используемый буфер — это ByteBuffer, который предоставляет набор методов для работы с массивами байтов. Помимо ByteBuffer, существуют и другие типы буферов, фактически, каждый из базовых типов Java (кроме типа Boolean) имеет соответствующий буфер.#### Канал

NIO использует каналы для чтения и записи.

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

Селектор

NIO имеет селекторы, в то время как IO их не имеет.

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

Основной процесс NIO

Обычно все IO в NIO начинаются с канала.

  • Чтение данных из канала: создайте буфер, затем запросите канал для чтения данных.
  • Запись данных в канал: создайте буфер, заполните его данными, затем запросите канал для записи данных.

Основные компоненты NIO

NIO включает в себя следующие основные компоненты:

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

Канал

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

Файлы или сокеты обычно считаются более высокими уровнями абстракции, в то время как каналы являются более низкими уровнями абстракции, что позволяет NIO использовать механизмы современных операционных систем для оптимизации производительности в определенных сценариях, таких как DMA (Direct Memory Access). Разные уровни абстракции взаимосвязаны, и мы можем получить канал через сокет, и наоборот.

Основное отличие каналов от потоков заключается в следующем:

  • Потоки однобуквенны - один поток может только читать или только записывать данные.
  • Каналы двунаправленны - один канал может использоваться для чтения и записи данных одновременно.

Каналы включают следующие типы:

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

Buffer(буфер)NIO отличается от традиционного I/O тем, что он основан на блоках (Block), и он обрабатывает данные в виде блоков. Buffer представляет собой непрерывный блок памяти, который используется для хранения данных, которые будут прочитаны или записаны с помощью NIO. Buffer позволяет загружать файл целиком в память для последующей обработки, в то время как традиционный подход предполагает обработку данных по мере их чтения.Данные, которые читаются или записываются в канал (Channel), должны быть сначала помещены в буфер. Это означает, что напрямую данные не читаются или записываются в канал, а сначала помещаются в буфер. Буфер фактически представляет собой массив, но он предоставляет структурированный доступ к данным и позволяет отслеживать процесс чтения/записи.

BIO и NIO уже хорошо интегрированы, java.io.* был переработан на основе NIO, поэтому теперь он может использовать некоторые из его особенностей. Например, некоторые классы в java.io.* содержат методы для чтения и записи данных блоками, что позволяет ускорить обработку данных даже в потоковых системах.

Типы буферов включают:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

Состояние буфера

  • capacity:максимальная емкость;
  • position:текущее количество прочитанных или записанных байтов;
  • limit:количество байтов, которые еще можно прочитать или записать;
  • mark:записывает позицию position, которая была установлена ранее, по умолчанию равно 0, это удобство, но не всегда необходимо.

Пример изменения состояния буфера:1. Создание буфера размером 8 байт, в этом случае position = 0, а limit = capacity = 8. Переменная capacity не изменяется, поэтому далее она не учитывается. 2. Чтение 5 байт данных из входного канала и запись их в буфер, в результате position перемещается и устанавливается на 5, а limit остается неизменным. 3. Перед записью данных из буфера в выходной канал необходимо вызвать метод flip(), который устанавливает limit на текущее значение position и position на 0. 4. Чтение 4 байта данных из буфера и запись их в выходной буфер, в результате position устанавливается на 4. 5. Наконец, необходимо вызвать метод clear() для очистки буфера, в результате position и limit устанавливаются на начальные значения.### Пример использования NIO для копирования файловВот пример быстрого копирования файлов с использованием NIO:

public static void fastCopy(String src, String dist) throws IOException {

    /* Получаем входной поток байтов для исходного файла */
    FileInputStream fin = new FileInputStream(src);

    /* Получаем канал для входного потока байтов */
    FileChannel fcin = fin.getChannel();

    /* Получаем выходной поток байтов для целевого файла */
    FileOutputStream fout = new FileOutputStream(dist);

    /* Получаем канал для выходного потока байтов */
    FileChannel fcout = fout.getChannel();

    /* Выделяем  Yöntem 1024 байта для буфера */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    while (true) {

        /* Читаем данные из входного канала в буфер */
        int r = fcin.read(buffer);

        /* read() возвращает -1 при достижении конца файла */
        if (r == -1) {
            break;
        }

        /* Переключаемся в режим записи */
        buffer.flip();

        /* Записываем содержимое буфера в выходной файл */
        fcout.write(buffer);

        /* Очищаем буфер */
        buffer.clear();
    }
}

DirectBuffer

NIO также предоставляет класс DirectBuffer, который позволяет напрямую обращаться к физической памяти. Обычный Buffer выделяет память из кучи JVM, в то время как DirectBuffer выделяет память напрямую из физической памяти.

Для вывода данных на внешнее устройство необходимо сначала скопировать данные из пользовательского пространства в пространство ядра, а затем в саму внешнюю систему. DirectBuffer упрощает этот процесс, позволяя напрямую копировать данные из пространства ядра в внешнюю систему, что уменьшает количество копирований данных.Важно отметить, что создание и удаление DirectBuffer требует значительных затрат, так как они выделяют память вне кучи JVM. Память, выделенная DirectBuffer, не управляется стандартной системой сборки мусора JVM, но она освобождается при удалении объекта DirectBuffer с помощью механизма Java-ссылок.

Selector (Выборник)

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

Selector является основой Java NIO-программирования. Он используется для проверки состояния одного или нескольких NIO Channel на наличие готовности к чтению или записи.NIO реализует модель Reactor для многоканального ввода-вывода. Один поток (Thread) использует селектор Selector, который проверяет несколько каналов Channel на наличие событий (например, accept, read). Если на каком-либо канале происходит событие, этот канал переходит в состояние готовности, и затем выполняются операции ввода-вывода. При установке каналов Channel в режим неблокирующего ввода-вывода, когда события ввода-вывода еще не достигли канала, поток не будет блокироваться в ожидании этих событий, а продолжит проверять другие каналы Channel, чтобы найти канал, на котором события уже произошли, и выполнить соответствующие операции. Из-за высокой стоимости создания и переключения потоков, использование одного потока для обработки нескольких событий вместо одного потока на событие обеспечивает лучшую производительность.Важно отметить, что только SocketChannel может быть настроен в режиме неблокирующего ввода-вывода, в то время как FileChannel не может, так как для FileChannel настройка неблокирующего режима не имеет смысла.

В настоящее время все операционные системы используют механизм I/O мультиплексирования epoll, который не имеет ограничения на максимальное количество подключений в 1024, как у традиционного механизма select. Поэтому Selector теоретически может обрабатывать тысячи клиентов.

Создание селектора

Selector selector = Selector.open();

Регистрация канала в селекторе

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

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

При регистрации канала в селекторе необходимо указать конкретное событие для регистрации, среди которых:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

Они определены в классе SelectionKey следующим образом:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
```Каждое событие можно рассматривать как битовую маску, которая образует целое число событий. Например:

```java
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

Отслеживание событий

int num = selector.select();

Используйте select() для отслеживания событий, которые появляются, и он будет блокировать, пока не появится хотя бы одно событие.

Получение событий

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}

Цикл событий

Поскольку один вызов select() не может обработать все события, и сервер может быть обязан постоянно слушать события, код обработки событий сервера обычно находится в бесконечном цикле.```java while (true) { int num = selector.select(); Set keys = selector.selectedKeys(); Iterator keyIterator = keys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { // ... } else if (key.isReadable()) { // ... } keyIterator.remove(); } }


### Пример использования NIO сокетов

```java
public class NIOServer {

    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);
    }
}
```        while (true) {

            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) {

                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

                    // Сервер создает SocketChannel для каждого нового соединения
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    // Этот новый канал используется для чтения данных от клиента
                    sChannel.register(selector, SelectionKey.OP_READ);

                } else if (key.isReadable()) {

                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }

                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();

        while (true) {## Маппинг файлов в память

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

Запись в маппированный в память файл может быть опасной, так как простое изменение одного элемента массива может привести к прямому изменению файла на диске. Изменение данных и сохранение данных на диск не разделены.В следующем коде первые 1024 байта файла маппируются в память. Метод `map()` возвращает объект `MappedByteBuffer`, который является подклассом `ByteBuffer`. Поэтому новый маппированный буфер можно использовать так же, как и любой другой `ByteBuffer`, а операционная система будет отвечать за выполнение маппирования по мере необходимости.```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

Сравнение NIO и BIO

Главное различие между BIO и NIO заключается в способе упаковки и передачи данных: BIO обрабатывает данные в виде потока, в то время как NIO обрабатывает данные в виде блока.

  • BIO в виде потока обрабатывает данные по одному байту: один входной поток генерирует один байт данных, один выходной поток потребляет один байт данных. Создание фильтров для потоковых данных очень легко, связывая несколько фильтров, чтобы каждый фильтр отвечал за часть сложной обработки. Недостатком является то, что ввод-вывод в виде потока обычно очень медленный.
  • NIO в виде блока обрабатывает данные по блоку: обработка данных по блокам намного быстрее, чем обработка данных по потоку. Однако NIO в виде блока не обладает такой же элегантностью и простотой, как BIO в виде потока.

Режим BIO:

img

Режим NIO:

img

Ссылки

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/turnon-javacore.git
git@api.gitlife.ru:oschina-mirror/turnon-javacore.git
oschina-mirror
turnon-javacore
turnon-javacore
master