【问题标题】:Java TCP File Transfer Only Complete On First AttemptJava TCP 文件传输仅在第一次尝试时完成
【发布时间】:2016-09-29 22:10:56
【问题描述】:

尽管对这个问题进行了数小时的研究,但我几乎没有取得任何进展。根据我的教授的说法,代码应该可以正常工作......

我有一个保持打开状态的服务器和一个请求文件的客户端。一旦客户端收到文件,客户端就会关闭。

当我打开服务器时,我可以传输一个完整的 .jpg 图像文件。然后客户端关闭,而服务器保持打开状态。我启动另一个客户端并尝试传输相同的图像,并且只有一部分字节被传输/写入磁盘。文件传输仅对服务器传输的第一个文件完全成功!

更奇怪的是,一个简单的 .txt 文本文件从未成功传输。我相信原因在于服务器端,因为它保持打开状态,而不是客户端,每次都重新开始。

服务器代码:

import java.io.*;
import java.net.*;
import java.util.Arrays;

class ft_server {

    public static void main(String args[]) throws Exception {

        /*
         * Asks user for port number and listens on that port
         */
        BufferedReader portFromUser = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("Enter the port you'd like to use: ");
        int portNumber = Integer.valueOf(portFromUser.readLine());

        if (portNumber < 1 || portNumber > 65535) {
            System.out.println("Please choose a port number between 1 and 65535.");
            return;
        }
        portFromUser.close();


        ServerSocket listenSocket = new ServerSocket(portNumber);
        /*
         * Finished with user input
         */

        /*
         * Continuously listens for clients:
         */
        while (true) {
            Socket clientSocket = listenSocket.accept();
            BufferedReader inFromClient = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            DataOutputStream outToClient = new DataOutputStream(clientSocket.getOutputStream());

            String clientIP = clientSocket.getRemoteSocketAddress().toString();
            System.out.println("The client " + clientIP + " connected!");

            String clientMessage = inFromClient.readLine();
            System.out.println("The client requested file: " + clientMessage);

            // Get file. If doesn't exist, let's client know.
            // Otherwise informs client of file size.
            File myFile = new File(clientMessage);

            if (!myFile.exists()) {
                outToClient.writeBytes("File does not exist!\n");
                return;
            } else {
                outToClient.writeBytes(String.valueOf((int)myFile.length()) + "\n");
            }

            // Create array for storage of file bytes:
            byte[] byteArray = new byte[(int)myFile.length()];
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(myFile));

            // Read file into array:
            bis.read(byteArray, 0, byteArray.length);

            // Send the file:
            outToClient.write(byteArray, 0, byteArray.length);

            outToClient.close();
            clientSocket.close();
        }
    }
}

客户代码:

import java.io.*;
import java.net.*;

class ft_client {

    public static void main(String args[]) throws Exception {

        int byteSize = 2022386;
        int bytesRead;

        /*
         * Asks user for IP and port:
         */
        BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("Enter an IP address: ");
        String ipAddress = inFromUser.readLine();
        System.out.println("Enter a port: ");
        String port = inFromUser.readLine();

        Socket clientSocket;

        try {
            // Makes socket, port, and calls connect. Assumes it's TCP:
            clientSocket = new Socket(ipAddress, Integer.valueOf(port));
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return;
        }

        // Creates InputStream from server to get file size and other messages:
        BufferedReader inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

        // Anything written to this will be sent to the server:
        DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream());

        // Asks for a file name to download from the server:
        System.out.println("What file do you want?: ");
        String message = inFromUser.readLine();
        outToServer.writeBytes(message + "\n");
        inFromUser.close();

        // Listens for confirmation from server.
        // If the file exists, the file size is delivered here:
        String response = inFromServer.readLine();
        System.out.println("File size: " + response);
        if (response.equals("File does not exist!")) {
            return;
        }

        // Receives file from server:
        byteSize = (int) Integer.valueOf(response);
        byte[] byteArray = new byte[byteSize];
        InputStream is = clientSocket.getInputStream(); // calling clientSocket.getInputStream() twice???
        FileOutputStream fos = new FileOutputStream(message);
        BufferedOutputStream bos = new BufferedOutputStream(fos);       

        // Continuously writes the file to the disk until complete:
        int total = 0;
        while ((bytesRead = is.read(byteArray)) != -1) {
            bos.write(byteArray, 0, bytesRead);
            total += bytesRead;
        }

        bos.close();
        System.out.println("File downloaded (" + total + " bytes read)");

