Евгений Матюшкин (skipy_ru) wrote,
Евгений Матюшкин
skipy_ru

Category:

Чтение данных из потока

Я достаточно часто сталкиваюсь с проблемами, вызваными неправильным чтением данных из потока (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
Tags: io, маленькие тонкости
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 17 comments
Неплохо бы ещё закрыть потоки в finally. И вспомнить, что сам close() может выкинуть Exception, отловить его и залогировать:


} finally {
   try {
       is.close();
   } catch(Exception e) {
       log.warn("Unable to close input stream", e)    
   }     
}


При этом для output stream'а close() ещё стоит вызвать в первом блока finally, чтобы быть уверенным, что ошибка закрытия (например, ошибка сброса буфера) не "проглотится".
Считаете надо это логировать?

Я убрал в конце концов:

public static void flushAndClose (@Nullable Closeable someStreamOrWriter) {
if (someStreamOrWriter != null) {
if (someStreamOrWriter instanceof Flushable) {//может УЖЕ быть закрыт => ожидаем IOException
try { ((Flushable) someStreamOrWriter).flush(); } catch (Throwable ignore) {}
}//i can flush

try {
someStreamOrWriter.close();
} catch (Throwable ignore) {}
}//i
}//flushAndClose


Случается тфу^3 редко, а когда случается - в общем объеме логов не видно.
В любом случае неясно в общем случае, что делать если там случится exception...

В редких случаях, когда можно ситуацию исправить - предлагается делать это вручную, а не с помощью "тихого" универсального метода.
Ну вот тут я не согласен. Не я открывал этот поток, не мне его закрывать. В смысле, открывал-то его, может, и я, но не в этом фрагменте кода. У меня вычитывание из потока может быть оформлено в виде отдельного метода, в который передается входной поток. И закрывать его там - моветон.

output stream, который открыл я, закрыть надо - это согласен. Сейчас добавлю.
Ещё можно обойтись без break:

BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(data), "UTF-8"));
StringBuilder b = new StringBuilder();
String s;
while ((s = in.readLine()) != null)
b.append(s);


Для потока байт немного по-другому.
Тоже так пишу, меньше строк кода получается.
Раз пошла такая пьянка мой вариант:

/** Используется в методах copy* для удобства чтения исходников т.к. copy принимает любой <0 как till eof. */
public static final int COPY_ALL_TILL_EOF = -1;
/** Скопировать всё до eof, после чего закрыть source stream/reader. */
public static final int COPY_ALL_CLOSE_SRC = -2;


public static long copy (@NotNull InputStream srcFrom, @NotNull OutputStream dstTo, final long maxCount) throws IOException {
long limit = maxCount >= 0 ? maxCount : Long.MAX_VALUE;//столько или меньше (сколько есть) any <0 до конца
long count = 0; //скопировано
int n; //считано на шаге (сейчас)
final byte[] buf = new byte[DEFAULT_BUFFER_SIZE];

while (limit > 0 && (n = srcFrom.read(buf, 0, limit >= DEFAULT_BUFFER_SIZE ? DEFAULT_BUFFER_SIZE : (int)limit)) != -1) {
dstTo.write(buf, 0, n);

count += n;//всего прочитано + считано сейчас
limit -= n;//осталось - считано сейчас
}//w

if (maxCount == COPY_ALL_CLOSE_SRC) { close(srcFrom); }

return count;
}//copy

Anonymous

July 11 2010, 15:31:01 UTC 9 years ago

А почему бы не использовать apache commons-io ?
Я конечно понимаю, что в образовательных целях надо все самому прочитать.
Но в боевых условиях, имхо лучше использовать уже вылизанный библиотечный код.
Все мы люди и нам свойственно ошибаться/опечатываться, особенно когда надо
"по быстрому" что то из файла прочитать.
Потому что:

Правильный код - 10 строк. Ради этого тащить за собой лишнюю библиотеку неразумно. Вообще, зависимость от сторонних библиотек - палка о двух концах. Никто не гарантирует, что код, во-первых, оптимальнее, чем тот, который напишу я, во-вторых, не содержит ошибок. commons-logging тоже использовали повсеместно. А когда затянули в веб-приложения - вот тут и накрыло проблемами с загрузчиком.

