杰瑞科技汇

Java Socket文件传输如何实现?

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

Java Socket文件传输如何实现?-图1
(图片来源网络,侵删)

我们将分步实现一个完整的、可运行的文件传输程序,包括:

  1. 服务器端:监听连接,接收文件数据,并将其保存到本地。
  2. 客户端:连接服务器,读取本地文件,并将文件数据发送给服务器。

核心概念

在开始编码前,我们需要明确几个关键点:

  • Socket 和 ServerSocket
    • ServerSocket:服务器端使用,负责在指定端口上监听客户端的连接请求。
    • Socket:客户端和服务器端一旦建立连接,都会得到一个 Socket 实例,用于后续的数据通信。
  • 输入/输出流
    • 通信的本质是流,服务器端需要从 Socket 获取一个输入流 (InputStream) 来读取客户端发送的数据。
    • 客户端需要从 Socket 获取一个输出流 (OutputStream) 来向服务器写入数据。
  • 文件输入/输出流
    • 客户端需要从本地文件读取数据,使用 FileInputStream
    • 服务器端需要将接收到的数据写入本地文件,使用 FileOutputStream
  • 关键问题:如何知道文件传输完了?
    • 这是最常见的问题,如果只是简单地将文件流通过 Socket 发送,服务器端会一直等待,不知道何时结束。
    • 解决方案:在发送文件数据之前,先发送文件的元信息(如文件名、文件大小),服务器端先接收这些元信息,然后循环读取输入流,直到读取到的字节数等于文件大小时,才认为文件传输完成。

代码实现

我们将创建两个 Java 类:FileServer.javaFileClient.java

项目结构

file-transfer-demo/
├── src/
│   ├── FileServer.java
│   └── FileClient.java
└── files/ (用于存放要传输的文件)
    └── source.txt

步骤 1: 服务器端 (FileServer.java)

服务器的工作流程是:

Java Socket文件传输如何实现?-图2
(图片来源网络,侵删)
  1. 创建 ServerSocket 并在指定端口(12345)上监听。
  2. 使用 accept() 方法阻塞,等待客户端连接。
  3. 客户端连接后,获取其 Socket 和输入流。
  4. 先读取文件名和文件大小
  5. 根据文件名创建 FileOutputStream
  6. 循环读取输入流,将数据写入 FileOutputStream,直到读取的字节数等于文件大小。
  7. 关闭所有资源。
// 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)

客户端的工作流程是:

  1. 创建 Socket 并连接到服务器的 IP 地址和端口。
  2. 获取输出流。
  3. 先发送文件名和文件大小
  4. 读取本地文件(使用 FileInputStream)。
  5. 循环读取文件内容,并通过 Socket 的输出流发送出去。
  6. 关闭所有资源。
// 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();
        }
    }
}

如何运行

  1. 准备文件:在 files 目录下创建一个 source.txt 文件,并写入一些内容,"Hello, this is a test file for socket transfer."。

  2. 编译代码:在项目根目录下打开终端,执行 javac 命令。

    javac src/FileServer.java src/FileClient.java
  3. 运行服务器:首先运行服务器端程序,它会启动并等待连接。

    Java Socket文件传输如何实现?-图3
    (图片来源网络,侵删)
    java -cp src FileServer

    你会看到输出:

    服务器已启动,监听端口 12345...
  4. 运行客户端:在另一个新的终端窗口中,运行客户端程序。

    java -cp src FileClient

    你会看到客户端的输出:

    已连接到服务器: 127.0.0.1
    已发送文件名: source.txt
    已发送文件大小: 42 bytes
    发送进度: 100.00%
    文件发送完成!
  5. 查看服务器输出:回到服务器端的终端,你会看到它接收文件的过程:

    客户端已连接: /127.0.0.1
    正在接收文件: source.txt
    文件大小: 42 bytes
    接收进度: 100.00%
    文件接收完成!
  6. 验证结果:检查你的项目目录,会发现多了一个 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 文件传输!

分享:
扫描分享到社交APP
上一篇
下一篇