为什么Java从套接字中读取随机数量而不是整个消息?

时间:2010-12-18 14:23:40

标签: java sockets file-io file-upload tcpclient

我正在开发一个项目,并对Java套接字有疑问。可以找到的源文件here

以纯文本成功传输文件大小后,我需要传输二进制数据。 (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%的随机字节数。我使用的是基于TCP的Socket,  所以我不必担心低层传输错误。

收到的号码会发生变化,但总是非常接近结束     在文件GLADIATOR / VIDEO_TS / VTS_07_1.VOB中收到7258144字节的7266304字节

应用程序然后在阻塞读取中挂起。我很困惑。服务器正在发送正确的 文件大小并在Ruby中成功实现,但我无法使Java版本工作。

为什么我读取的字节数少于通过TCP套接字发送的字节数?

以上是因为你们许多人在下面指出的错误。

BufferedReader吃了我的socket输入的8Kb。可以找到正确的实现 Here

4 个答案:

答案 0 :(得分:4)

如果您的in是BufferedReader,那么您遇到了缓冲超过需要的常见问题。 BufferedReader的默认缓冲区大小为8192个字符,大约是您所期望的和您所获得的之间的差异。所以你缺少的数据是在BufferedReader的内部缓冲区内,转换为字符(我想知道为什么它没有因某种转换错误而中断)。

唯一的解决方法是在不使用任何缓冲的读取器的情况下逐字节读取第一行。据我所知,Java不提供具有readLine()功能的无缓冲的InputStreamReader(除了已弃用的DataInputStream.readLine(),如下面的注释所示),因此您必须自己完成。我会通过读取单个字节,将它们放入ByteArrayOutputStream直到遇到EOL,然后使用具有适当编码的String构造函数将结果字节数组转换为String来实现。

请注意,虽然您不能使用BufferedInputReader,但是从一开始就没有什么能阻止您使用BufferedInputStream,这将使逐字节读取更有效。

<强>更新

事实上,我现在正在做这样的事情,只是有点复杂。它是一个应用程序协议,涉及交换一些在XML中很好地表示的数据结构,但它们有时会附加二进制数据。我们通过在根XML中包含两个属性来实现它:fragmentLength和isLastFragment。第一个表示二进制数据的字节数跟随XML部分,isLastFragment是一个布尔属性,表示最后一个片段,因此读取方知道将不再有二进制数据。 XML以null结尾,因此我们不必处理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
        }

使用以null结尾的XML结果非常棒,因为我们可以在不更改传输协议的情况下添加其他属性和元素。在传输级别,我们也不必担心处理UTF-8,因为XML解析器会为我们做。在你的情况下,你可能对这两行很好,但如果你以后需要添加更多的元数据,你可能也希望考虑以null结尾的XML。

答案 1 :(得分:1)

谢尔盖可能对缓冲区内的数据丢失是正确的,但我不确定他的解释。 (BufferedReaders通常不会保留缓冲区内的数据。他可能会想到BufferedWriters的问题,如果底层流过早关闭,可能会丢失数据。) [没关系;我误解了谢尔盖的回答。其余部分是有效的AFAIK。]

我认为您有一个特定于您的应用程序的问题。在client code中,您开始阅读如下:

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读取的字节都将丢失。您的应用程序正在挂起,因为它认为服务器持有某些字节,但服务器没有它们。

解决方案不是从套接字进行逐字节读取(哎呀!你的CPU不好!),而是要一致地使用BufferedReader。或者,要使用二进制数据缓冲,请将BufferedReader更改为包装套接字InputStream的BufferedInputStream。

顺便说一句,TCP并不像许多人认为的那样可靠。例如,当服务器套接字关闭时,它可能会将写入的数据写入套接字,然后在套接字连接关闭时丢失。致电Socket.setSoLinger有助于防止此问题。

编辑:同样顺便说一句,你通过将字节和字符数据视为可以互换来玩火,就像你在下面所做的那样。如果数据确实是二进制的,则转换为String可能会破坏数据。也许你想写入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;

编辑2 :澄清(或试图澄清: - }处理二进制与字符串数据。

答案 2 :(得分:1)

这是你的问题。您使用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来正确处理字符串转换。然后在字符和二进制文件之间切换会更容易。

答案 3 :(得分:1)

您的基本问题是BufferedReader将读取尽可能多的数据并放入其缓冲区。它会根据您的要求为您提供数据。这是完成的全部要点,即减少对OS的调用次数。使用缓冲输入的唯一安全方法是在连接的整个生命周期中使用相同的缓冲区。

在您的情况下,您只使用缓冲区读取两行,但很可能已将8192个字节读入缓冲区。 (缓冲区的默认大小)假设前两行由32个字节组成,这留下8160等待你读取,但是你绕过缓冲区来执行socket上的read()直接导致8160字节你最终丢弃的缓冲区。 (你缺少的金额)

BTW:如果您检查缓冲读卡器的内容,您应该能够在调试器中看到这一点。