【问题标题】:Why does Java read random amounts from a socket but not the whole message?为什么 Java 从套接字读取随机数量而不是整个消息?
【发布时间】:2011-05-27 13:54:02
【问题描述】:

我正在处理一个项目,并且对 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 中收到 7266304 字节的 7258144 字节

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

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

以上是由于你们中的许多人在下面指出的一个错误。

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

【问题讨论】:

  • 在这个问题上我已经把头撞在桌子上很长一段时间了......

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


【解决方案1】:

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

唯一的解决方法是逐字节读取第一行,而不使用任何缓冲的 classes 阅读器。据我所知,Java 不提供具有 readLine() 功能的无缓冲 InputStreamReader(不推荐使用的 DataInputStream.readLine() 除外,如下面的 cmets 所示),因此您必须自己做。我会通过读取单个字节,将它们放入 ByteArrayOutputStream 直到遇到 EOL,然后使用具有适当编码的 String 构造函数将生成的字节数组转换为 String。

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

更新

事实上,我现在正在做类似的事情,只是稍微复杂一点。它是一种应用程序协议,涉及交换一些可以很好地用 XML 表示的数据结构,但它们有时会附加二进制数据。我们通过在根 XML 中有两个属性来实现这一点:fragmentLength 和 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 解析器会为我们做这件事。在您的情况下,您可能对这两行没问题,但如果您稍后需要添加更多元数据,您可能也希望考虑使用 null 终止的 XML。

【讨论】:

  • 实际上是一个 BufferedReader。如果 MPEG2 标头类似于 JPEG,则二进制数据的第一位可能主要是文本
  • 我认为这个答案并不完全正确。请看我的回复:stackoverflow.com/questions/4478438/…
  • @Dan:我认为 Sergey 和你说的完全一样:“丢失”字节位于 BufferedReader 中,它们已被读取(并解码为字符),但会永远不会被消耗。
  • DataInputStream 有一个 readLine 方法。它已被弃用,因为它不能正确进行字符转换,但只要您的输入是 ASCII,它实际上就可以正常工作。您可以使用它读取标题行,然后使用 DataInputStream 的正常读取方法来获取二进制数据。请注意,DataInputStream 可以在内部进行一些缓冲(据我所知 - 它涉及 PushbackInputStream,这是未记录的),因此不要尝试使用底层流,只需使用 DataInputStream。
  • @Dan,也许是我经常遇到这个问题,因为我的工作涉及处理大量流数据。像创建一个缓冲的读取对象(在 C++ 中),从中读取登录名和密码,然后通过 fork() + exec() 将套接字描述符传递给另一个进程对我来说听起来像是一个常见的错误。
【解决方案2】:

这是你的问题。您使用的程序的前几行 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】:

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

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

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

    【讨论】:

      【解决方案4】:

      Sergei 关于数据在缓冲区中丢失的说法可能是正确的,但我不确定他的解释。 (BufferedReader 通常不会在其缓冲区中保存数据。他可能正在考虑 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:澄清(或试图澄清 :-} 二进制与字符串数据的处理。

      【讨论】:

      • 他正在接收一个视频文件,所以一直使用 BufferedReader 是不行的。只要有意义,就可以混合字符串和二进制数据。例如,HTTP 一直在执行此操作 - 一个文本标头,后跟(可能)二进制数据。他的技术本质上是相同的(只有非常简单的标题)。
      • @Sergey Tachenov:谢谢。我的意思是将问题描述为将二进制数据和字符串数据视为同一事物。混合它们很好,只要您将二进制数据视为二进制数据,将字符串数据视为字符串数据。我将编辑我的回复。
      • 关闭缓冲的写入器流会在关闭之前自动刷新流,因此不存在丢失数据的问题。
      • @Sanjay,它将它刷新到套接字中,它可能会或可能不会在关闭时实际发送它。因此链接到处理该问题的 SO_LINGER 选项。
      • @Sergey:啊,感谢您澄清上下文。我实际上是断章取义地阅读了该声明。
      猜你喜欢
      • 1970-01-01
      • 2021-11-08
      • 2015-01-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-14
      相关资源
      最近更新 更多