Почему Java читает случайные суммы из сокета, но не все сообщение? - PullRequest
4 голосов
/ 18 декабря 2010

Я работаю над проектом и у меня есть вопрос о сокетах Java. Исходный файл, который можно найти здесь .

После успешной передачи размера файла в виде обычного текста мне нужно передать двоичные данные. (DVD. VOB файлы)

У меня есть цикл, такой как

                // Read this files size
                long fileSize = Integer.parseInt(in.readLine());

                // Read the block size they are going to use
                int blockSize = Integer.parseInt(in.readLine());
                byte[] buffer = new byte[blockSize];

                // Bytes "red"
                long bytesRead = 0;
                int read = 0;

                while(bytesRead < fileSize){
                System.out.println("received " + bytesRead + " bytes" + " of " + fileSize + " bytes in file " + fileName);
                read = socket.getInputStream().read(buffer);
                if(read < 0){
                    // Should never get here since we know how many bytes there are
                    System.out.println("DANGER WILL ROBINSON");
                    break;
                }
                binWriter.write(buffer,0,read);
                bytesRead += read;
            }

Я прочитал случайное количество байтов, близкое к 99%. Я использую Socket, основанный на TCP, поэтому мне не нужно беспокоиться об ошибках передачи нижнего уровня.

Полученный номер изменяется, но всегда очень близко к концу получил 7258144 байта из 7266304 байта в файле GLADIATOR / VIDEO_TS / VTS_07_1.VOB

Затем приложение зависает в блокирующем чтении. Я в замешательстве. Сервер отправляет правильный Размер файла и имеет успешную реализацию в Ruby, но я не могу заставить работать версию Java.

Зачем мне читать меньше байтов, чем отправлено через сокет TCP?

Это связано с ошибкой, которую многие из вас указали ниже.

BufferedReader съел 8Кб ввода моего сокета. Правильная реализация может быть найдена Здесь

Ответы [ 4 ]

4 голосов
/ 18 декабря 2010

Если ваш in является BufferedReader, то вы столкнулись с общей проблемой с буферизацией больше, чем нужно.Размер буфера по умолчанию для BufferedReader составляет 8192 символа, что примерно соответствует разнице между тем, что вы ожидали, и тем, что вы получили.Таким образом, данные, которые вам не хватает, находятся во внутреннем буфере BufferedReader, преобразованном в символы (мне интересно, почему он не сломался с какой-то ошибкой преобразования).-байт без использования каких-либо буферизованных классов читателей.Насколько я знаю, в Java нет небуферизованного InputStreamReader с возможностью readLine () (за исключением устаревшего DataInputStream.readLine (), как указано в комментариях ниже), поэтому вам придется делать это самостоятельно.Я сделал бы это, читая отдельные байты, помещая их в ByteArrayOutputStream до тех пор, пока не встретил EOL, а затем преобразовывая полученный байтовый массив в строку, используя конструктор String с соответствующей кодировкой.

Обратите внимание, что пока вы можете 'Если вы не используете BufferedInputReader, ничто не мешает вам использовать BufferedInputStream с самого начала, что сделает побитовое чтение более эффективным.

Обновление

ФактическиЯ делаю что-то подобное прямо сейчас, только немного сложнее.Это прикладной протокол, который включает в себя обмен некоторыми структурами данных, которые хорошо представлены в XML, но к ним иногда прикрепляются двоичные данные.Мы реализовали это, имея два атрибута в корневом XML: фрагментLength и isLastFragment.Первый указывает, сколько байтов двоичных данных следует за частью XML, а isLastFragment является логическим атрибутом, указывающим последний фрагмент, поэтому сторона чтения знает, что двоичных данных больше не будет.XML завершается нулем, поэтому нам не нужно иметь дело с readLine ().Код для чтения выглядит следующим образом:

    InputStream ins = new BufferedInputStream(socket.getInputStream());
    while (!finished) {
      ByteArrayOutputStream buf = new ByteArrayOutputStream();
      int b;
      while ((b = ins.read()) > 0) {
        buf.write(b);
      }
      if (b == -1)
        throw new EOFException("EOF while reading from socket");
      // b == 0
      Document xml = readXML(new ByteArrayInputStream(buf.toByteArray()));
      processAnswers(xml);
      Element root = xml.getDocumentElement();
      if (root.hasAttribute("fragmentLength")) {
        int length = DatatypeConverter.parseInt(
                root.getAttribute("fragmentLength"));
        boolean last = DatatypeConverter.parseBoolean(
                root.getAttribute("isLastFragment"));
        int read = 0;
        while (read < length) {
          // split incoming fragment into 4Kb blocks so we don't run 
          // out of memory if the client sent a really large fragment
          int l = Math.min(length - read, 4096);
          byte[] fragment = new byte[l];
          int pos = 0;
          while (pos < l) {
            int c = ins.read(fragment, pos, l - pos);
            if (c == -1)
              throw new EOFException(
                      "Preliminary EOF while reading fragment");
            pos += c;
            read += c;
          }
          // process fragment
        }

Использование XML с нулевым символом в конце для этого оказалось очень полезным, поскольку мы можем добавлять дополнительные атрибуты и элементы без изменения транспортного протокола.На транспортном уровне нам также не нужно беспокоиться об обработке UTF-8, потому что анализатор XML сделает это за нас.В вашем случае вам, вероятно, хорошо с этими двумя строками, но если вам нужно добавить больше метаданных позже, вы можете рассмотреть и XML с нулевым символом в конце.

1 голос
/ 18 декабря 2010

Ваша основная проблема заключается в том, что BufferedReader будет читать столько данных, сколько доступно, и помещать в свой буфер. Это даст вам данные, как вы просите об этом. В этом весь смысл буферизации, то есть уменьшения количества обращений к ОС. Единственный безопасный способ использовать вход с буферизацией - использовать один и тот же буфер в течение срока действия соединения.

В вашем случае вы используете буфер только для чтения двух строк, однако весьма вероятно, что 8192 байта было считано в буфер. (Размер буфера по умолчанию) Скажем, первые две строки состоят из 32 байтов, это оставляет 8160 в ожидании чтения, однако вы пропускаете буфер для выполнения read () в сокете, непосредственно приводя к оставшимся 8160 байтов буфер, который вы в итоге отбрасываете. (сумма, которую вам не хватает)

Кстати: вы должны увидеть это в отладчике, если осмотрите содержимое буферизованного ридера.

1 голос
/ 18 декабря 2010

Вот ваша проблема.Первые несколько строк программы вы используете in.readLine (), который, вероятно, является своего рода BufferedReader.BufferedReaders будет считывать данные с сокета в 8K кусках.Поэтому, когда вы сделали первый readLine (), он прочитал первые 8K в буфер.Первые 8K содержат ваши два числа, за которыми следуют символы новой строки, затем некоторая часть заголовка файла VOB (это недостающий фрагмент).Теперь, когда вы переключились на использование getInputStream () из сокета, вы получаете 8K передачи при условии, что вы начинаете с нуля.

socket.getInputStream().read(buffer);  // you can't do this without losing data.

Хотя BufferedReader удобен для чтения символьных данных, переключения между двоичными и символьными данными.в потоке не возможно с этим.Вам придется переключиться на использование InputStream вместо Reader и преобразовать первые несколько частей вручную в символьные данные.Если вы читаете файл с использованием буферизованного байтового массива, вы можете прочитать первый фрагмент, найти новые строки и преобразовать все слева от этого в символьные данные.Затем напишите все справа от вашего файла, затем начните читать остальную часть файла.

Раньше это было проще с DataInputStream, но он не выполняет хорошую работу по обработке символов для вас (readLine устарела, а BufferedReader - единственная замена - doh).Вероятно, следует написать замену DataInputStream, которая под крышками использует Charset для правильной обработки преобразования строк.Тогда переключение между символами и двоичным кодом будет проще.

1 голос
/ 18 декабря 2010

Сергей, возможно, был прав насчет потери данных внутри буфера, но я не уверен в его объяснении.(BufferedReaders обычно не хранит данные внутри своих буферов. Возможно, он думает о проблеме с BufferedWriters, которая может потерять данные, если основной поток преждевременно завершит работу.) [Не берите в голову;Я неправильно понял ответ Сергея.В остальном это действительно AFAIK.]

Я думаю, что у вас есть проблема, характерная для вашего приложения.В вашем коде клиента вы начинаете читать следующим образом:

public static void recv(Socket socket){
    try {
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        //...
        int numFiles = Integer.parseInt(in.readLine());

... и продолжаете использовать in для начала обмена.Но затем вы переключаетесь на использование необработанного потока сокетов:

            while(bytesRead > fileSize){
                read = socket.getInputStream().read(buffer);

Поскольку in является BufferedReader, он уже собирается заполнить свой буфер до 8192 байтов из входного потока сокета.Любые байты, которые находятся в этом буфере и которые вы не читаете из in, будут потеряны.Ваше приложение зависает, потому что считает, что сервер удерживает некоторые байты, но на сервере их нет.

Решение состоит в том, чтобы не выполнять побайтные чтения из сокета (ой!плохой процессор!), но использовать BufferedReader последовательно.Или, чтобы использовать буферизацию с двоичными данными, замените BufferedReader на BufferedInputStream, который обертывает InputStream сокета.

Кстати, TCP не так надежен, как полагают многие.Например, когда сокет сервера закрывается, он может записать данные в сокет, которые затем теряются при отключении соединения сокета.Вызов Socket.setSoLinger может помочь предотвратить эту проблему.

РЕДАКТИРОВАТЬ: Кстати, вы играете с огнем, обрабатывая данные байтов и символов, как если бы онивзаимозаменяемы, как вы делаете ниже.Если данные действительно являются двоичными, то преобразование в строку может привести к повреждению данных.Возможно, вы хотите записывать в BufferedOutputStream?

                // Java is retarded and reading and writing operate with
                // fundamentally different types. So we write a String of
                // binary data.
                fileWriter.write(new String(buffer));
                bytesRead += read;

EDIT 2 : уточнено (или попытались уточнить: -} обработка двоичных и строковых данных.

...