title: Java NIO
date: 2020-02-19 18:54:21
categories:
- Java
- JavaSE
- IO
tags:
- Java
- JavaSE
- IO
- NIO
permalink: /pages/6912a8/
Ключевые слова:
Channel
,Buffer
,Selector
,несинхронный
,множественный выбор
NIO — это модель несинхронного ввода-вывода, введенная в Java 1.4. Она соответствует пакету java.nio
, который предоставляет абстракции Channel
, Selector
и Buffer
.
В NIO буква N может означать как Non-blocking (несинхронный), так и New. NIO поддерживает методы ввода-вывода, ориентированные на буферы и каналы. NIO предоставляет два различных реализации каналов, соответствующих Socket
и ServerSocket
из традиционной модели BIO: SocketChannel
и ServerSocketChannel
. Оба канала поддерживают как синхронный, так и несинхронный режимы. В синхронном режиме каналы работают так же, как в традиционной модели, но их производительность и надежность невысоки. В несинхронном режиме ситуация прямо противоположная. Для приложений с низкой нагрузкой и низкой конкуренцией можно использовать синхронный ввод-вывод для ускорения разработки и улучшения поддержки. Для приложений с высокой нагрузкой и высокой конкуренцией следует использовать несинхронный режим NIO.
BIO синхронен, а NIO несинхронен.Все потоки BIO являются синхронными. Это означает, что когда поток вызывает read()
или write()
, он блокируется до тех пор, пока не будет прочитана какая-либо информация или информация не будет полностью записана. В это время поток не может выполнять другие задачи.NIO позволяет нам выполнять асинхронные операции ввода-вывода. Например, в одном потоке данные могут быть прочитаны из канала в буфер, а затем поток может продолжить выполнение других задач. Аналогично, при записи данных поток может продолжить выполнение других задач, не дожидаясь завершения записи.
BIO ориентирован на потоки (Stream oriented), а NIO ориентирован на буферы (Buffer oriented).
Буфер — это объект, содержащий данные для записи или чтения. В NIO-библиотеке введение объекта Buffer представляет собой важное отличие от BIO. В модели BIO с потоками данные могут быть напрямую записаны или прочитаны из объекта Stream. Хотя в Stream есть расширения с префиксом Buffer, они являются просто обертками для потока, и данные все равно читаются в буфер, а в NIO данные напрямую записываются или читаются из буферов. В NIO все данные обрабатываются с помощью буферов. При чтении данных они напрямую читаются из буферов; при записи данных они записываются в буферы. В любое время доступ к данным в NIO осуществляется через буферы.
Наиболее часто используемый буфер — это ByteBuffer, который предоставляет набор методов для работы с массивами байтов. Помимо ByteBuffer, существуют и другие типы буферов, фактически, каждый из базовых типов Java (кроме типа Boolean) имеет соответствующий буфер.#### Канал
NIO использует каналы для чтения и записи.
Каналы являются двунаправленными, они могут читать и записывать данные, в то время как потоки являются однобуквенными. Независимо от того, читаете ли вы или записываете данные, каналы могут взаимодействовать только с буферами. Благодаря буферам, каналы могут асинхронно читать и записывать данные.
NIO имеет селекторы, в то время как IO их не имеет.
Селекторы используются для обработки нескольких каналов с помощью одного потока. Это позволяет использовать меньшее количество потоков для работы с этими каналами. Переключение между потоками для операционной системы является дорогостоящим процессом. Поэтому селекторы полезны для повышения производительности системы.
Обычно все IO в NIO начинаются с канала.
NIO включает в себя следующие основные компоненты:
Канал является аналогом потока в BIO, через который можно читать и записывать данные.Канал, подобно файловым дескрипторам в операционных системах, таких как Linux, является абстракцией, используемой для поддержки операций массового ввода-вывода в NIO.
Файлы или сокеты обычно считаются более высокими уровнями абстракции, в то время как каналы являются более низкими уровнями абстракции, что позволяет NIO использовать механизмы современных операционных систем для оптимизации производительности в определенных сценариях, таких как DMA (Direct Memory Access). Разные уровни абстракции взаимосвязаны, и мы можем получить канал через сокет, и наоборот.
Основное отличие каналов от потоков заключается в следующем:
Каналы включают следующие типы:
FileChannel
— чтение и запись данных из файла;DatagramChannel
— чтение и запись данных сетевого соединения через UDP;SocketChannel
— чтение и запись данных сетевого соединения через TCP;ServerSocketChannel
— может прослушивать новые TCP-соединения, создавая для каждого нового соединения объект SocketChannel
.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();
}
}
NIO также предоставляет класс DirectBuffer
, который позволяет напрямую обращаться к физической памяти. Обычный Buffer
выделяет память из кучи JVM, в то время как DirectBuffer
выделяет память напрямую из физической памяти.
Для вывода данных на внешнее устройство необходимо сначала скопировать данные из пользовательского пространства в пространство ядра, а затем в саму внешнюю систему. DirectBuffer
упрощает этот процесс, позволяя напрямую копировать данные из пространства ядра в внешнюю систему, что уменьшает количество копирований данных.Важно отметить, что создание и удаление DirectBuffer
требует значительных затрат, так как они выделяют память вне кучи JVM. Память, выделенная DirectBuffer
, не управляется стандартной системой сборки мусора JVM, но она освобождается при удалении объекта DirectBuffer
с помощью механизма Java-ссылок.
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);
Главное различие между BIO и NIO заключается в способе упаковки и передачи данных: BIO обрабатывает данные в виде потока, в то время как NIO обрабатывает данные в виде блока.
Режим BIO:
Режим NIO:
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )