Абстрактное представление файлов, файловые операции с потоками байтов и потоками символов относятся к традиционному вводу-выводу, и мы провели глубокое исследование в предыдущих статьях.
Тем не менее, традиционный поток ввода-вывода по-прежнему имеет много недостатков, особенно его блокировку и изначально медленные чтение и запись на диск, что значительно снижает эффективность использования ЦП.
Таким образом, jdk 1.4 выпустил пакет NIO.Дизайн чтения и записи файлов NIO подрывает традиционный дизайн ввода-вывода.Использование «канала» + «буферной области» делает новую операцию ввода-вывода непосредственно обращенной к буферной области, и она не блокируется, что повышает эффективность Это не раз и не два, давайте посмотрим.
канал
Мы сказали, что ядро NIO — это канал и буферная область, поэтому режим их работы следующий:
Каналы чем-то похожи на потоки в IO, но разница в том, что один и тот же канал допускает и чтение, и запись, а любой поток является либо потоком чтения, либо потоком записи.
Но вы должны понимать, что каналы, как и потоки, должны быть основаны на физических файлах, и каждый поток или канал оперирует файлами через файловые указатели. random Доступный для чтения и записи указатель файла доступа к файлу "RandomAccessFile".
«RandomAccessFile» доступен как для чтения, так и для записи, поэтому канал, основанный на нем, является двунаправленным, поэтому предложение «канал является двунаправленным» является предпосылкой и не может быть вырвано из контекста.
Основные типы каналов следующие:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel — это файловый канал, SocketChannel и ServerSocketChannel используются для чтения и записи дейтаграмм сетевого сокета TCP, а DatagramChannel используется для чтения и записи дейтаграмм сетевого сокета UDP.
Канал не может существовать сам по себе, он всегда должен быть привязан к области буфера, все данные будут существовать только в области буфера, независимо от того, пишете вы или читаете, должно быть, чтобы область буфера достигала файла на диске через канал или файл на диск поступает по каналу cache area.
То есть область буфера является "начальной точкой" и "конечной точкой" данных. Каковы различия между этими каналами, как их использовать и как они в основном реализованы. После того, как мы введем понятие " буферная зона», узнаем подробно.
Буфер Буфер
Buffer — это базовый класс всех конкретных буферов, это абстрактный класс со множеством классов реализации, включая буферы различных типов данных.
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- MappedByteBuffer
В качестве примера для изучения возьмем ByteBuffer.Остальные буферы также основаны на байтовых буферах, но есть еще один процесс преобразования байтов.MappedByteBuffer — это специальный метод буферизации, о котором мы расскажем отдельно.
В Buffer есть несколько важных свойств членов, давайте посмотрим:
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
long address;
отметьте свойство у нас не незнакомым, для повторного чтения. Описание емкости Емкость буфера, т.е. сколько может храниться максимальный объем всего буфера данных. прямой адрес памяти для операции, отличной от памяти jvm, о которой пойдет речь.
И позицию и лимит я хочу использовать для объяснения картинки:
Так как область кэша сосуществует с чтением и записью, значения этих двух переменных имеют разные значения в разных режимах.
В режиме записи,Так называемый режим записи заключается в записи содержимого буфера в канал.. position представляет позицию в буфере байта, куда должен быть записан следующий байт, а limit представляет позицию последнего байта, который должен быть записан в буфер.
В режиме чтения,Так называемый режим чтения — чтение данных из канала в буфер. position представляет позицию, в которой следующий прочитанный байт должен быть сохранен в буфере, limit равен емкости.
Для деталей связанных операций чтения и записи мы рассмотрим с вами исходный код позже, чтобы углубить принцип взаимодействия между каналами и областями кэша.Здесь мы обсудим проблему, на которую вы, возможно, не обращали особого внимания.
Память JVM делится на стек и кучу.Это знание, что у всех есть глубокие знания, но на самом деле есть еще кусок памяти вне кучи, то есть прямая память, которая делится на JVM.Многие люди не знают, для чего используется эта память.
Это часть физической памяти, специально используемая JVM для работы с устройствами ввода-вывода.Нижний уровень Java использует API языка C для вызова операционной системы для взаимодействия с устройствами ввода-вывода.
Например, если в памяти Java имеется массив байтов, и теперь вызывается поток для его записи в файл на диске, то JVM сначала скопирует массив байтов в память вне кучи, а затем вызовет API языка C для укажите непрерывный Данные для диапазона адресов записываются на диск.
Операция чтения также аналогична, и дополнительная работа по копированию, выполняемая JVM, также имеет смысл, поскольку JVM работает на основе механизма автоматической сборки мусора, и все данные в памяти будут непрерывно перемещаться во время GC. системный API, чтобы сообщить об операции Система записывает память определенного места в памяти на диск, и в это время GC перемещает часть данных.После завершения GC, операционная система записала неправильные данные?
Поэтому, когда JVM взаимодействует с периферийными устройствами ввода-вывода, она копирует данные памяти в память вне кучи, а затем вызывает системный API для косвенной записи на диск, и чтение аналогично. Поскольку сборщик мусора не управляет памятью вне кучи, вы должны не забывать освобождать ее, когда она израсходована.
Понимание этого небольшого знания является необходимым условием для понимания реализации исходного кода, иначе вы можете не знать, что делает разработчик кода. Что ж, давайте сначала рассмотрим базовое использование и реализацию операций чтения в исходном коде.
RandomAccessFile file = new RandomAccessFile
("C:\\Users\\yanga\\Desktop\\note.txt","rw");
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
byte[] res = new byte[1024];
buffer.get(res,0,buffer.limit());
System.out.println(new String(res));
channel.close();
Давайте посмотрим на такой кусок кода.Этот код условно разделен на четыре части.Первая часть используется для получения файлового канала,вторая часть используется для выделения области буфера и завершения операции чтения,и третья часть используется для печати данных в области буфера. , четвертая часть предназначена для закрытия соединения канала.
первая часть:
Метод getChannel используется для получения экземпляра канала, связанного с файлом. Конкретная реализация выглядит следующим образом:
public final FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, rw, this);
}
return channel;
}
}
public static FileChannel open
(FileDescriptor var0, String var1, boolean var2, boolean var3, Object var4) {
return new FileChannelImpl(var0, var1, var2, var3, false, var4);
}
Метод getChannel вызовет фабричный метод FileChannelImpl для создания экземпляра FileChannelImpl, который является реализацией подкласса абстрактного класса FileChannel.
Необходимые параметры, необходимые для формирования экземпляра FileChannelImpl, — это указатель на файл, полный путь к файлу, права на чтение и запись и т. д.
Вторая часть:
Базовая структура Buffer была кратко представлена выше и не будет здесь повторяться.Так называемая область буфера представляет собой массив байтов.
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
Создание экземпляра ByteBuffer производится по заводскому шаблону, а параметр емкости должен быть указан как емкость внутреннего массива байтов. HeapByteBuffer — это память виртуальной машины в куче. Все данные будут храниться в пространстве кучи. Мы представим одного из его братьев и сестер, DirectByteBuffer, который размещается в памяти вне кучи, о чем мы поговорим чуть позже.
С тем же успехом мы могли бы продолжить построение этого HeapByteBuffer:
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, new byte[cap], 0);
}
Вызовите конструктор родительского класса для инициализации некоторых значений свойств, упомянутых нами в ByteBuffer, таких как позиция, емкость, метка, предел, смещение и массив байтов hb.
Далее давайте посмотрим на цепочку вызовов этого метода чтения.
Этот метод чтения является переопределением метода чтения родительского класса FileChannel подклассом FileChannelImpl. Этот метод не является ядром операции чтения. Кратко резюмируем, что этот метод сначала получит блокировку текущего экземпляра канала. Если он не занят другими потоками, то блокировка будет занята, и метод чтения IOUtil будет называться.
Метод чтения IOUtil также вызывает множество методов, а некоторые даже являются локальными методами.Здесь мы лишь кратко представим общую логику всего метода чтения, а конкретные детали оставляем вам для самостоятельного изучения.
Сначала определяем, является ли наш экземпляр ByteBuffer DirectBuffer, то есть определяем, размещен ли текущий экземпляр ByteBuffer в прямой памяти.Если да, то вызываем метод readIntoNativeBuffer для чтения данных с диска и непосредственно помещаем их в прямую память, где Экземпляр ByteBuffer находится. .
В противном случае виртуальная машина выделяет блок памяти в области прямой памяти, первый адрес которой хранится в свойстве address экземпляра var5.
Затем прочитайте данные с диска и поместите их в область прямой памяти, представленную var5.
Наконец, метод put запишет данные из прямой области памяти, представленной var5, в буферную область кучи, представленную var1, и освободит временно созданное пространство прямой памяти.
Таким образом, данные были успешно прочитаны в буфер, который мы передали. Операция записи обратная, можете сравнить сами, в любом случае, если данные в куче хотят попасть на диск, они должны пройти процесс копирования памяти вне кучи.
Третья и четвертая части относительно просты и не будут здесь повторяться. Напоминаем, что если вы хотите лучше использовать этот канал и область буфера для операций чтения и записи файлов, вы всегда должны понимать значения нескольких переменных в области буфера, каковы текущие значения позиции и предела , а примерное положение какое, оно должно быть четким, а то этот буфер с сосуществованием чтения и записи может вас запутать.
селектор селектор
Селектор — это компонент Java NIO, который используется для мониторинга различных состояний нескольких каналов и управления несколькими каналами. Но в сущности, поскольку FileChannel не поддерживает регистрацию селекторов, обычно считается, что селекторы обслуживают каналы сетевых сокетов.
«NIO не блокируется» у всех на устах, если быть точным, относится к процессу обмена данными между клиентом и сервером в сетевом программировании без блокировки. Обычное чтение и запись файлов по-прежнему блокируется, как и ввод-вывод, что может сбивать с толку многих новичков, включая меня, я всегда не мог понять, почему чтение и запись файлов в NIO были неблокирующими, они явно блокировались.
Создание селектора обычно выполняется с помощью фабричного метода Selector, Selector.open :
Selector selector = Selector.open();
Если канал хочет зарегистрироваться с помощью селектора, он должен настроить режим на неблокирующий режим, например:
//创建一个 TCP 套接字通道
SocketChannel channel = SocketChannel.open();
//调整通道为非阻塞模式
channel.configureBlocking(false);
//向选择器注册一个通道
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Приведенный выше код является простейшей версией регистрации канала в селекторе.Каналы, поддерживающие регистрацию селекторов, имеют метод register, который используется для регистрации текущего канала экземпляра в указанном селекторе.
Первый параметр этого метода — целевой селектор, а второй параметр — собственно бинарная маска, которая указывает, какие события текущего канала интересуют текущий селектор. В типе перечисления предусмотрены следующие значения:
- int OP_READ = 1 << 0;
- int OP_WRITE = 1 << 2;
- int OP_CONNECT = 1 << 3;
- int OP_ACCEPT = 1 << 4;
Этот механизм использования бинарной маски для представления определенных состояний, с ним мы также столкнулись, когда говорили о структуре файлов классов виртуальной машины, он использует бинарный бит для описания состояния.
Метод register вернет экземпляр SelectionKey, представляющий связь между селектором и каналом. Вы можете вызвать его метод селектора, чтобы вернуть текущий связанный экземпляр селектора, или вызвать его метод канала, чтобы вернуть экземпляр канала в текущей ассоциации.
Кроме того, метод readyOps SelectionKey будет возвращать набор готовых событий среди событий в текущем канале, представляющем интерес для текущего выбора, и по-прежнему возвращает целочисленное значение, которое является двоичной маской.
Например:
int readySet = selectionKey.readyOps();
Если значение readySet равно 13, двоичное "0000 1101", считая от конца к началу, первый бит равен 1, третий бит равен 1 и четвертый бит равен 1, то считывается канал, связанный с селектором. и пиши готов. , соединение готово.
Поэтому, когда мы регистрируем канал в селекторе, мы можем прослушивать различные события канала через возвращаемый экземпляр SelectionKey.
Конечно, когда в селекторе зарегистрировано несколько каналов, мы не можем записывать экземпляры SelectionKey, возвращаемые при их регистрации один за другим, для прослушивания событий канала.Селектор должен иметь метод для возврата экземпляров SelectionKey, связанных со всеми успешно зарегистрированные каналы.
Set<SelectionKey> keys = selector.selectedKeys();
Метод selectedKeys возвращает коллекцию экземпляров SelectionKey для всех каналов, успешно зарегистрированных в селекторе. Через экземпляр SelectionKey этой коллекции мы можем получить готовность к событию всех каналов и выполнить соответствующие операции обработки.
Применим вышеизложенные теоретические знания на простом примере взаимодействия клиент-сервер:
Код сервера:
Фактический эффект от работы этой небольшой программы заключается в следующем.Клиент устанавливает запрос к серверу.Когда запрос будет полностью установлен,клиент проверит, есть ли данные, записанные на сервер, и задача сервера очень Просто Принять любой Запрос клиента подключается и записывает для него часть данных.
Не смотрите на весь процесс очень простой, но пока у вас есть небольшая неопределенность, вы не можете реализовать эту функцию.Если вы мне не верите, попробуйте, особенно клиентский код с селекторами, который стоит проанализировать построчно. Напоминаем, что вам следует уделять больше внимания тому, какие методы являются блокирующими, а какие неблокирующими, что поможет вам проанализировать код.
На самом деле это самая простая модель запроса сервер-клиент. Понимание этого поможет вам понять принципы работы браузеров и веб-серверов. Вы также можете оставить мне сообщение, и давайте учиться и обсуждать вместе.
Предположительно вы также можете обнаружить, что код с селектором намного сложнее, и он не обязательно эффективнее исходного кода, это на самом деле потому, что ваша функция относительно проста и не требует большого количества обработки каналов. логика сложна, селектор Польза для вас будет очень очевидна.
На самом деле в NIO тоже есть часть AIO, то есть асинхронный IO не вводится, потому что асинхронный IO включает в себя множество других аспектов знаний, которые здесь пока вводить не будем.В последующих статьях будут представлены асинхронные задачи и другой связанный контент отдельно.
Весь код, изображения, файлы в статье хранятся в облаке на моем GitHub:
(https://github.com/SingleYam/overview_java)
Добро пожаловать в публичный аккаунт WeChat: Gorky on the code, все статьи будут синхронизированы в публичном аккаунте.