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

时间:2016-09-29 22:10:57

标签: java file sockets tcp transfer

尽管研究了这个问题几个小时,但我的进展甚微。根据我的教授的说法,代码应该按照书面形式工作......

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

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

另外奇怪的是,一个简单的.txt文本文件从未成功传输。我相信原因是在服务器端,因为它保持打开而不是客户端,每次都会启动。

服务器代码:

<div id="Ticket">
  <div id="Left">
    <div class="test1 Look"></div>
    <div class="test1 Look"></div>
  </div>
  <div id="Right">
    <div class="test2 Look"></div>
  </div>
</div>

客户代码:

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

缓冲读卡器是否会干扰输出流?有更好的方法来传输文件吗?

1 个答案:

答案 0 :(得分:1)

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

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

read()方法没有义务填充byteArray - 只返回某些并告诉你它读取了多少字节。从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消耗了什么。

作为一般规则,一旦将缓冲代码插入某些内容,您必须小心从该缓冲区中读取 。在这种情况下,它很难,因为你无法从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)将填充它 - 它不会。可以假设任何和所有读取操作都会返回与系统必须处理的数据一样多的数据:在这种情况下,它可能会在获得第一个或第二个数据包后立即返回,并使用未充满的数组。这与 C Unix 读取行为相同。

试试这个 - 我反复读取和写入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文件,您会注意到其中一些整数应该变长以应对数字&gt; = 2 ^ 31。

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