nio java что это
Java IO против NIO
Узнайте о библиотеках ввода-вывода Java и NIO и о том, чем они отличаются.
1. Обзор
Обработка ввода и вывода – обычные задачи для Java-программистов. В этом уроке мы рассмотрим оригинал java.io ( IO ) библиотеки и более новые java.nio ( NIO ) библиотеки и чем они отличаются при общении по сети.
2. Основные характеристики
Давайте начнем с рассмотрения ключевых функций обоих пакетов.
2.1. ЭТА java.эта
2.2. NIO – java.nio
Теперь давайте посмотрим, как мы используем каждый из этих пакетов, когда отправляем данные на сервер или читаем его ответ.
3. Настройте Наш Тестовый Сервер
Здесь мы будем использовать WireMock для имитации другого сервера, чтобы мы могли запускать наши тесты независимо.
Мы настроим его так, чтобы он прослушивал наши запросы и отправлял нам ответы точно так же, как это сделал бы настоящий веб-сервер. Мы также будем использовать динамический порт, чтобы не конфликтовать с какими-либо службами на нашей локальной машине.
Давайте добавим зависимость Maven для WireMock с тестом областью действия:
В тестовом классе давайте определим правило JUnit @ для запуска WireMock на свободном порту. Затем мы настроим его так, чтобы он возвращал нам ответ HTTP 200, когда мы запрашиваем предопределенный ресурс, с текстом сообщения в виде некоторого текста в формате JSON:
Теперь, когда мы настроили наш макет сервера, мы готовы провести некоторые тесты.
4. Блокировка ввода – вывода- java.io
Давайте посмотрим, как работает оригинальная модель ввода-вывода с блокировкой, прочитав некоторые данные с веб-сайта. Мы будем использовать java.net.Socket для получения доступа к одному из портов операционной системы.
4.1. Отправить запрос
Для обычной связи по протоколу HTTP или HTTPS порт будет равен 80 или 443. Однако в этом случае мы используем wireMockRule.port() для доступа к динамическому порту, который мы настроили ранее.
4.2. Дождитесь ответа
Давайте откроем Входной поток в сокете для доступа к ответу, прочитаем поток с помощью BufferedReader и сохраним его в StringBuilder :
5. Неблокирующий ввод – вывод-java.nio
Теперь давайте посмотрим, как неблокирующая модель ввода-вывода nio пакета работает с тем же примером.
5.1. Отправить запрос
Во-первых, давайте откроем наш SocketChannel :
А теперь давайте получим стандартную кодировку UTF-8 | для кодирования и написания нашего сообщения:
5.2. Прочитайте ответ
После отправки запроса мы можем прочитать ответ в неблокирующем режиме, используя необработанные буферы.
Поскольку мы будем обрабатывать текст, нам понадобится ByteBuffer для необработанных байтов и Буфер символов для преобразованных символов (с помощью кодировщика ):
В нашем буфере останется свободное место, если данные будут отправлены в многобайтовом наборе символов.
При работе с буферами нам нужно знать, насколько велик буфер (емкость), где мы находимся в буфере (текущая позиция), и как далеко мы можем зайти (предел).
Когда в нашем буфере не останется места, потому что мы еще не обработали все его данные, то SocketChannel.read() вернет ноль прочитанных байтов, но наш buffer.position() все равно будет больше нуля.
Поскольку наши данные могут поступать по частям, давайте заключим наш код чтения буфера в цикл с условиями завершения, чтобы проверить, подключен ли наш сокет по-прежнему или мы были отключены, но в нашем буфере все еще остались данные:
И давайте не забудем закрыть() наш сокет (если только мы не открыли его в блоке “попытка с ресурсами”):
5.3. Хранение Данных Из Нашего Буфера
Ответ от сервера будет содержать заголовки, из-за которых объем данных может превысить размер нашего буфера. Итак, мы будем использовать StringBuilder для создания нашего полного сообщения по мере его поступления.
6. Заключение
Напротив, библиотеки java.nio обеспечивают неблокирующую связь с использованием буфера s и Канала s и могут обеспечивать прямой доступ к памяти для повышения производительности. Однако с такой скоростью возникает дополнительная сложность обработки буферов.
NIO: между Сциллой и Харибдой?
Одним из широко освещаемых свойств фреймворка java.NIO является неблокируемость, что означает способность к параллельному выполнению операций ввода-вывода и вычислений. Если приложение, запросившее чтение файла, имеет вычислительную задачу, которую можно обработать до получения данных из файла, то становится возможным одновременное выполнение этих операций. В случае отложенной записи, возможностей для параллелизма еще больше, так как при записи, в отличие от чтения, приложение не ожидает поступления данных.
Примечание. Хотелось бы озаглавить эту статью «NIO: производительность
или и совместимость?», но в силу известных ограничений приходится цитировать имена этих древних греческих старух.
Фактором успеха здесь является аппаратная поддержка. Контроллеры mass storage устройств, такие как SATA AHCI (Advanced Host Controller Interface) и NVMe (Non-Volatile Memory Interface for PCI Express) способны обрабатывать достаточно длинные последовательности операций ввода-вывода и перемещать данные между оперативной памятью и накопителем в режиме bus-master, без участия программы, выполняемой центральным процессором.
Рис.1 Список команд, формируемый драйвером AHCI в оперативной памяти и аппаратно интерпретируемый контроллером, может содержать до 32 дескрипторов операций ввода-вывода. Иллюстрация из документа AHCI Specification
Противоречия и решения
Здесь мы подходим к еще одной, менее очевидной, но при этом очень важной характеристике фреймворка NIO, в основе которого два взаимно-противоречивых критерия:
Java Native Interface
Одним из альтернативных решений является сопряжение Java-классов и библиотек, написанных на C или ассемблере. Здесь нельзя не упомянуть нативные классы, реализующие интерфейс JNI (Java Native Interface), основанный на классических конвенциях вызова, стандартизуемых для каждой операционной системы и дополняемых механизмом, обеспечивающим взаимодействие JVM и нативного кода. Кстати, овладев технологией JNI, можно получить доступ к указателям, отсутствие которых в Java, иногда доставляет неудобство.
В то же время, применение такого радикального метода как интеграция нативного кода, нивелирует кросс-платформенные преимущества Java, резко повышает трудоемкость разработки приложений и вероятность ошибок. Поддержка нескольких платформ, для JNI-решения, неизбежно будет означать fork-конструкции с необходимостью написания и сопровождения набора библиотек, количество которых равно количеству поддерживаемых систем.
Надо признать, есть ситуации, когда применение JNI необходимо. Например, поддержка некоторых специальных устройств, таких как аппаратный генератор случайных чисел.
Как оказалось, высокопроизводительный код можно разработать и на Java, если оптимально спроектировать систему абстракций, инкапсулирующих аппаратные ресурсы платформы.
Рассмотрим операцию чтения файла с диска. Очевидно наличие двух участников процесса: источник данных (накопитель или файл) и получатель (буфер в оперативной памяти). Все сказанное ниже, справедливо и для записи файла на диск, отличается только направление передачи информации, источник и получатель меняются местами.
Для пользовательских приложений, накопитель или файл представлен функциями ОС API дискового ввода-вывода. Не будем рассматривать возможность прямого программирования регистров контроллера дисков пользовательским приложением, канувшую в прошлое со времен MS-DOS, из очевидных соображений совместимости и безопасности.
Буфер, это заданный диапазон памяти в адресном пространстве приложения. Если быть совсем нудным точным, то в ряде высокоуровневых платформ, буфер-получатель может физически размещаться в кэш-памяти центрального процессора, но с соблюдением классических правил кэширования, не теряя ассоциации с заданным диапазоном адресов ОЗУ.
Итак, рассматриваемые объекты аппаратной платформы, это:
Конечно, подобный инструмент не может быть реализован без низкоуровневого аппаратно-зависимого программирования. Мы говорим лишь о том, что применение этого инструмента избавляет прикладного программиста от такой необходимости.
Утилита NIOBench
Утилита NIOBench, разработанная IC Book Labs и предназначенная для измерения производительности mass storage подсистемы, иллюстрирует сказанное, используя каналы и буферы при выполнении файловых операций.
Рис.2 Утилита NIOBench, вывод результатов измерения скорости чтения, записи и копирования файлов на жестком диске ноутбука ASUS N750JK (при обработке данных используется медиана и среднее арифметическое)
Рис.3 Текстовый рапорт утилиты NIOBench с детальным протоколированием результатов: очевидно влияние кэширования
Методы ввода-вывода, в частности операции чтения, записи и копирования файлов, являющиеся объектом бенчмарок, основаны на следующих архитектурных элементах.
Резюме
Концепция каналов и буферов, лежащая в основе технологии NIO, точно соответствует архитектуре подсистем хранения данных, основная функциональность которой сводится к перемещению информации между оперативной памятью (буферами) и разнообразными накопителями (каналами).
Вместе с тем, никакого чуда не произошло, и низкоуровневая работа, от которой любой фреймворк освобождает прикладных программистов, всего лишь переносится на разработчиков фреймворка и программистов системных…
Русские Блоги
Java NIO: обзор NIO / Разница между IO и NIO
1. Несколько основных понятий в NIO
В NIO есть несколько ключевых понятий: канал (канал), буфер (буфер), селектор (селектор).
Здесь InputStream фактически предоставляет канал для чтения файлов.
Поэтому канал в NIO можно сравнить с потоком в традиционном IO, но следует отметить, что в традиционном IO поток является однонаправленным, например InputStream может выполнять только операции чтения, а OutputStream может выполнять только операции записи. Канал является двунаправленным и может использоваться как для операций чтения, так и для записи.
Вот одна из основных вещей в NIO: Селектор. Можно сказать, что это самая важная часть NIO. Роль Selector заключается в опросе каждого зарегистрированного канала. Как только он обнаружит, что произошло зарегистрированное событие канала, он получит событие и затем обработает его.
Например, посмотрите на следующий пример:
Используйте один поток для обработки Selector, а затем используйте метод Selector.select () для получения события прибытия. После получения события прибытия вы можете отвечать на эти события одно за другим.
2. Канал
Как упоминалось ранее, Channel очень похож на Stream в традиционном IO. Хотя они очень похожи, между ними есть большие различия. Основное отличие состоит в том, что канал является двунаправленным, и его можно читать или записывать через канал, тогда как поток может выполнять только односторонние операции и может только читать или через поток. написать;
Ниже перечислены наиболее часто используемые каналы:
Используя FileChannel, вы можете читать или записывать данные из файла, через SocketChannel использовать TCP для чтения и записи данных на оба конца сетевого подключения, а через ServerSocketChanel вы можете прослушивать TCP-соединения, инициированные клиентом, и создавать новые для каждого TCP-соединения. SocketChannel для чтения и записи данных, через DatagramChannel, протокол UDP для чтения и записи данных на оба конца сетевого подключения.
Вот пример записи данных в файл через FileChannel:
Приведенная выше программа запишет строку «java nio» в файл data.txt в каталоге проекта. Обратите внимание, что метод переворота буфера должен быть вызван перед вызовом метода записи канала, в противном случае содержимое не может быть записано правильно. В следующем посте я объясню, как использовать буфер.
Три. Буфер
Буфер, как следует из названия, буфер на самом деле является контейнером, является непрерывным массивом. Канал предоставляет канал для чтения данных из файлов и сетей, но чтение или запись данных должны проходить через буфер. В частности, посмотрите на следующую картину, чтобы понять:
\
Приведенная выше схема описывает процесс отправки данных от клиента на сервер, а затем сервер получает данные. Когда клиент отправляет данные, он должен сначала сохранить данные в буфере, а затем записать содержимое буфера в канал. Чтобы получить данные на стороне сервера, вы должны прочитать данные в буфер через канал, а затем взять данные из буфера для обработки.
В NIO Buffer является родительским классом верхнего уровня, который является абстрактным классом. Обычно используемые подклассы Buffer:
Если это для чтения и записи файлов, могут быть использованы вышеуказанные типы буфера. Но для чтения и записи по сети наиболее часто используется ByteBuffer.
Конкретное использование буфера и его ограничение, положение и емкость будут понятны в следующей статье.
Четыре. Селектор
Класс Selector является основным классом NIO. Селектор может определять, происходит ли событие на нескольких зарегистрированных каналах. Если событие происходит, событие получается, и соответствующая обработка ответа выполняется для каждого события. Таким образом, вы можете управлять несколькими каналами только одним потоком, то есть управлять несколькими соединениями. Это позволяет функциям считывать и записывать только тогда, когда в соединении есть реальные события чтения и записи, что значительно снижает нагрузку на систему и избавляет от необходимости создавать поток для каждого соединения, поддерживать несколько потоков и избегать Издержки, вызванные переключением контекста между несколькими потоками.
Ключевым классом, связанным с Selector, является SelectionKey, SelectionKey представляет событие прибытия, эти два класса составляют ключевую логику для сервера для обработки бизнеса.
1. Концепция
Во-вторых, главное отличие между NIO и IO
В следующей таблице приведены основные различия между Java IO и NIO:
1. Потоково-ориентированный и буферно-ориентированный
Первое большое различие между Java IO и NIO состоит в том, что IO ориентирован на поток, а NIO ориентирован на буфер. Java IO ориентирован на поток, что означает, что каждый раз, когда один или несколько байтов считываются из потока, пока все байты не прочитаны, они нигде не кэшируются. Кроме того, он не может перемещать данные в потоке вперед и назад. Если вам нужно переместить данные, прочитанные из потока, туда и обратно, вам необходимо сначала их кэшировать в буфер. Буферно-ориентированный подход в Java NIO немного отличается. Данные считываются в буфер, который они обрабатывают позже, и могут быть перемещены туда и обратно в буфере при необходимости. Это увеличивает гибкость в обработке. Однако вам также необходимо проверить, содержит ли буфер все данные, которые необходимо обработать. Кроме того, убедитесь, что при чтении большего количества данных в буфер не перезаписывайте необработанные данные в буфере.
2. Блокирующий и неблокирующий IO
Различные потоки ввода-вывода Java заблокированы. Это означает, что когда поток вызывает read () или write (), поток блокируется до тех пор, пока некоторые данные не будут прочитаны или данные не будут полностью записаны. В течение этого времени поток не может делать ничего другого. Неблокирующий режим Java NIO позволяет потоку отправлять запрос на чтение данных из канала, но он может получать только текущие доступные данные. Если в данный момент нет доступных данных, он не получит ничего, вместо того, чтобы держать поток заблокированным Пока данные не станут читаемыми, поток может продолжать делать другие вещи. То же самое верно для неблокирующих записей. Поток запрашивает запись некоторых данных в канал, но ему не нужно ждать, пока они будут полностью записаны. Этот поток может одновременно выполнять другие действия. Потоки обычно используют время простоя неблокирующего ввода-вывода для выполнения операций ввода-вывода на других каналах, поэтому один поток теперь может управлять несколькими входными и выходными каналами.
3. Селекторы
Селектор Java NIO позволяет одному потоку контролировать несколько входных каналов. Вы можете зарегистрировать несколько каналов, чтобы использовать селектор, а затем использовать отдельный поток для «выбора» каналов: уже есть входы, которые можно обрабатывать в этих каналах. Или выберите канал, который готов к записи. Этот механизм выбора позволяет одному потоку управлять несколькими каналами.
В-третьих, как NIO и IO влияют на дизайн приложений
Независимо от того, выбираете ли вы набор инструментов IO или NIO, это может повлиять на следующие аспекты разработки вашего приложения:
1. Вызов API для класса NIO или IO.
2. Обработка данных.
3. Количество потоков, используемых для обработки данных.
1. API вызов
Конечно, вызов API с использованием NIO выглядит иначе, чем при использовании IO, но это не удивительно, потому что он не только читает побайтово из InputStream, но данные должны быть прочитаны в буфер перед обработкой.
2. Обработка данных
Использование чистого дизайна NIO по сравнению с дизайном ввода-вывода также влияет на обработку данных.
В дизайне ввода-вывода мы читаем побайтные данные из InputStream или Reader. Предположим, вы имеете дело с потоком текстовых данных на основе строк, например:
Поток текстовых строк может быть обработан так:
Обратите внимание, что статус обработки определяется тем, как долго выполняется программа. Другими словами, как только возвращается метод reader.readLine (), вы знаете, что текстовая строка была прочитана, и readline () блокируется, пока не будет прочитана вся строка, что и является причиной. Вы также знаете, что эта строка содержит имена, аналогично, когда возвращается второй вызов readline (), вы знаете, что эта строка содержит age и т. Д. Как видите, этот обработчик запускается только при считывании новых данных и знает, что это за данные на каждом шаге. После того, как запущенный поток обработает некоторые прочитанные данные, поток не будет откатывать данные (большую часть времени). Следующая диаграмма также иллюстрирует этот принцип:
Реализация NIO будет отличаться, следующий простой пример
Обратите внимание на вторую строку, прочитайте байты из канала в ByteBuffer. Когда этот вызов метода возвращается, вы не знаете, находятся ли все необходимые данные в буфере. Что вы знаете, так это то, что буфер содержит несколько байтов, что немного усложняет обработку. Предположим, что после первого вызова чтения (буфера) данные, считанные в буфер, составляют только половину строки, например «Имя: An», можете ли вы обработать данные? Очевидно, нет, вам нужно подождать, пока вся строка данных не будет считана в кеш, до чего любая обработка данных не имеет смысла. Итак, как вы узнаете, содержит ли буфер достаточно данных для обработки? Хорошо, ты не знаешь. Найденный метод может только просматривать данные в буфере. В результате вы должны проверить данные в буфере несколько раз, прежде чем узнаете, что все данные находятся в буфере. Это не только неэффективно, но и может усложнить разработку программы. Например:
Метод bufferFull () должен отслеживать, сколько данных считывается в буфер, и возвращает true или false, в зависимости от того, заполнен ли буфер. Другими словами, если буфер готов к обработке, это означает, что буфер заполнен.
Метод bufferFull () сканирует буфер, но он должен оставаться в том же состоянии до вызова метода bufferFull (). Если нет, то следующие данные, считанные в буфер, могут быть не прочитаны в правильном месте. Это невозможно, но это еще одна проблема, требующая внимания.
Если буфер заполнен, его можно обработать. Если это не удовлетворяет вас и имеет смысл в вашем конкретном случае, вы можете обработать часть данных. Но во многих случаях это не так. На следующем рисунке показан «буфер данных готов к циклу»:
4. Резюме
NIO позволяет вам использовать только один (или несколько) отдельных потоков для управления несколькими каналами (сетевыми подключениями или файлами), но вы платите за то, что анализ данных может быть более сложным, чем чтение данных из потока блокировки.
Если вам нужно управлять десятками тысяч подключений, которые открыты одновременно, эти подключения одновременно отправляют только небольшой объем данных, например сервер чата. Сервер NIO может быть преимуществом. Точно так же, если вам нужно поддерживать множество открытых соединений с другими компьютерами, такими как P2P-сети, использование одного потока для управления всеми вашими исходящими соединениями может быть преимуществом. Схема конструкции нескольких соединений для одного потока показана ниже:
Java NIO: один поток для управления несколькими подключениями
Если у вас небольшое количество соединений, которые используют очень высокую пропускную способность и отправляют большие объемы данных одновременно, возможно, типичная реализация сервера ввода-вывода может подойти. Следующая диаграмма иллюстрирует типичную конструкцию сервера ввода-вывода:
Java IO: Типичное соединение IO-сервера с одним дизайном обрабатывается одним потоком.
Основные отличия Java IO и Java NIO
Когда я начал изучать стандартный ввод/вывод в Java, то первое время был немного шокирован обилием интерфейсов и классов пакета java.io.*, дополненных еще и целым перечнем специфических исключений.
Потратив изрядное количество часов на изучение и реализацию кучи разнообразных туториалов из Интернета, начал чувствовать себя уверенно и вздохнул с облегчением. Но в один прекрасный момент понял, что для меня все только начинается, так как существует еще и пакет java.nio.*, известный ещё под названием Java NIO или Java New IO. Вначале казалось, что это тоже самое, ну типа вид сбоку. Однако, как оказалось, есть существенные отличия, как в принципе работы, так и в решаемых с их помощью задачах.
Разобраться во всем этом мне здорово помогла статья Джакоба Дженкова (Jakob Jenkov) – “Java NIO vs. IO”. Ниже она приводиться в адаптированном виде.
Поспешу заметить, что статья не является руководством по использованию Java IO и Java NIO. Её цель – дать людям, начинающим изучать Java, возможность понять концептуальные отличия между двумя указанными инструментами организации ввода/вывода.
Основные отличия между Java IO и Java NIO
IO | NIO |
---|---|
Потокоориентированный | Буфер-ориентированный |
Блокирующий (синхронный) ввод/вывод | Неблокирующий (асинхронный) ввод/вывод |
Селекторы |
Потокоориентированный и буфер-ориентированный ввод/вывод
Основное отличие между двумя подходами к организации ввода/вывода в том, что Java IO является потокоориентированным, а Java NIO – буфер-ориентированным. Разберем подробней.
Потокоориентированный ввод/вывод подразумевает чтение/запись из потока/в поток одного или нескольких байт в единицу времени поочередно. Данная информация нигде не кэшируются. Таким образом, невозможно произвольно двигаться по потоку данных вперед или назад. Если вы хотите произвести подобные манипуляции, вам придется сначала кэшировать данные в буфере.
Подход, на котором основан Java NIO немного отличается. Данные считываются в буфер для последующей обработки. Вы можете двигаться по буферу вперед и назад. Это дает немного больше гибкости при обработке данных. В то же время, вам необходимо проверять содержит ли буфер необходимый для корректной обработки объем данных. Также необходимо следить, чтобы при чтении данных в буфер вы не уничтожили ещё не обработанные данные, находящиеся в буфере.
Блокирующий и неблокирующий ввод/вывод
Потоки ввода/вывода (streams) в Java IO являются блокирующими. Это значит, что когда в потоке выполнения (tread) вызывается read() или write() метод любого класса из пакета java.io.*, происходит блокировка до тех пор, пока данные не будут считаны или записаны. Поток выполнения в данный момент не может делать ничего другого.
Неблокирующий режим Java NIO позволяет запрашивать считанные данные из канала (channel) и получать только то, что доступно на данный момент, или вообще ничего, если доступных данных пока нет. Вместо того, чтобы оставаться заблокированным пока данные не станут доступными для считывания, поток выполнения может заняться чем-то другим.
Каналы – это логические (не физические) порталы, через которые осуществляется ввод/вывод данных, а буферы являются источниками или приёмниками этих переданных данных. При организации вывода, данные, которые вы хотите отправить, помещаются в буфер, а он передается в канал. При вводе, данные из канала помещаются в предоставленный вами буфер.
Каналы напоминают трубопроводы, по которым эффективно транспортируются данные между буферами байтов и сущностями по ту сторону каналов. Каналы – это шлюзы, которые позволяют получить доступ к сервисам ввода/вывода операционной системы с минимальными накладными расходами, а буферы – внутренние конечные точки этих шлюзов, используемые для передачи и приема данных.
Тоже самое справедливо и для неблокирующего вывода. Поток выполнения может запросить запись в канал некоторых данных, но не дожидаться при этом пока они не будут полностью записаны.
Таким образом неблокирующий режим Java NIO позволяет использовать один поток выполнения для решения нескольких задач вместо пустого прожигания времени на ожидание в заблокированном состояний. Наиболее частой практикой является использование сэкономленного времени работы потока выполнения на обслуживание операций ввода/вывода в другом или других каналах.
Селекторы
Селекторы в Java NIO позволяют одному потоку выполнения мониторить несколько каналов ввода. Вы можете зарегистрировать несколько каналов с селектором, а потом использовать один поток выполнения для обслуживания каналов, имеющих доступные для обработки данные, или для выбора каналов, готовых для записи.
Чтобы лучше понять концепцию и выгоду от применения селекторов, давайте абстрагируемся от программирования и представим себе железнодорожный вокзал. Вариант без селектора: есть три железнодорожных пути (каналы), на каждый из них в любой момент времени может прибыть поезд (данные из буфера), на каждом пути постоянно ожидает сотрудник вокзала (поток выполнения), задача которого – обслуживание прибывшего поезда. В результате трое сотрудников постоянно находятся на вокзале даже если там вообще нет поездов. Вариант с селектором: ситуация та же, но для каждой платформы есть индикатор, сигнализирующий сотруднику вокзала (поток выполнения) о прибытии поезда. Таким образом на вокзале достаточно присутствия одного сотрудника.
Влияние Java NIO и Java IO на дизайн приложения
Выбор между Java NIO и Java IO может на следующие аспекты дизайна вашего приложения:
1. API обращений к классам ввода/вывода;
2. Обработка данных;
3. Количество потоков выполнения, использованных для обработки данных.
API обращений к классам ввода/вывода
Естественно, использование Java NIO серьезно отличается от использования Java IO. Так как, вместо чтения данных байт за байтом с использованием, например InputStream, данные для начала должны быть считаны в буфер и браться для обработки уже оттуда.
Обработка данных
Обработка данных при использовании Java NIO тоже отличается.
Как уже упоминалось, при использовании Java IO вы читаете данные байт за байтом с InputStream или Reader. Представьте, что вы проводите считывание строк текстовой информации:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
Этот поток строк текста может обрабатываться следующим образом:
Обратите внимание, как состояние процесса обработки зависит от того, насколько далеко продвинулось выполнение программы. Когда первый метод readLine() возвращает результат выполнения, вы уверенны – целая строка текста была считана. Метод является блокирующим и действие блокировки продолжается до тех пор, пока вся строка не будет считана. Вы также четко понимаете, что данная строка содержит имя. Подобно этому, когда метод вызывается во второй раз, вы знаете, что в результате получите возраст.
Как вы видите, прогресс в выполнении программы достигается только тогда, когда доступны новые данные для чтения, и для каждого шага вы знаете что это за данные. Когда поток выполнения достигает прогресса в считывании определенной части данных, поток ввода (в большинстве случаев) уже не двигает данные назад. Данный принцип хорошо демонстрирует следующая схема:
Имплементация с использованием Java IO будет выглядеть несколько иначе:
Обратите внимание на вторую строчку кода, в которой происходит считывание байтов из канала в ByteBuffer. Когда возвращается результат выполнения данного метода, вы не можете быть уверенны, что все необходимые вам данные находятся внутри буфера. Все, что вам известно, так это то, что буфер содержит некоторые байты. Это немного усложняет процесс обработки.
Представьте, что после первого вызова метода read(buffer), в буфер было считано только половину строки. Например, “Name: An”. Сможете ли вы обработать такие данные? Наверное, что нет. Вам придется ждать пока, по крайней мере, одна полная строка текста не будет считана в буфер.
Так как же вам узнать, достаточно ли данных для корректной обработки содержит буфер? А никак. Единственный вариант узнать, это посмотреть на данные, содержащиеся внутри буфера. В результате вам придется по нескольку раз проверять данные в буфере, пока они не станут доступными для корректной обработки. Это неэффективно и может негативно сказаться на дизайне программы. Например:
Метод bufferFull() должен следить за тем, сколько данных считано в буфер и возвращать true или false, в зависимости от того, заполнен буфер или нет. Другими словами, если буфер готов к обработке, то он считается заполненным.
Также метод bufferFull() должен оставлять буфер в неизмененном состоянии, поскольку в противном случае следующая порция считанных данных может быть записана в неправильное место.
Если буфер заполнен, данные из него могут быть обработаны. Если он не заполнен вы все же будете иметь возможность обработать уже имеющиеся в нем данные, если это имеет смысл в вашем конкретном случае. В большинстве случаев – это бессмысленно.
Следующая схема демонстрирует процесс определения готовности данных в буфере для корректной обработки:
Итоги
Java NIO позволяет управлять несколькими каналами (сетевыми соединениями или файлами) используя минимальное число потоков выполнения. Однако ценой такого подхода является более сложный, чем при использовании блокирующих потоков, парсинг данных.
Если вам необходимо управлять тысячами открытых соединений одновременно, причем каждое из них передает лишь незначительный объем данных, выбор Java NIO для вашего приложения может дать преимущество. Дизайн такого типа схематически изображен на следующем рисунке:
Если вы имеете меньшее количество соединений, по которым передаются большие объемы данных, то лучшим выбором станет классический дизайн системы ввода/вывода:
Важно понимать, что Java NIO отнюдь не является заменой Java IO. Его стоит рассматривать как усовершенствование – инструмент, позволяющий значительно расширить возможности по организации ввода/вывода. Грамотное использование мощи обоих подходов позволит вам строить хорошие высокопроизводительные системы.
Стоит заметить, что с выходом версии Java 1.7 появился еще и Java NIO.2, но присущие ему новшества касаются, в первую очередь, работы с файловым вводом/выводом, поэтому выходят за рамки этой статьи.