【问题标题】:Sending large data over TCP/IP socket通过 TCP/IP 套接字发送大数据
【发布时间】:2020-12-03 05:42:24
【问题描述】:

我有一个小项目,在C# 中运行一个服务器,在Java 中运行一个客户端。服务器将图像发送到客户端。 有些图像非常大(有时高达 10MiB),所以我将图像字节拆分并以 32768 bytes 的块发送。 我的 C# 服务器代码如下:

using (var stream = new MemoryStream(ImageData))
{
   for (int j = 1; j <= dataSplitParameters.NumberOfChunks; j++)
   {
      byte[] chunk;
      if (j == dataSplitParameters.NumberOfChunks)
         chunk = new byte[dataSplitParameters.FinalChunkSize];
      else
         chunk = new byte[dataSplitParameters.ChunkSize];

      int result = stream.Read(chunk, 0, chunk.Length);

      string line = DateTime.Now + ", Status OK, " + ImageName+ ", ImageChunk, " + j + ", " + dataSplitParameters.NumberOfChunks + ", " + chunk.Length;

      //write read params
      streamWriter.WriteLine(line);
      streamWriter.Flush();
      
      //write the data
      binaryWriter.Write(chunk);
      binaryWriter.Flush();
      Console.WriteLine(line);

      string deliveryReport = streamReader.ReadLine();
      Console.WriteLine(deliveryReport);
     }
  }

而我的Java客户端代码如下:

long dataRead = 0;
for (int j = 1; j <= numberOfChunks; j++) {
    String line = bufferedReader.readLine();
    tokens = line.split(", ");
    System.out.println(line);

    int toRead = Integer.parseInt(tokens[tokens.length - 1]);
    byte[] chunk = new byte[toRead];
    int read = inputStream.read(chunk, 0, toRead);
    //do something with the data
    dataRead += read;

    String progressReport = pageLabel + ", progress: " + dataRead + "/" + dataLength + " bytes.";
    bufferedOutputStream.write((progressReport + "\n").getBytes());
    bufferedOutputStream.flush();

    System.out.println(progressReport);
}

问题是当我运行代码时,客户端崩溃并显示正在读取虚假数据的错误,或者客户端和服务器都挂起。这是错误:

Document Page 1, progress: 49153/226604 bytes.
�9��%>�YI!��F�����h�
Exception in thread "main" java.lang.NumberFormatException: For input string: .....

我做错了什么?

【问题讨论】:

  • 调试您的应用程序。您的代码假定您可以拆分到达客户端的任何行。如果二进制数据包含换行符(0x0D 0x0A 或只是 0x0A)会怎样?
  • @f1sh 我不认为是这样,因为我有一个定义明确的协议:(1)服务器发送read params作为文本数据,客户端读取一行作为文本数据; (2) 服务器发送预定义大小的二进制数据,客户端读取预定义大小的二进制数据; (3)客户端发送文本数据行,服务器读取文本数据行。我只是想知道这个协议是如何崩溃的。
  • 您首先发送一个日期时间。你处理过吗?出于多种原因,一旦将原始流包装在 BufferedReader 中,就不要使用它。
  • 您是否曾经将line 打印到控制台?这将是开始调试过程的第一件事。

标签: java c# networking tcp large-data


【解决方案1】:

您的 Java 代码似乎使用了BufferedReader。它将数据读入自己的缓冲区,这意味着它在底层套接字输入流中不再可用 - 这是您的第一个问题。第二个问题是inputStream.read 的使用方式 - 不能保证读取您请求的所有字节,您必须在它周围放置一个循环。

这不是一个特别容易解决的问题。当您在同一流中混合二进制和文本数据时,很难将其读回。在 Java 中,有一个名为 DataInputStream 的类可以提供一点帮助 - 它有一个 readLine 方法来读取一行文本,还有一个方法来读取二进制数据:

DataInputStream dataInput = new DataInputStream(inputStream);

for (int j = 1; j <= numberOfChunks; j++) {
    String line = dataInput.readLine();
    ...
    byte[] chunk = new byte[toRead];
    int read = dataInput.readFully(chunk);
    ...
}

DataInputStream 有限制:readLine 方法已被弃用,因为它假定文本以 latin-1 编码,并且不允许您使用不同的文本编码。如果您想在这条路上走得更远,您需要创建一个自己的类来读取您的流格式。

