?

Log in

Previous Entry | Next Entry

Я достаточно часто сталкиваюсь с проблемами, вызваными неправильным чтением данных из потока (java.io.InputStream). В последнее время такие проблемы почему-то стали появляться особенно часто, в связи с чем я решил разъяснить принцип раз и навсегда и просто давать всем желающим ссылку.

Чаще всего чтение производят с помощью метода int read(). Этот вариант, безусловно, имеет право на существование, если знать, как дальше быть с возвращаемым результатом (который int, а вовсе не byte). Мне лично ближе чтение блоками. Оно как-то понятнее и избавляет от всех вопросов преобразования значения.

Итак, код:

// входной поток, получается откуда-то извне
InputStream is; 
// буфер для чтения, разумного объема
byte[] buffer = new byte[32768];
// Выходной поток, ByteArrayOutputStream используется только для примера
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// цикл чтения
while (true){
    // читаем данные в буфер
    int readBytesCount = is.read(buffer);
    if (readBytesCount == -1) {
        // данные закончились
        break;
    }
    if (readBytesCount > 0) {
        // данные были считаны - есть, что записать
        baos.write(buffer, 0, readBytesCount);
    }
}
baos.flush();
baos.close();
byte[] data = baos.toByteArray();
Разбираем, что к чему.

На входе у нас есть поток. Безразлично, откуда он взялся. Мы из него читаем. Что делается дальше:


  1. Выделяется буфер разумного размера. В этом примере - 32Кб, в принципе, размер зависит только от объема читаемых данных (если надо прочитать 100 байт, выделять 1Мб будет неразумно)

  2. Дальше в примере создается ByteArrayOutputStream. Это просто пример выходного потока, если у вас есть, куда писать данные - этот шаг необязателен. :)

  3. Дальше начинается цикл чтения. Он бесконечный, выход реализован внутри. В цикле мы делаем следующее:

    1. Читаем данные в буфер с помощью метода is.read(byte[] buffer). В этом методе будет прочитано от 0 до buffer.length байт. Это надо запомнить на всю жизнь - никто не гарантирует вычитывания всего буфера, даже если данных достаточно! Реальное количество прочитанных байтов возвращается из метода как значение.

    2. Если реальное количество прочитанных байтов равно -1, это означает, что данные закончились. На это можно полагаться. В этом случае мы прерываем выполнение цикла чтения.

    3. Если прочитано больше нуля байтов (а может быть и ноль!) - мы записываем данные с помощью метода класса OutputStream write(byte[] buffer, int position, int length). Обратите внимание - поскольку буфер может быть заполнен при последнем чтении не полностью, необходимо указывать, сколько байтов из него надо взять.

    4. Продолжаем выполнение цикла.


  4. Сбрасываем остаток данных в поток и закрываем его

  5. Получаем данные в виде массива байтов

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

Еще я видел вариант, основанный на методе класса InputStream - int available(). Он возвращает количество байтов, доступных для чтения без блокировки. Это в теории. На практике - он реализован не во всех потоках, а реализация по умолчанию возвращает 0. Потому лично я его никогда не использую.

Описанный метод является универсальным при чтении блоками. В частности, он работает при чтении из символьных потоков (java.io.Reader), вместо типа byte при этом используется char.

Это всё. Всем спасибо!

С уважением,
Евгений aka Skipy



P.S. Комментарии? Дополнения?

P.P.S. Напоследок хочу напомнить. Читать из потока byte, преобразовывать его в char простым приведением типа и пытаться построить из полученных "символов" строку - грубая ошибка. Это будет работать для латинских символов (да и то не во всех случаях!), а для нелатинских может работать лишь по счастливой случайности. Правильный вариант - сделать на основе InputStream экземпляр Reader, с указанием кодировки (new InputStreamReader(inputStream, "<имя кодировки>")), и читать уже из него. О кодировках можно прочитать тут: http://www.skipy.ru/technics/encodings.html

Comments

medl
Apr. 4th, 2012 11:54 am (UTC)
и все же у меня будет к Вам еще один вопрос.

А что делать, если нужно дважды записать что-то в один сокет?
byte[] transf = new byte[600];
Arrays.fill(transf, (byte)50);

out.write(transf);
out.flush();

out.write(transf);
out.flush();

на входе получается ситуация:
b 1200
c 0

или это уже проблема архитектуры? И нужно организовать вопрос-ответ?
skipy_ru
Apr. 5th, 2012 07:53 am (UTC)
Когда Вы пишете байты, нигде не остается информации, как именно Вы их писали. Хоть два по 600, хоть один по 1200, хоть 200 по 6. Считывается весь блок, который доступен, либо столько, сколько поместится в буфер (если доступно больше). Если нужно понимать размер блока и считывать именно его - реализуйте протокол. Простейший - длина-данные-длина-данные-длина-данные. Если вместо длины 0 - всё, данные кончились. Пишем - int-byte[]-int-byte[]-int-byte[] ... Читаем так же.