核心原理
通过 Socket 传输文件的基本思想是:

- 建立连接:客户端作为发起方,连接到服务器端。
- 发送元数据:客户端首先向服务器发送文件的“元数据”,例如文件名和文件大小,服务器需要先接收这些信息,以便做好接收文件的准备(如创建文件、分配空间等)。
- 发送文件内容:客户端将文件内容以二进制流的形式,通过 Socket 的输出流发送出去。
- 接收文件内容:服务器通过 Socket 的输入流,以字节流的形式接收数据,并将其写入到本地文件中。
- 关闭连接:传输完成后,双方关闭 Socket 和相关的流资源。
关键点:
- 流式传输:文件被看作一个字节流,而不是一个整体,这样可以避免一次性将大文件加载到内存中,非常适合传输大文件。
- 元数据:必须先告知服务器要传什么文件以及文件有多大,否则服务器无法正确地保存文件。
- 资源管理:
Socket、InputStream、OutputStream等都是需要手动关闭的资源,最好使用try-finally或try-with-resources语句来确保它们被正确关闭。
代码实现
我们将创建两个类:FileClient.java(客户端)和 FileServer.java(服务器)。
服务器端代码 (FileServer.java)
服务器负责在指定端口监听连接,接收文件元数据,然后接收文件内容并保存到本地。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class FileServer {
private static final int PORT = 12345;
private static final String SAVE_DIR = "received_files/";
public static void main(String[] args) {
// 确保保存目录存在
new File(SAVE_DIR).mkdirs();
System.out.println("服务器启动,等待连接...");
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
// 循环监听,可以处理多个客户端连接
while (true) {
try (Socket clientSocket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(clientSocket.getInputStream())) {
System.out.println("客户端已连接: " + clientSocket.getInetAddress());
// 1. 接收文件名
String fileName = dataInputStream.readUTF();
System.out.println("正在接收文件: " + fileName);
// 2. 接收文件大小
long fileSize = dataInputStream.readLong();
System.out.println("文件大小: " + fileSize + " bytes");
// 3. 接收文件内容
File receivedFile = new File(SAVE_DIR + fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(receivedFile)) {
byte[] buffer = new byte[4096];
int bytesRead;
long totalBytesRead = 0;
// 循环读取,直到读取完所有字节
while (totalBytesRead < fileSize && (bytesRead = dataInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
System.out.println("文件接收完成!已保存至: " + receivedFile.getAbsolutePath());
}
} catch (IOException e) {
System.err.println("处理客户端请求时出错: " + e.getMessage());
}
}
} catch (IOException e) {
System.err.println("服务器启动或运行时出错: " + e.getMessage());
}
}
}
客户端代码 (FileClient.java)
客户端负责连接服务器,读取本地文件,并发送文件名和文件内容。

import java.io.*;
import java.net.Socket;
public class FileClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 12345;
public static void main(String[] args) {
// 要发送的文件路径
String filePath = "sample_file.txt";
File fileToSend = new File(filePath);
if (!fileToSend.exists()) {
System.err.println("错误: 文件 " + filePath + " 不存在!");
return;
}
try (
// 1. 创建Socket连接
Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
// 2. 准备文件输入流
FileInputStream fileInputStream = new FileInputStream(fileToSend);
// 3. 准备Socket输出流,并用DataOutputStream包装以发送基本数据类型
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream())
) {
System.out.println("已连接到服务器 " + SERVER_HOST + ":" + SERVER_PORT);
System.out.println("正在发送文件: " + fileToSend.getName());
// 4. 发送文件名 (UTF格式)
dataOutputStream.writeUTF(fileToSend.getName());
System.out.println("已发送文件名: " + fileToSend.getName());
// 5. 发送文件大小 (long类型)
long fileSize = fileToSend.length();
dataOutputStream.writeLong(fileSize);
System.out.println("已发送文件大小: " + fileSize + " bytes");
// 6. 发送文件内容
byte[] buffer = new byte[4096];
int bytesRead;
long totalBytesSent = 0;
// 循环读取并发送,直到文件末尾
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
dataOutputStream.write(buffer, 0, bytesRead);
totalBytesSent += bytesRead;
// 可选:打印进度
System.out.printf("发送进度: %.2f%%\n", (double) totalBytesSent / fileSize * 100);
}
// 刷新输出流,确保所有数据都被发送
dataOutputStream.flush();
System.out.println("文件发送完成!");
} catch (IOException e) {
System.err.println("发送文件时出错: " + e.getMessage());
}
}
}
关键点解析
-
流的选择和包装
FileInputStream/FileOutputStream: 用于读写本地文件的原始字节流。Socket.getInputStream()/Socket.getOutputStream(): 用于通过网络收发数据的原始字节流。DataInputStream/DataOutputStream: 这是关键,它们可以包装其他的InputStream/OutputStream,并提供方便的方法来读写 Java 的基本数据类型,如readUTF()/writeUTF()(用于字符串),readLong()/writeLong()(用于长整型,如文件大小),这使得发送元数据变得非常简单。
-
文件名和文件大小的发送顺序
- 必须先发送文件名,再发送文件大小,最后发送文件内容,服务器端的接收顺序必须与此完全一致。
- 如果先发送内容,服务器不知道要创建什么文件名,也不知道要接收多少字节,会导致数据错乱。
-
缓冲区
- 代码中使用了
byte[] buffer = new byte[4096];作为缓冲区,这是为了提高效率,网络I/O和文件I/O都是比较慢的操作,使用缓冲区可以减少I/O操作的次数,从而显著提升性能,4096是一个常见的缓冲区大小。
- 代码中使用了
-
进度显示
(图片来源网络,侵删)- 在客户端代码中,我们通过比较
totalBytesSent和fileSize来计算并打印发送进度,这对于大文件传输的用户体验很重要。
- 在客户端代码中,我们通过比较
-
try-with-resources语句- 代码中使用了
try (Socket socket = ...; InputStream is = ...)这种语法,它会自动在try块结束时关闭括号中声明的资源,只要这些资源实现了AutoCloseable接口(Socket,InputStream,OutputStream等都实现了),这可以防止资源泄漏,是现代Java推荐的写法。
- 代码中使用了
如何运行和测试
-
准备文件 在与
FileClient.java相同的目录下,创建一个名为sample_file.txt的文件,并在里面写入一些测试内容,"Hello, this is a test file for socket transfer."。 -
编译代码 打开终端或命令提示符,进入包含
.java文件的目录,运行:javac FileServer.java FileClient.java
-
启动服务器 在第一个终端窗口中运行服务器:
java FileServer
你会看到输出:
服务器启动,等待连接... -
启动客户端 在第二个终端窗口中运行客户端:
java FileClient
你会看到客户端的连接和发送进度信息,以及服务器端的接收信息。
-
验证结果 传输完成后,在服务器的运行目录下,你会发现多了一个
received_files文件夹,里面有一个sample_file.txt文件,打开它,内容应该与客户端发送的文件完全一致。
进阶与优化
-
并发处理 当前的服务器是单线程的,一次只能处理一个客户端,如果需要同时处理多个客户端,可以使用多线程,一个简单的改进是在
FileServer的accept()循环中,为每个客户端连接创建一个新的线程来处理。// 在 FileServer.java 的 main 方法中修改 while (true) { Socket clientSocket = serverSocket.accept(); // 为每个连接创建一个新线程 new Thread(() -> { // ... 将原来的 try-with-resources 块里的代码放在这里 ... }).start(); } -
传输校验 为了确保文件在传输过程中没有损坏,可以在发送前和接收后计算文件的 MD5 或 SHA-1 哈希值,并进行比较。
-
断点续传 更高级的功能是断点续传,这需要客户端和服务器记录已传输的字节数,客户端在连接后,可以先发送一个已传输的字节数,服务器从该位置继续写入文件。
-
使用 NIO 对于高性能、高并发的场景,可以使用 Java NIO (New I/O) 中的
Selector、Channel和Buffer,NIO 使用非阻塞 I/O,可以更高效地管理大量连接,但编程模型比传统的 BIO (Blocking I/O) 更复杂。
这个示例为你提供了一个坚实的基础,你可以基于此进行扩展和优化,以满足更复杂的业务需求。
