Рубрики
Без рубрики

Java IO против NIO

Узнайте о библиотеках ввода-вывода Java и NIO и о том, чем они отличаются.

Автор оригинала: baeldung.

1. Обзор

Обработка ввода и вывода – обычные задачи для Java-программистов. В этом уроке мы рассмотрим оригинал java.io ( IO ) библиотеки и более новые java.nio ( NIO ) библиотеки и чем они отличаются при общении по сети.

2. Основные характеристики

Давайте начнем с рассмотрения ключевых функций обоих пакетов.

2.1. ЭТА java.эта

В java.io пакет был представлен в Java 1.0 , с Читателем представлен в Java 1.1. Он обеспечивает:

  • Входной поток и Выходной поток – которые предоставляют данные по одному байту за раз
  • Считыватель и Писатель – удобные обертки для потоков
  • режим блокировки – для ожидания полного сообщения

2.2. NIO – java.nio

Пакет java.nio был представлен в Java 1.4 и обновлен в Java 1.7 (NIO.2) с улучшенными файловыми операциями и асинхронным каналом . Он обеспечивает:

  • Буфер для одновременного чтения фрагментов данных
  • Кодировщик символов – для сопоставления необработанных байтов с читаемыми символами/из них
  • Канал – для связи с внешним миром
  • Селектор – для включения мультиплексирования на Выбираемом канале и предоставления доступа к любому каналу , готовому для ввода-вывода
  • неблокирующий режим-читать все, что готово

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

3. Настройте Наш Тестовый Сервер

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

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

Давайте добавим зависимость Maven для WireMock с тестом областью действия:


    com.github.tomakehurst
    wiremock-jre8
    2.26.3
    test

В тестовом классе давайте определим правило JUnit @ для запуска WireMock на свободном порту. Затем мы настроим его так, чтобы он возвращал нам ответ HTTP 200, когда мы запрашиваем предопределенный ресурс, с текстом сообщения в виде некоторого текста в формате JSON:

@Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());

private String REQUESTED_RESOURCE = "/test.json";

@Before
public void setup() {
    stubFor(get(urlEqualTo(REQUESTED_RESOURCE))
      .willReturn(aResponse()
      .withStatus(200)
      .withBody("{ \"response\" : \"It worked!\" }")));
}

Теперь, когда мы настроили наш макет сервера, мы готовы провести некоторые тесты.

4. Блокировка ввода – вывода- java.io

Давайте посмотрим, как работает оригинальная модель ввода-вывода с блокировкой, прочитав некоторые данные с веб-сайта. Мы будем использовать java.net.Socket для получения доступа к одному из портов операционной системы.

4.1. Отправить запрос

В этом примере мы создадим запрос GET для получения наших ресурсов. Во-первых, давайте создадим Сокет для доступа к порту , который прослушивает наш сервер WireMock:

Socket socket = new Socket("localhost", wireMockRule.port())

Для обычной связи по протоколу HTTP или HTTPS порт будет равен 80 или 443. Однако в этом случае мы используем wireMockRule.port() для доступа к динамическому порту, который мы настроили ранее.

Теперь давайте откроем Выходной поток в сокете , завернутый в OutputStreamWriter и передадим его PrintWriter для написания нашего сообщения. И давайте убедимся, что мы очистили буфер, чтобы наш запрос был отправлен:

OutputStream clientOutput = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput));
writer.print("GET " + TEST_JSON + " HTTP/1.0\r\n\r\n");
writer.flush();

4.2. Дождитесь ответа

Давайте откроем Входной поток в сокете для доступа к ответу, прочитаем поток с помощью BufferedReader и сохраним его в StringBuilder :

InputStream serverInput = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));
StringBuilder ourStore = new StringBuilder();

Давайте используем reader.readLine() для блокировки, дождемся полной строки, а затем добавим строку в наш магазин. Мы будем продолжать читать, пока не получим null, , который указывает на конец потока:

for (String line; (line = reader.readLine()) != null;) {
   ourStore.append(line);
   ourStore.append(System.lineSeparator());
}

