Java TCP 文件传输仅在第一次尝试时完成

Java TCP File Transfer Only Complete On First Attempt

尽管我研究了这个问题几个小时,但进展甚微。据我的教授说,代码应该像写的那样工作...

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

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

更奇怪的是,一个简单的 .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();
    }
}

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

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

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

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

Reads up to len bytes of data from this input stream into an array of bytes. If len is not zero, the method blocks until some input is available; otherwise, no bytes are read and 0 is returned.

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

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 消耗了什么。

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

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

在这种情况下,请尝试完全不使用 BufferedReader,并尝试一直使用 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) 将填充它 - 它不会。最好假设任何和所有读取操作都会 return 您与系统必须处理的数据一样多:在这种情况下,它可能会 return 一旦它获得第一个数据包或二,带有未填充的阵列。这与 CUnix 读取行为相同。

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

        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();

一个变体是这个 - 同样的东西,但一次一个字节。可能会更清楚一点。它会很慢 - 所有这些读取和写入都达到了 OS,但是如果你在 socket/file 流周围放置一个 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 方法,它可以满足您的需求! - 基本上包装了上面的代码。这是最简单的解决方案,可以说是最正确的方法:"use existing libraries where possible"。 OTOH,这是最不教育,你花在read/writes这样的习惯上的时间不会从你的预期寿命中扣除!

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

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