        clientSocket.close();
    }
}

缓冲读取器是否会干扰输出流?有没有更好的文件传输方式?

【问题讨论】:

  • 不要在同一个套接字上混合缓冲和非缓冲流和读取器。找到另一种发送文件名的方式,例如使用DataOutputStream.writeUTF(),并使用DataInputStream.readUTF() 阅读。
  • 我会试一试的。但是,混合它们有什么本质上的问题吗?
  • 缓冲阅读器,错误,缓冲。如果在您读取文件名时它已经存在,它会读取一些图像数据。
  • 混合流类型有点糟糕,是的。客户端发送带有DataOutput.writeBytes(s) 的文件名,它发送每个字符的底部字节;然后服务器使用系统的默认字符编码通过 byte->char 转换器 (InputStreamReader) 使用 BufferedReader.readLine() 读取它。只要你坚持使用纯 ASCII 就可以了,但如果你将 UTF-8 作为默认编码并且文件名中包含非 ASCII 字符,那么一切都会变得非常混乱。
  • @EJP 哦,天哪,我看不到您所说的缓冲区问题!一定要迟到了。不是每次都在服务器循环中重新创建所有 BufferedReader 吗?而且每次都重启客户端....?

标签: java file sockets tcp transfer


【解决方案1】:

值得检查一下,在您的服务器代码中,从文件 read() 调用返回什么值,所以:

int bytesRead = bis.read(byteArray, 0, byteArray.length);
System.out.println("File bytes read: " + bytesRead + " from file size: " + myFile.length());

read() 方法没有义务填充 byteArray - 只是返回 something 并告诉您它读取了多少字节。来自docs,它:

从此输入流中读取最多 len 个字节的数据到一个数组中 字节。如果 len 不为零,则该方法将阻塞,直到某个输入为 可用的;否则,不读取任何字节并返回 0。

您需要循环阅读。我会这样做(实际上,和你的客户一样!):

int n;
while ((n = bis.read(byteArray, 0, byteArray.length)) != -1) {
    // Send the chunk of n bytes
    outToClient.write(byteArray, 0, n);
}
bis.close();
outToClient.close();

或类似的东西。我也关闭了该文件:它会在 GC/finalize 时关闭,但这可能需要一段时间,同时您正在保持文件处于打开状态。

编辑

在这种情况下,您的图像读取的具体问题在于您的客户端代码。您阅读了代码顶部附近的文件大小:

    // Creates InputStream from server to get file size and other messages:
    BufferedReader inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

然后你再次访问客户端:

    InputStream is = clientSocket.getInputStream(); // calling clientSocket.getInputStream() twice???

正如您的评论所暗示的,这很糟糕!感谢@EJP 强调这一点!

这会导致缓冲区过度摄取的问题:BufferedReader 在其腹部消耗的字节数比您从中提取的要多,因此当您第二次访问 clientSocket 输入流时,读取指针已移动。您再也不会查看 BufferedReader 消耗了什么。

作为一般规则,一旦您将缓冲代码插入到某个东西上,您必须小心地从该缓冲区中读取only。在这种情况下,这很困难,因为您无法从 Reader 读取图像(原始二进制)数据,因为它将忙于将二进制值解释为字符并将它们读取为 UTF-8 或其他内容。

即使没有缓冲区,在同一个流上混合阅读器(面向文本)和二进制数据(DataStreams)也是一个小罪。 HTTP 和电子邮件就是这样做的,所以你是一个很好的伙伴,但他们通过非常严格的指定而侥幸逃脱。问题是,无论你是在阅读 Unix“LF”还是 Windows“CR/LF”行尾等,你都可以很容易地在每一端遇到本地/默认字符编码的问题。

在这种情况下,请尝试完全不使用 BufferedReaders,并尝试一直使用 DataInput/Output 流。尝试writeUTF(s)readUTF() 传输字符串数据。理想情况下,像这样创建它们:

    DataInputStream inFromServer = new DataInputStream (new BufferedInputStream(clientSocket.getInputStream()));

所以你仍然可以获得缓冲的好处。

编辑 2

所以看到新的客户端代码:

        byteSize = (int) Integer.valueOf(response);
        byte[] byteArray = new byte[byteSize];
        FileOutputStream fos = new FileOutputStream(message);

        int readBytes = inFromServer.read(byteArray);

        // Continuously writes the file to the disk until complete:
        int total = 0;
        for (int i=0; i<byteArray.length; i++) {
            fos.write(byteArray[i]);
            total++;
        }

        fos.close();

