?

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

( 17 comments — Leave a comment )
cleam
Jun. 25th, 2010 02:08 pm (UTC)
Неплохо бы ещё закрыть потоки в finally. И вспомнить, что сам close() может выкинуть Exception, отловить его и залогировать:


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


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

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

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 редко, а когда случается - в общем объеме логов не видно.
magicprinc
Jun. 28th, 2010 07:16 am (UTC)
В любом случае неясно в общем случае, что делать если там случится exception...

В редких случаях, когда можно ситуацию исправить - предлагается делать это вручную, а не с помощью "тихого" универсального метода.
skipy_ru
Jun. 28th, 2010 09:02 am (UTC)
Ну вот тут я не согласен. Не я открывал этот поток, не мне его закрывать. В смысле, открывал-то его, может, и я, но не в этом фрагменте кода. У меня вычитывание из потока может быть оформлено в виде отдельного метода, в который передается входной поток. И закрывать его там - моветон.

output stream, который открыл я, закрыть надо - это согласен. Сейчас добавлю.
yakov_sirotkin
Jun. 25th, 2010 02:14 pm (UTC)
Ещё можно обойтись без 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);


Для потока байт немного по-другому.
kefirfromperm
Jun. 25th, 2010 08:22 pm (UTC)
Тоже так пишу, меньше строк кода получается.
magicprinc
Jun. 28th, 2010 07:53 am (UTC)
Раз пошла такая пьянка мой вариант:

/** Используется в методах 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)
Jul. 11th, 2010 03:31 pm (UTC)
А почему бы не использовать apache commons-io ?
Я конечно понимаю, что в образовательных целях надо все самому прочитать.
Но в боевых условиях, имхо лучше использовать уже вылизанный библиотечный код.
Все мы люди и нам свойственно ошибаться/опечатываться, особенно когда надо
"по быстрому" что то из файла прочитать.
skipy_ru
Jul. 12th, 2010 07:42 am (UTC)
Потому что:

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

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

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

Спешка нужна при ловле блох. До того момента, как этот код попадет в боевое приложение, он будет неоднократно протестирован. Если нет - ну, сами себе злобные Буратины.
(Anonymous)
Jan. 6th, 2011 12:03 pm (UTC)
gaitletwile
Хороший у вас блог! удачи в развитии
skipy_ru
Jan. 7th, 2011 11:11 am (UTC)
Re: gaitletwile
Спасибо! Стараюсь по мере возможностей...
medl
Apr. 2nd, 2012 03:30 pm (UTC)
Нашел ваш пост из 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;
}

Помочь, увы, никто пока не может :\
skipy_ru
Apr. 2nd, 2012 06:59 pm (UTC)
Могу только предположить.

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

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

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

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

на счет еще одного вопроса - разобрался, ошибку понял. Нужно было закрывать клиентский сокет
skipy_ru
Apr. 3rd, 2012 01:24 pm (UTC)
Совершенно верно, пока сокет не закрыли - на другой стороне нет причин предполагать, что данные закончились. И -1 не вернется, а блокирующее чтение будет ждать.
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[] ... Читаем так же.
( 17 comments — Leave a comment )