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 一旦它获得第一个数据包或二,带有未填充的阵列。这与 C 和 Unix 读取行为相同。
试试这个 - 我反复读写 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 编译器的笔记本电脑上写这篇文章的,所以我实际上没有 运行 以上内容!如果有任何困难,请回复。)
尽管我研究了这个问题几个小时,但进展甚微。据我的教授说,代码应该像写的那样工作...
我有一个保持打开状态的服务器和一个请求文件的客户端。客户端收到文件后,客户端关闭。
当我打开服务器时,我可以传输一个完整的.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 一旦它获得第一个数据包或二,带有未填充的阵列。这与 C 和 Unix 读取行为相同。
试试这个 - 我反复读写 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 编译器的笔记本电脑上写这篇文章的,所以我实际上没有 运行 以上内容!如果有任何困难,请回复。)