在这里,我们假设因为 byteArray 数组设置为正确的大小,inFromServer.read(byteArray) 将填充它 - 它不会。最好假设任何和所有读取操作都会返回与系统必须处理的数据一样多的数据:在这种情况下,它可能会在收到第一个或两个数据包时立即返回,并且数组填充不足。这与 CUnix 的读取行为相同。

试试这个 - 我反复读写一个 4K 缓冲区,直到达到字节数(通过对读取的返回值求和来确定):

        byteSize = (int) Integer.valueOf(response);
        byte[] byteArray = new byte[4096];
        FileOutputStream fos = new FileOutputStream(message);
        int total = 0;
        // Continuously writes the file to the disk until complete:
        while (total < byteSize && (readBytes = inFromServer.read(byteArray)) != -1) {
            fos.write(byteArray, 0, readBytes);
            total += readBytes;
        }
        fos.close();

变体是这样的 - 相同的东西,但一次一个字节。可能会清楚一点。它会很慢 - 所有这些读取和写入都会影响操作系统,但如果你在套接字/文件流周围放置一个 BufferedInputStream/BufferedOutputStream,它就会解决这个问题。我已经添加了它们:

    DataInputStream inFromServer = 
            new DataInputStream(new BufferedInputStream(clientSocket.getInputStream()));
    ...         
        byteSize = (int) Integer.valueOf(response);

        OutputStream fos = new BufferedOutputStream(FileOutputStream(message));
        int total = 0;
        int ch;
        // Continuously writes the file to the disk until complete:
        while (total < byteSize && (ch = inFromServer.read()) != -1) {
            fos.write(ch);
            total ++;
        }
        fos.close();

最后!最简单的答案就是这个。您的代码,但更改为:

        int readBytes = inFromServer.readFully(byteArray);

是的! 1990 年代 Javasoft 中的那些好人添加了一个 DataInput.readFully 方法,它可以满足您的需求! - 基本上包装了上面的代码。这是最简单的解决方案,并且可以说是最正确的方法:“尽可能使用现有的库”。 OTOH,这是最不具有教育意义的,而且你花在习惯这样的读/写上的时间不会从你的预期寿命中扣除!

事实上,readFully 方法有严重的局限性。试着把它指向一个 1GB 的文件,看看会发生什么(在你修复了顶部的数组大小之后):你会 a)耗尽内存,并且 b)希望当你摄取一个巨大的 blob 时,你可以至少将其假脱机到磁盘。如果您尝试 2.5G 文件,您会注意到其中一些整数应该变成长整数以处理 >= 2^31 的数字。

如果是我,我会做 4K 缓冲区。 (顺便说一句,我是在没有安装 Java 编译器的笔记本电脑上写的,所以我实际上并没有运行上面的代码!如果有任何困难,请回复。)

【讨论】:

  • 由于某种原因,bis.read从未返回 -1,所以程序只是在那里暂停。我只是取消了循环,因为它没有必要。
  • @NickH 添加了有关缓冲区的后续信息。不返回 -1 的事情......这很奇怪!我的意思是,一定要小心,我已经记不清有多少次我看到这个假设失败了,所以至少检查它返回的内容,如果它不是你所期望的,请提醒一下。
  • 我已将所有 BufferedReader 更改为 DataInput/OutputStreams。但现在我有一个不同的问题。我可以writeUTF(来自服务器,将文件长度作为字符串发送)和来自客户端的readUTFfirst 尝试中就好了。当循环第二次开始时,来自服务器的writeUTF 工作,但客户端中的readUTF 抛出:“java.io.UTFDataFormatException:字节0 附近的格式错误的输入”。流发生了一些事情。第一次通过它发送字符串就好了,第二次发生了其他事情。
  • 此外,我已将writeUTF 更改为writeInt,它第一次发送正确的文件大小。第二次,当从客户端调用readInt 时,它读取的不是正确的大小(大概是发送的),而是读取了一些非常大的 negative 数字。我不知道这是从哪里来的。在writeIntreadInt 之间的某个地方,正确的文件大小被更改为一个大的负整数,这就是客户端读取它的内容!
  • @NickH 嗯,不看代码很难说。听起来像是在某个地方你得到更多或更少的字节 - 可能来自图像内容,然后下一个大小消息变得不对齐。大的负数可能来自客户端试图读取错误的字节作为 int。请确保检查每个 read 返回值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多