Я понимаю, что фреймворки объема spring писать самому неразумно. Но уж чтение из файла-то, простите...

> особенно когда надо "по быстрому" что то из файла прочитать.

Спешка нужна при ловле блох. До того момента, как этот код попадет в боевое приложение, он будет неоднократно протестирован. Если нет - ну, сами себе злобные Буратины.
Хороший у вас блог! удачи в развитии
Спасибо! Стараюсь по мере возможностей...
Нашел ваш пост из javatalks.ru и прошу вашей помощи
Все же вопрос про read(). В проекте нашел вот такой вот странный метод:

/**
* Reads the bytes, if any, from the socket.
* This is tailored for small reads which are not in the Transfer form such as the initial handshake.
*
* @param timeout how long to attempt reading the socket, in milliseconds, before giving up. If < 0, uses defaultTimeout instead.
* @return the bytes from the socket
* @throws Exception
*/
private byte[] oldSimpleReadSocket(int timeout) throws Exception
{
int messageSize = 0;
byte [] buff = null;
byte byte1 = 0;

if(timeout < 0)
timeout = this.defaultTimeout;

this.sock.setSoTimeout(timeout); //timeout is in millisecods

try
{
byte1 = (byte)this.istream.read(); //wait for the first byte
}
catch (SocketTimeoutException e)
{
throw new Exception ("The call to 'Simple' readSocket timed out waiting for the notification from the application.");
}

//Make sure there was actually something read from the socket
if (byte1 != -1)
{
messageSize = this.istream.available();
buff = new byte[messageSize+1];
buff[0] = byte1;

if(messageSize > 0)
this.istream.read(buff, 1, messageSize);
}

return buff;
}

Вначале считывается первый байт, а если уж что-то было, считывается все остальное, но с методом .available(); и созданием байтового массива. Документация говорит, что available() вообще в таких целях применять нельзя, но гугл показал, что во многих крупных проектах так делается :) я с дебаггером не могу найти отличия от такой отрефакториной мною версии:

private byte[] simpleReadSocket(int timeout) throws Exception
{
int messageSize = 0;
byte [] buff = null;

if(timeout < 0)
timeout = this.defaultTimeout;

this.sock.setSoTimeout(timeout); //timeout is in millisecods

try
{
messageSize = this.istream.available();
buff = new byte[messageSize];

if(messageSize > 0)
this.istream.read(buff, 0, messageSize);
}
catch (SocketTimeoutException e)
{
throw new Exception ("The call to 'Simple' readSocket timed out waiting for the notification from the application.");
}
return buff;
}

Помочь, увы, никто пока не может :\
Могу только предположить.

В первом случае сначала идет блокирующее чтение. Т.е. мы дожидаемся, когда в буфер в ОС свалится блок байтов, только тогда первый из них нам отдадут. И в этом случае available покажет, сколько там всего сложено.

А во втором случае available сразу может вернуть 0 - и до свиданья.

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

Но это только предположение, надо смотреть на исходник available в той версии, под которую этот писалось. Он скорее всего native.
Огромное спасибо за ответ! Я разобрался:
Метод read() блокирующий, а available() нет - вот и вся разгадка :)
На сайте оракла самая старшая версия документация 1.3.1 - метод available() не нативный
Сокет на той стороне блокирующий
Попытался запустить свой код, и получил ровно ту ситуацию, о которой Вы говорили - available() вернул 0. Под дебагом все отлично. Огромнейшее спасибо, разобрался, стало понятно. Джуниор стал чуть умнее :)

на счет еще одного вопроса - разобрался, ошибку понял. Нужно было закрывать клиентский сокет
Совершенно верно, пока сокет не закрыли - на другой стороне нет причин предполагать, что данные закончились. И -1 не вернется, а блокирующее чтение будет ждать.

medl

7 years ago

skipy_ru

7 years ago