将文件数据作为二进制流,通过 Socket 连接从一端(客户端)发送到另一端(服务器端)。

我们将分步实现一个完整的、可运行的文件传输程序,包括:
- 服务器端:监听连接,接收文件数据,并将其保存到本地。
- 客户端:连接服务器,读取本地文件,并将文件数据发送给服务器。
核心概念
在开始编码前,我们需要明确几个关键点:
- Socket 和 ServerSocket:
ServerSocket:服务器端使用,负责在指定端口上监听客户端的连接请求。Socket:客户端和服务器端一旦建立连接,都会得到一个Socket实例,用于后续的数据通信。
- 输入/输出流:
- 通信的本质是流,服务器端需要从
Socket获取一个输入流 (InputStream) 来读取客户端发送的数据。 - 客户端需要从
Socket获取一个输出流 (OutputStream) 来向服务器写入数据。
- 通信的本质是流,服务器端需要从
- 文件输入/输出流:
- 客户端需要从本地文件读取数据,使用
FileInputStream。 - 服务器端需要将接收到的数据写入本地文件,使用
FileOutputStream。
- 客户端需要从本地文件读取数据,使用
- 关键问题:如何知道文件传输完了?
- 这是最常见的问题,如果只是简单地将文件流通过 Socket 发送,服务器端会一直等待,不知道何时结束。
- 解决方案:在发送文件数据之前,先发送文件的元信息(如文件名、文件大小),服务器端先接收这些元信息,然后循环读取输入流,直到读取到的字节数等于文件大小时,才认为文件传输完成。
代码实现
我们将创建两个 Java 类:FileServer.java 和 FileClient.java。
项目结构
file-transfer-demo/
├── src/
│ ├── FileServer.java
│ └── FileClient.java
└── files/ (用于存放要传输的文件)
└── source.txt
步骤 1: 服务器端 (FileServer.java)
服务器的工作流程是:

