我会提供一个完整、可运行的示例,包括服务器端和客户端,并深入讲解其中的关键点、最佳实践以及可能遇到的问题。

核心思路
使用 Socket 传输文件的基本流程如下:
- 建立连接:服务器端创建一个
ServerSocket并在指定端口监听,客户端使用Socket连接服务器的 IP 地址和端口,一旦连接建立,双方就获得了一个用于双向通信的Socket。 - 获取输出/输入流:
- 发送方(客户端):从
Socket获取一个OutputStream,用于将文件数据写入网络流。 - 接收方(服务器):从
Socket获取一个InputStream,用于从网络流中读取文件数据。
- 发送方(客户端):从
- 文件元数据传输:在传输文件内容之前,必须先发送文件的“元数据”(metadata),比如文件名和文件大小,接收方需要这些信息来:
- 知道接收到的文件应该保存为什么名字。
- 知道应该读取多少字节才算传输完成,以防止因 TCP 流式传输特性导致的粘包或数据不完整问题。
- 传输:
- 发送方:使用
FileInputStream读取本地文件,然后通过Socket的OutputStream将数据块(Buffer)发送出去。 - 接收方:通过
Socket的InputStream读取数据块,然后使用FileOutputStream将数据写入本地文件。
- 发送方:使用
- 关闭资源:传输完成后,必须按顺序关闭所有打开的流和 Socket 连接,以释放系统资源。
完整代码示例
这个示例包含两个独立的 Java 类:FileServer 和 FileClient。
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("服务器已启动,正在监听端口 " + port + "...");
// 等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 获取输入流,用于接收数据
try (InputStream inputStream = clientSocket.getInputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream)) {
// 1. 读取文件名
String fileName = dataInputStream.readUTF();
System.out.println("准备接收文件: " + fileName);
// 2. 读取文件大小
long fileSize = dataInputStream.readLong();
System.out.println("文件大小: " + fileSize + " bytes");
// 3. 创建本地文件输出流
File receivedFile = new File("received_" + fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(receivedFile)) {
byte[] buffer = new byte[4096]; // 4KB的缓冲区
int bytesRead;
long totalBytesRead = 0;
System.out.println("开始接收文件内容...");
// 4. 循环读取文件内容
while ((bytesRead = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
// 打印接收进度
System.out.printf("接收进度: %.2f%%%n", (totalBytesRead * 100.0) / fileSize);
}
System.out.println("\n文件接收完成!");
System.out.println("文件已保存为: " + receivedFile.getAbsolutePath());
}
}
} catch (IOException e) {
System.err.println("服务器发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
FileClient.java (客户端)
客户端负责连接服务器,发送文件元数据,然后读取本地文件并通过 Socket 发送出去。

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 = "sample.txt"; // 要发送的文件路径
File file = new File(filePath);
// 检查文件是否存在
if (!file.exists()) {
System.err.println("错误: 文件 " + filePath + " 不存在!");
return;
}
try (Socket socket = new Socket(serverAddress, port);
FileInputStream fileInputStream = new FileInputStream(file);
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
System.out.println("已连接到服务器 " + serverAddress + ":" + port);
System.out.println("准备发送文件: " + file.getName());
// 1. 发送文件名
dataOutputStream.writeUTF(file.getName());
// 2. 发送文件大小
long fileSize = file.length();
dataOutputStream.writeLong(fileSize);
System.out.println("文件大小: " + fileSize + " bytes");
// 3. 发送文件内容
byte[] buffer = new byte[4096]; // 4KB的缓冲区
int bytesRead;
long totalBytesSent = 0;
System.out.println("开始发送文件内容...");
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
dataOutputStream.write(buffer, 0, bytesRead);
totalBytesSent += bytesRead;
// 打印发送进度
System.out.printf("发送进度: %.2f%%%n", (totalBytesSent * 100.0) / fileSize);
}
// 确保所有数据都已发送
dataOutputStream.flush();
System.out.println("\n文件发送完成!");
} catch (IOException e) {
System.err.println("客户端发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
如何运行
-
准备文件:在你的项目根目录下创建一个名为
sample.txt的文件,并写入一些内容。 -
编译和运行:
-
先启动服务器:在终端或 IDE 中运行
FileServer。服务器已启动,正在监听端口 12345... -
再启动客户端:在另一个终端或 IDE 中运行
FileClient。已连接到服务器 127.0.0.1:12345 准备发送文件: sample.txt 文件大小: 25 bytes 开始发送文件内容... 发送进度: 100.00% 文件发送完成!
-
-
查看结果:回到服务器的控制台,你会看到接收信息,在你的项目目录下,会出现一个名为
received_sample.txt的新文件,内容与sample.txt一致。
关键点与最佳实践解析
为什么需要先传输文件名和大小?
- 文件名:接收方(服务器)需要知道将接收到的数据保存为什么名字,如果文件名是动态的,这个信息至关重要。
- 文件大小:这是最关键的一点,TCP 是一个流式协议,它不关心消息的边界,你发送的数据会像水流一样持续不断地到达接收方,如果没有一个明确的结束标记(比如文件大小),接收方将无法判断何时停止读取,它可能会读取到下一个文件的数据(如果连续传输多个文件),或者一直等待永远不会到达的“结束符”,导致程序卡死,文件大小作为“结束标记”是必须的。
缓冲区 的大小
代码中使用了 byte[] buffer = new byte[4096];。
- 为什么需要缓冲区?:如果每次只读写一个字节(
inputStream.read()),那么对于大文件来说,需要进行数百万次 I/O 操作,这会极大地消耗 CPU 和 I/O 资源,效率极低。 - 如何选择大小?:使用缓冲区(如 4KB, 8KB, 16KB)可以显著减少 I/O 操作次数,4KB 或 8KB 是一个在大多数操作系统上表现良好的通用值,它通常与磁盘块大小或内存页大小相匹配,能获得较好的性能。
资源管理(try-with-resources)
我们使用了 try-with-resources 语句(try ( ... ))。
- 为什么重要?:
InputStream,OutputStream,Socket,ServerSocket等都是需要系统资源的对象,如果程序在运行中发生异常,这些资源可能不会被正确关闭,导致资源泄漏。 try-with-resources的作用:它会自动调用这些实现了AutoCloseable接口的资源的close()方法,无论try块是正常结束还是因异常退出,这确保了资源总是被释放,是现代 Java 编程的最佳实践。
流的嵌套
我们看到了 DataInputStream 和 FileInputStream 的嵌套使用:
DataInputStream(new FileInputStream(file))
- 作用:这是装饰器模式的应用。
FileInputStream负责从文件读取原始字节。DataInputStream在其上增加了一个功能:可以方便地读取 Java 的基本数据类型(如UTF字符串、long、int等),而不用自己去处理字节的拼接和转换。
- 对应关系:
- 发送方用
DataOutputStream写入UTF字符串和long数字。 - 接收方必须用
DataInputStream来读取它们,顺序必须严格一致。
- 发送方用
进度显示
代码中的进度条 (System.out.printf(...)) 是一个很好的用户体验增强,它通过计算已传输字节数占总字节数的百分比来显示,让用户了解传输的实时状态。
可能的扩展与问题
-
大文件传输和内存问题:对于非常大的文件(如几个 GB),直接使用
file.length()获取long类型大小没有问题,但如果文件大小超过了long的最大值(虽然不太可能),或者你想用int类型,需要特别注意,确保 JVM 有足够的堆内存来处理缓冲区,但对于 4KB 的缓冲区来说,这通常不是问题。 -
网络中断处理:当前代码没有处理网络中断的情况(如网线被拔掉),在实际应用中,你应该捕获
SocketException或IOException,并检查totalBytesRead是否等于fileSize,如果不相等,说明传输未完成,需要进行重试或通知用户失败。 -
并发传输:当前服务器一次只能处理一个客户端连接,如果要实现一个能同时处理多个客户端的文件服务器,你需要使用多线程。
ServerSocket在accept()一个连接后,应该创建一个新的Thread或Runnable任务来处理这个客户端的文件接收,ServerSocket立即返回accept()状态,等待下一个客户端。 -
安全性:这个示例没有任何安全措施,在生产环境中,你应该考虑:
- 身份验证:确保客户端是合法的。
- 加密:使用
SSL/TLS(即SSLSocket和SSLServerSocket)来加密传输的数据,防止文件在网络上被窃听。 - 路径遍历攻击:确保客户端发送的文件名是安全的,不能包含 这样的路径,防止攻击者将文件写入服务器上的任意位置。
希望这个详细的解释和示例能帮助你完全理解 Java Socket 文件传输的原理和实现!