有些图像非常大(有时高达 10MiB),所以我将图像字节拆分并以每个 32768 字节的块的形式发送。

你知道这完全没有必要对吧?将数兆字节的数据发送到 TCP 套接字并在接收端将所有数据流式传输是绝对没有问题的。

【讨论】:

    【解决方案2】:

    基本问题。

    将输入流包装到缓冲读取器后您必须停止访问输入流。该缓冲读取器是缓冲的,它将读取尽可能多的数据,它仅限于精确读取到下一个换行符并停在那里。

    java端的BufferedReader读取的远不止这些,所以已经消耗了一大堆图片数据,没有出路了。通过制作 BufferedReader,你使这项工作变得不可能,所以你不能这样做。

    根本问题。

    您只有一个 TCP/IP 连接。在此,您发送一些不相关的文本(页面、进度等),然后发送未知数量的图像数据,然后发送另一个不相关的进度更新。

    这从根本上被打破了。图像解析器怎么可能知道在发送图像的中途,您会收到一条状态更新行?文本也只是二进制数据,没有让客户端知道的神奇标识符:这个字节是图像数据的一部分,但是这个字节是一些在中间发送的带有进度信息的文本。

    简单的修复。

    你会认为简单的解决方法是……好吧,那就别那么做了!你为什么要发送这个进度?客户端完全有能力知道它读取了多少字节,发送它是没有意义的。只需.. 获取您的二进制数据。打开输出流。发送所有这些数据。在客户端,打开输入流,读取所有数据。不要涉及字符串。不要使用任何带有“与字符一起使用”的味道(所以,BufferedReader?不。BufferedInputStream 很好)。

    ...但是现在客户端不知道标题,也不知道总大小!

    所以制作一个有线协议。这可能是微不足道的。

    这是你的有线协议:

    1. 4 字节,大端:SizeOfName
    2. SizeOfName 字节数。 UTF-8 编码的文档标题。
    3. 4 字节,大端:SizeOfData
    4. SizeOfData 字节数。图片数据。

    如果您真的希望客户端能够呈现进度条并知道标题的话。如果不需要,不要这样做,直接发送字节,并通过关闭连接发出文件已完全发送的信号。

    这是一些示例 java 代码:

    try (InputStream in = ....) {
      int nameSize = readInt(in);
      byte[] nameBytes = in.readNBytes(nameSize);
      String name = new String(nameBytes, StandardCharsets.UTF_8);
      int dataSize = readInt(in);
      try (OutputStream out = 
        Files.newOutputStream(Paths.get("/Users/TriSky/image.png")) {
    
        byte[] buffer = new byte[65536];
        while (dataSize > 0) {
          int r = in.read(buffer);
          if (r == -1) throw new IOException("Early end-of-stream");
          out.write(buffer, 0, r);
          dataSize -= r;
        }
      }
    }
    
    public int readInt(InputStream in) throws IOException {
        byte[] b = in.readNBytes(4);
        return ByteBuffer.wrap(b).getInt();
    }
    

    结束语

    您的应用程序中的另一个错误是您使用了错误的方法。 Java 的“读取(字节)”方法不会(必然)完全填充该字节数组。所有 read(byte[]) 都会读取至少 1 个字节(除非流已关闭,否则它不会读取,并返回 -1。想法是:read 将读取 最佳 个字节字节:现在准备好给你多少。谁知道 - 如果你忽略 in.read(bytes) 的返回值,你的代码必然会被破坏,而你正在这样做。例如,您真正想要的是readNBytes,它保证它完全填充该字节数组(或直到流结束,以先发生者为准)。

    注意,在上面的传输代码中,我也使用了基本的读取,但是这里我并没有忽略返回值。

    【讨论】:

    • 很好的答案!你涵盖了各个方面。
    【解决方案3】:

    当您尝试发送图像时,您必须将图像作为普通文件打开,然后将图像子串成一些块,每个块将其更改为“base64encode”当您发送并且客户端解码它时,因为 图像数据不是普通数据,所以 base64encode 将此符号更改为像 这样的普通字符AfHM65Hkgf7MM

    【讨论】:

      猜你喜欢
      • 2012-10-13
      • 1970-01-01
      • 2013-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-14
      • 1970-01-01
      • 2016-02-17
      相关资源
      最近更新 更多