- 创建
ServerSocket并在指定端口(12345)上监听。 - 使用
accept()方法阻塞,等待客户端连接。 - 客户端连接后,获取其
Socket和输入流。 - 先读取文件名和文件大小。
- 根据文件名创建
FileOutputStream。 - 循环读取输入流,将数据写入
FileOutputStream,直到读取的字节数等于文件大小。 - 关闭所有资源。
// FileServer.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class FileServer {
public static void main(String[] args) {
int port = 12345;
// 服务器端保存文件的目录
String saveDir = "received_files/";
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,监听端口 " + port + "...");
// 等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress());
// 获取输入流
DataInputStream dis = new DataInputStream(clientSocket.getInputStream());
// 1. 读取文件名
String fileName = dis.readUTF();
System.out.println("正在接收文件: " + fileName);
// 2. 读取文件大小
long fileSize = dis.readLong();
System.out.println("文件大小: " + fileSize + " bytes");
// 3. 创建文件输出流
File saveFile = new File(saveDir, fileName);
// 确保保存目录存在
if (!saveFile.getParentFile().exists()) {
saveFile.getParentFile().mkdirs();
}
FileOutputStream fos = new FileOutputStream(saveFile);
// 4. 开始接收文件内容
byte[] buffer = new byte[4096];
long bytesRead = 0;
int read;
while (bytesRead < fileSize && (read = dis.read(buffer, 0, (int) Math.min(buffer.length, fileSize - bytesRead))) != -1) {
fos.write(buffer, 0, read);
bytesRead += read;
// 打印进度
System.out.printf("接收进度: %.2f%%%n", (bytesRead * 100.0) / fileSize);
}
System.out.println("文件接收完成!");
// 关闭流
fos.close();
dis.close();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
步骤 2: 客户端 (FileClient.java)
客户端的工作流程是:
- 创建
Socket并连接到服务器的 IP 地址和端口。 - 获取输出流。
- 先发送文件名和文件大小。
- 读取本地文件(使用
FileInputStream)。 - 循环读取文件内容,并通过 Socket 的输出流发送出去。
- 关闭所有资源。
// FileClient.java
import java.io.*;
import java.net.Socket;
public class FileClient {
public static void main(String[] args) {
String serverAddress = "127.0.0.1"; // 本地回环地址
int port = 12345;
// 要发送的文件路径
String filePath = "files/source.txt";
try (Socket socket = new Socket(serverAddress, port)) {
System.out.println("已连接到服务器: " + serverAddress);
// 获取输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
// 1. 创建文件对象
File file = new File(filePath);
if (!file.exists()) {
System.out.println("文件不存在: " + filePath);
return;
}
// 2. 发送文件名
dos.writeUTF(file.getName());
System.out.println("已发送文件名: " + file.getName());
// 3. 发送文件大小
long fileSize = file.length();
dos.writeLong(fileSize);
System.out.println("已发送文件大小: " + fileSize + " bytes");
// 4. 发送文件内容
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[4096];
int read;
long totalSent = 0;
while ((read = fis.read(buffer)) != -1) {
dos.write(buffer, 0, read);
totalSent += read;
// 打印进度
System.out.printf("发送进度: %.2f%%%n", (totalSent * 100.0) / fileSize);
}
System.out.println("文件发送完成!");
// 刷新输出流,确保所有数据都被发送
dos.flush();
// 关闭流
fis.close();
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如何运行
-
准备文件:在
files目录下创建一个source.txt文件,并写入一些内容,"Hello, this is a test file for socket transfer."。 -
编译代码:在项目根目录下打开终端,执行
javac命令。javac src/FileServer.java src/FileClient.java
-
运行服务器:首先运行服务器端程序,它会启动并等待连接。
(图片来源网络,侵删)java -cp src FileServer
你会看到输出:
服务器已启动,监听端口 12345... -
运行客户端:在另一个新的终端窗口中,运行客户端程序。
java -cp src FileClient
你会看到客户端的输出:
已连接到服务器: 127.0.0.1 已发送文件名: source.txt 已发送文件大小: 42 bytes 发送进度: 100.00% 文件发送完成! -
查看服务器输出:回到服务器端的终端,你会看到它接收文件的过程:
客户端已连接: /127.0.0.1 正在接收文件: source.txt 文件大小: 42 bytes 接收进度: 100.00% 文件接收完成! -
验证结果:检查你的项目目录,会发现多了一个
received_files文件夹,里面有一个source.txt文件,其内容与原始文件完全相同。
进阶与优化
上面的例子是一个基础版本,在实际应用中,你可能需要考虑以下优化:
使用 Buffered 流提高性能
频繁的 I/O 操作(尤其是网络 I/O)性能开销较大,使用缓冲流可以显著减少 I/O 次数,提高效率。
-
客户端优化:
// 将 FileInputStream 包装在 BufferedInputStream 中 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); // ... while ((read = bis.read(buffer)) != -1) { dos.write(buffer, 0, read); } bis.close(); -
服务器端优化:
// 将 FileOutputStream 包装在 BufferedOutputStream 中 BufferedOutputStream bos = new BufferedOutputStream(fos); // ... while (bytesRead < fileSize && (read = dis.read(buffer, 0, (int) Math.min(buffer.length, fileSize - bytesRead))) != -1) { bos.write(buffer, 0, read); bytesRead += read; } bos.close(); // 关闭时会自动刷新并关闭底层的 fos
处理大文件和内存问题
对于非常大的文件(如 GB 级别),一次性将文件读入内存是不现实的,上面的代码已经通过循环读取和写入解决了这个问题,因为它使用了一个固定大小的 byte[] 缓冲区,而不是一次性读取整个文件。
更健壮的错误处理和资源管理
- 使用
try-with-resources:上面的代码已经使用了try-with-resources语句,它能自动实现Closeable接口的资源的关闭,非常推荐使用,可以避免资源泄漏。 - 异常处理:可以添加更细致的异常处理,例如处理
SocketTimeoutException(如果设置了超时)、FileNotFoundException等。
文件校验(确保文件完整性)
文件在网络传输过程中可能会出错,为了确保接收到的文件和发送的文件完全一致,可以在传输前后计算文件的校验值(如 MD5、SHA-1)。
- 客户端:在发送文件前,计算源文件的 MD5 值,并将其作为元数据发送给服务器。
- 服务器:在接收完文件后,计算接收文件的 MD5 值,并与客户端发送过来的 MD5 值进行比较,如果一致,则说明文件传输无误。
// 伪代码:计算MD5
import java.security.MessageDigest;
// ...
public static String getMD5(File file) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[8192];
int read;
while ((read = fis.read(buffer)) != -1) {
md.update(buffer, 0, read);
}
}
byte[] digest = md.digest();
// 将 byte[] 转换为 16 进制字符串
// ...
}
// 客户端发送
dos.writeUTF(getMD5(file));
// 服务器端接收和校验
String receivedMD5 = dis.readUTF();
String calculatedMD5 = getMD5(saveFile);
if (!receivedMD5.equals(calculatedMD5)) {
System.err.println("文件校验失败,文件可能已损坏!");
}
多线程处理并发请求
上面的服务器一次只能处理一个客户端的请求,要实现一个可以同时处理多个客户端的服务器,你需要将 accept() 之后的所有处理逻辑(接收文件)放到一个单独的线程中。
// 在 FileServer.java 的 main 方法中修改
while (true) {
Socket clientSocket = serverSocket.accept();
// 为每个客户端连接创建一个新线程来处理
new Thread(() -> {
try {
// ... 原来的文件接收代码 ...
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
希望这份详细的指南能帮助你理解并实现 Java Socket 文件传输!