5. Неблокирующий ввод – вывод-java.nio

Теперь давайте посмотрим, как неблокирующая модель ввода-вывода nio пакета работает с тем же примером.

На этот раз мы создадим java.nio.канал . SocketChannel для доступа к порту на нашем сервере вместо java.net.Сокета и передайте ему Адрес inetsocket .

5.1. Отправить запрос

Во-первых, давайте откроем наш SocketChannel :

InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());
SocketChannel socketChannel = SocketChannel.open(address);

А теперь давайте получим стандартную кодировку UTF-8 | для кодирования и написания нашего сообщения:

Charset charset = StandardCharsets.UTF_8;
socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));

5.2. Прочитайте ответ

После отправки запроса мы можем прочитать ответ в неблокирующем режиме, используя необработанные буферы.

Поскольку мы будем обрабатывать текст, нам понадобится ByteBuffer для необработанных байтов и Буфер символов для преобразованных символов (с помощью кодировщика ):

ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
CharsetDecoder charsetDecoder = charset.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(8192);

В нашем буфере останется свободное место, если данные будут отправлены в многобайтовом наборе символов.

Обратите внимание, что если нам нужна особенно высокая производительность, мы можем создать MappedByteBuffer в собственной памяти, используя ByteBuffer.allocateDirect() . Однако в нашем случае использование allocate() из стандартной кучи достаточно быстро.

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

Итак, давайте прочитаем из нашего SocketChannel , передав его нашему ByteBuffer для хранения наших данных. Или чтение из SocketChannel завершится с нашей ByteBuffer текущей позицией, установленной на следующий байт для записи (сразу после последнего записанного байта), но с неизменным пределом :

socketChannel.read(byteBuffer)

Наш SocketChannel.read() возвращает количество прочитанных байтов , которые могут быть записаны в наш буфер. Это будет равно -1, если сокет был отключен.

Когда в нашем буфере не останется места, потому что мы еще не обработали все его данные, то SocketChannel.read() вернет ноль прочитанных байтов, но наш buffer.position() все равно будет больше нуля.

Чтобы убедиться, что мы начинаем чтение с нужного места в буфере, мы будем использовать Buffer.flip (), чтобы установить текущее положение нашего ByteBuffer равным нулю и ограничить его последним байтом, который был записан SocketChannel . Затем мы сохраним содержимое буфера, используя наш метод сохранения содержимого буфера , который мы рассмотрим позже. Наконец, мы будем использовать buffer.compact() для уплотнения буфера и установки текущей позиции, готовой для нашего следующего чтения из SocketChannel.

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

while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
    byteBuffer.flip();
    storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore);
    byteBuffer.compact();
}

И давайте не забудем закрыть() наш сокет (если только мы не открыли его в блоке “попытка с ресурсами”):

socketChannel.close();

5.3. Хранение Данных Из Нашего Буфера

Ответ от сервера будет содержать заголовки, из-за которых объем данных может превысить размер нашего буфера. Итак, мы будем использовать StringBuilder для создания нашего полного сообщения по мере его поступления.

Чтобы сохранить наше сообщение, мы сначала декодируем необработанные байты в символы в нашем буфере . Затем мы перевернем указатели, чтобы прочитать наши символьные данные, и добавим их в наш расширяемый StringBuilder. Наконец, мы очистим буфер символов , готовый к следующему циклу записи/чтения.

Итак, теперь давайте реализуем наш полный метод хранения содержимого буфера () , передающий наши буферы, Кодировщик символов и Строковый конструктор :

void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer, 
  CharsetDecoder charsetDecoder, StringBuilder ourStore) {
    charsetDecoder.decode(byteBuffer, charBuffer, true);
    charBuffer.flip();
    ourStore.append(charBuffer);
    charBuffer.clear();
}

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

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

Напротив, библиотеки java.nio обеспечивают неблокирующую связь с использованием буфера s и Канала s и могут обеспечивать прямой доступ к памяти для повышения производительности. Однако с такой скоростью возникает дополнительная сложность обработки буферов.

Как обычно, код для этой статьи доступен на GitHub .