杰瑞科技汇

Java如何用Socket实现文件传输?

核心原理

通过 Socket 传输文件的基本思想是:

Java如何用Socket实现文件传输?-图1
(图片来源网络,侵删)
  1. 建立连接:客户端作为发起方,连接到服务器端。
  2. 发送元数据:客户端首先向服务器发送文件的“元数据”,例如文件名和文件大小,服务器需要先接收这些信息,以便做好接收文件的准备(如创建文件、分配空间等)。
  3. 发送文件内容:客户端将文件内容以二进制流的形式,通过 Socket 的输出流发送出去。
  4. 接收文件内容:服务器通过 Socket 的输入流,以字节流的形式接收数据,并将其写入到本地文件中。
  5. 关闭连接:传输完成后,双方关闭 Socket 和相关的流资源。

关键点

  • 流式传输:文件被看作一个字节流,而不是一个整体,这样可以避免一次性将大文件加载到内存中,非常适合传输大文件。
  • 元数据:必须先告知服务器要传什么文件以及文件有多大,否则服务器无法正确地保存文件。
  • 资源管理SocketInputStreamOutputStream 等都是需要手动关闭的资源,最好使用 try-finallytry-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)

客户端负责连接服务器,读取本地文件,并发送文件名和文件内容。

Java如何用Socket实现文件传输?-图2
(图片来源网络,侵删)
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());
        }
    }
}

关键点解析

  1. 流的选择和包装

    • FileInputStream / FileOutputStream: 用于读写本地文件的原始字节流。
    • Socket.getInputStream() / Socket.getOutputStream(): 用于通过网络收发数据的原始字节流。
    • DataInputStream / DataOutputStream: 这是关键,它们可以包装其他的 InputStream / OutputStream,并提供方便的方法来读写 Java 的基本数据类型,如 readUTF() / writeUTF() (用于字符串),readLong() / writeLong() (用于长整型,如文件大小),这使得发送元数据变得非常简单。
  2. 文件名和文件大小的发送顺序

    • 必须先发送文件名,再发送文件大小,最后发送文件内容,服务器端的接收顺序必须与此完全一致。
    • 如果先发送内容,服务器不知道要创建什么文件名,也不知道要接收多少字节,会导致数据错乱。
  3. 缓冲区

    • 代码中使用了 byte[] buffer = new byte[4096]; 作为缓冲区,这是为了提高效率,网络I/O和文件I/O都是比较慢的操作,使用缓冲区可以减少I/O操作的次数,从而显著提升性能,4096是一个常见的缓冲区大小。
  4. 进度显示

    Java如何用Socket实现文件传输?-图3
    (图片来源网络,侵删)
    • 在客户端代码中,我们通过比较 totalBytesSentfileSize 来计算并打印发送进度,这对于大文件传输的用户体验很重要。
  5. try-with-resources 语句

    • 代码中使用了 try (Socket socket = ...; InputStream is = ...) 这种语法,它会自动在 try 块结束时关闭括号中声明的资源,只要这些资源实现了 AutoCloseable 接口(Socket, InputStream, OutputStream 等都实现了),这可以防止资源泄漏,是现代Java推荐的写法。

如何运行和测试

  1. 准备文件 在与 FileClient.java 相同的目录下,创建一个名为 sample_file.txt 的文件,并在里面写入一些测试内容,"Hello, this is a test file for socket transfer."。

  2. 编译代码 打开终端或命令提示符,进入包含 .java 文件的目录,运行:

    javac FileServer.java FileClient.java
  3. 启动服务器 在第一个终端窗口中运行服务器:

    java FileServer

    你会看到输出:服务器启动,等待连接...

  4. 启动客户端 在第二个终端窗口中运行客户端:

    java FileClient

    你会看到客户端的连接和发送进度信息,以及服务器端的接收信息。

  5. 验证结果 传输完成后,在服务器的运行目录下,你会发现多了一个 received_files 文件夹,里面有一个 sample_file.txt 文件,打开它,内容应该与客户端发送的文件完全一致。


进阶与优化

  1. 并发处理 当前的服务器是单线程的,一次只能处理一个客户端,如果需要同时处理多个客户端,可以使用多线程,一个简单的改进是在 FileServeraccept() 循环中,为每个客户端连接创建一个新的线程来处理。

    // 在 FileServer.java 的 main 方法中修改
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 为每个连接创建一个新线程
        new Thread(() -> {
            // ... 将原来的 try-with-resources 块里的代码放在这里 ...
        }).start();
    }
  2. 传输校验 为了确保文件在传输过程中没有损坏,可以在发送前和接收后计算文件的 MD5 或 SHA-1 哈希值,并进行比较。

  3. 断点续传 更高级的功能是断点续传,这需要客户端和服务器记录已传输的字节数,客户端在连接后,可以先发送一个已传输的字节数,服务器从该位置继续写入文件。

  4. 使用 NIO 对于高性能、高并发的场景,可以使用 Java NIO (New I/O) 中的 SelectorChannelBuffer,NIO 使用非阻塞 I/O,可以更高效地管理大量连接,但编程模型比传统的 BIO (Blocking I/O) 更复杂。

这个示例为你提供了一个坚实的基础,你可以基于此进行扩展和优化,以满足更复杂的业务需求。

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