核心原理
通过 Socket 传输文件,本质上就是将文件从一台计算机(客户端)的内存中读取出来,然后通过 Socket 连接,以字节流的形式发送到另一台计算机(服务器),服务器再将接收到的字节流写入到自己的文件系统中。

整个过程可以分解为以下几个步骤:
-
建立连接:
- 服务器创建一个
ServerSocket,在指定端口上监听客户端的连接请求。 - 客户端创建一个
Socket,并指定服务器的 IP 地址和端口号,向服务器发起连接。 - 连接成功后,客户端和服务器各自会得到一个
Socket对象,通过这个对象上的InputStream和OutputStream进行双向通信。
- 服务器创建一个
-
客户端发送文件:
- 客户端获取一个
FileInputStream来读取本地文件。 - 通过
Socket的getOutputStream()获取输出流。 - 将
FileInputStream中的数据,分批写入到Socket的输出流中。 - 关键点:在发送文件数据之前,通常需要先发送文件的元数据,例如文件名和文件大小,这样服务器才能知道要创建一个多大的文件,以及用什么名字保存。
- 客户端获取一个
-
服务器接收文件:
(图片来源网络,侵删)- 服务器通过
Socket的getInputStream()获取输入流。 - 先读取客户端发来的文件名和文件大小。
- 根据文件名创建一个
FileOutputStream用于写入文件。 - 从
Socket的输入流中循环读取数据,并写入到FileOutputStream中,直到读取到预定大小的数据为止。
- 服务器通过
-
关闭资源:
- 传输完成后,务必按照“后开先关”的原则,依次关闭所有打开的流和 Socket 连接,以释放系统资源,顺序通常是:
FileOutputStream->Socket的OutputStream->FileInputStream->Socket的InputStream->Socket本身。
- 传输完成后,务必按照“后开先关”的原则,依次关闭所有打开的流和 Socket 连接,以释放系统资源,顺序通常是:
代码实现
下面我们分客户端和服务器两部分来编写代码。
服务器端代码
服务器负责监听、接收文件信息、接收文件数据并保存。
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; // 服务器监听端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,等待客户端连接...");
// accept() 是一个阻塞方法,会一直等待直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 接收文件名
DataInputStream dis = new DataInputStream(clientSocket.getInputStream());
String fileName = dis.readUTF();
System.out.println("准备接收文件: " + fileName);
// 接收文件大小
long fileSize = dis.readLong();
System.out.println("文件大小: " + fileSize + " 字节");
// 创建文件输出流
File receivedFile = new File("received_" + fileName);
try (FileOutputStream fos = new FileOutputStream(receivedFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
byte[] buffer = new byte[4096]; // 4KB的缓冲区
long remainingBytes = fileSize;
int bytesRead;
// 循环读取文件数据,直到所有字节都接收完毕
while (remainingBytes > 0 && (bytesRead = dis.read(buffer, 0, (int) Math.min(buffer.length, remainingBytes))) != -1) {
bos.write(buffer, 0, bytesRead);
remainingBytes -= bytesRead;
// 可选:打印接收进度
// System.out.printf("接收进度: %.2f%%%n", (1 - (double) remainingBytes / fileSize) * 100);
}
System.out.println("文件接收完成!");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码
客户端负责连接服务器、读取本地文件、发送文件信息、发送文件数据。
FileClient.java
import java.io.*;
import java.net.Socket;
public class FileClient {
public static void main(String[] args) {
String serverAddress = "127.0.0.1"; // 服务器IP地址,本地回环地址
int port = 12345; // 服务器端口
String filePath = "C:\\path\\to\\your\\file.txt"; // 要发送的文件路径,请替换为你的实际文件路径
File fileToSend = new File(filePath);
if (!fileToSend.exists()) {
System.out.println("文件不存在: " + filePath);
return;
}
try (Socket socket = new Socket(serverAddress, port);
FileInputStream fis = new FileInputStream(fileToSend);
BufferedInputStream bis = new BufferedInputStream(fis);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream())) {
System.out.println("已连接到服务器: " + serverAddress);
// 1. 发送文件名
dos.writeUTF(fileToSend.getName());
System.out.println("已发送文件名: " + fileToSend.getName());
// 2. 发送文件大小
long fileSize = fileToSend.length();
dos.writeLong(fileSize);
System.out.println("已发送文件大小: " + fileSize + " 字节");
// 3. 发送文件内容
byte[] buffer = new byte[4096]; // 4KB的缓冲区
int bytesRead;
long totalBytesSent = 0;
while ((bytesRead = bis.read(buffer)) != -1) {
dos.write(buffer, 0, bytesRead);
totalBytesSent += bytesRead;
// 可选:打印发送进度
// System.out.printf("发送进度: %.2f%%%n", (double) totalBytesSent / fileSize * 100);
}
System.out.println("文件发送完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
如何运行
- 准备文件:在客户端代码
FileClient.java中,修改filePath变量为你电脑上一个真实文件的路径,D:\\test\\my_photo.jpg。 - 编译代码:确保你的 Java 环境已配置好,在包含这两个文件的目录下打开终端,运行
javac FileServer.java FileClient.java进行编译。 - 启动服务器:首先运行服务器端程序。
java FileServer
你会看到控制台输出 "服务器已启动,等待客户端连接..."。
- 启动客户端:在另一个终端窗口中,运行客户端程序。
java FileClient
- 观察结果:
- 客户端会显示连接成功、发送文件名和文件大小的信息。
- 服务器端会显示客户端已连接、准备接收文件以及接收完成的信息。
- 文件传输完成后,你会在服务器端程序的运行目录下找到一个名为
received_原文件名的新文件,这就是成功接收到的文件。
最佳实践与改进建议
- 使用缓冲流:如代码所示,
BufferedInputStream和BufferedOutputStream能显著提高文件读写性能,因为它们减少了底层 I/O 操作的次数。 - 发送文件元数据:在发送文件内容前,先发送文件名和大小是至关重要的,这能让接收方正确地创建文件并判断传输是否完整。
- 使用
try-with-resources:这个语法能自动关闭实现了AutoCloseable接口(如所有 I/O 流和 Socket)的资源,避免了因忘记关闭close()而导致的资源泄漏,是现代 Java 编程的推荐做法。 - 增加校验机制:为了确保文件在传输过程中没有损坏,可以增加校验机制。
- 简单方法:在发送文件前,计算文件的 MD5 或 SHA-1 哈希值,并先发送给服务器,服务器接收完文件后,再计算接收到的文件的哈希值进行比对。
- 复杂方法:使用
CheckedInputStream和CheckedOutputStream,它们可以在流中自动计算校验和(如 Adler-32)。
- 处理大文件:对于非常大的文件(如几个 GB),直接使用
long fileSize是没问题的,因为 Java 的long类型可以表示很大的数值,但要注意,dis.read(buffer, 0, ...)的第二个参数int限制了单次读取的最大字节数,我们的代码Math.min(buffer.length, remainingBytes)已经处理了这个问题,确保不会溢出。 - 增强用户体验:在客户端和服务器端加入进度条,能让用户更直观地了解传输状态,代码中已注释掉了相关逻辑,你可以根据需要取消注释并实现。
- 多线程处理:上面的例子是单线程的,一次只能处理一个客户端,如果需要同时为多个客户端服务,可以在服务器的主循环中使用多线程或线程池。
// 服务器端多线程改进示例
while (true) {
Socket clientSocket = serverSocket.accept();
// 为每个客户端连接创建一个新线程来处理
new Thread(new ClientHandler(clientSocket)).start();
}
// ClientHandler 类需要实现 Runnable 接口,包含处理单个客户端连接的所有逻辑
class ClientHandler implements Runnable {
// ... 实现 run() 方法,将原 FileServer main 方法中的逻辑移到这里 ...
}
这个指南为你提供了一个健壮且可扩展的 Java Socket 文件传输基础,你可以根据具体需求进行进一步的定制和优化。
