杰瑞科技汇

Java Socket文件上传如何实现?

我们将分步实现一个包含客户端和服务器端的完整示例。

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

核心设计思路

为了通过 Socket 传输文件,我们不能直接把文件二进制流一股脑地发过去,因为接收方无法知道文件从哪里开始,到哪里结束,我们需要一个简单的协议来规范通信过程。

一个简单且有效的协议如下:

  1. 客户端 -> 服务器 (握手阶段)

    • 客户端首先发送一个固定的字符串,"FILE_UPLOAD_START",告诉服务器“我要开始上传文件了”。
    • 紧接着,客户端发送文件名("test.txt")和文件大小(1024 字节)。
    • 服务器收到这些信息后,解析出文件名和大小,并准备好一个输出流来接收文件内容。
  2. 客户端 -> 服务器 (文件传输阶段)

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

    客户端开始读取本地文件,并将文件内容通过 Socket 的输出流,以字节块的形式发送给服务器。

  3. 服务器 -> 客户端 (确认阶段)

    • 服务器不断从输入流中读取数据,并写入到本地文件,直到读取到的字节数等于之前接收到的文件大小。
    • 文件接收完毕后,服务器可以发送一个确认消息,"UPLOAD_SUCCESS",告知客户端文件已成功接收。
  4. 关闭连接

    双方关闭 Socket 连接。

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

步骤 1:服务器端代码

服务器负责监听客户端的连接,接收文件信息,然后将接收到的数据保存到本地。

FileUploadServer.java

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class FileUploadServer {
    private static final int SERVER_PORT = 8888;
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT)) {
            System.out.println("服务器已启动,等待客户端连接...");
            // 循环监听,以便处理多个客户端连接
            while (true) {
                try (Socket socket = serverSocket.accept();
                     DataInputStream dis = new DataInputStream(socket.getInputStream());
                     FileOutputStream fos = new FileOutputStream("received_" + System.currentTimeMillis() + ".dat")) {
                    System.out.println("客户端 " + socket.getInetAddress() + " 已连接。");
                    // 1. 读取客户端发送的协议标识
                    String protocol = dis.readUTF();
                    if (!"FILE_UPLOAD_START".equals(protocol)) {
                        System.out.println("收到非法的协议标识: " + protocol);
                        continue; // 忽略本次连接
                    }
                    // 2. 读取文件名和文件大小
                    String fileName = dis.readUTF();
                    long fileSize = dis.readLong();
                    System.out.println("开始接收文件: " + fileName + ", 大小: " + fileSize + " 字节");
                    // 3. 循环读取文件内容并写入本地文件
                    byte[] buffer = new byte[4096];
                    long remainingBytes = fileSize;
                    int bytesRead;
                    while (remainingBytes > 0 && (bytesRead = dis.read(buffer, 0, (int) Math.min(buffer.length, remainingBytes))) != -1) {
                        fos.write(buffer, 0, bytesRead);
                        remainingBytes -= bytesRead;
                    }
                    System.out.println("文件接收完成!");
                } catch (IOException e) {
                    System.err.println("处理客户端请求时发生错误: " + e.getMessage());
                }
            }
        } catch (IOException e) {
            System.err.println("服务器启动失败: " + e.getMessage());
        }
    }
}

代码解释:

  1. ServerSocket serverSocket = new ServerSocket(8888): 在端口 8888 上创建一个服务器套接字,开始监听客户端连接。
  2. serverSocket.accept(): 这是一个阻塞方法,会一直等待直到有客户端连接,一旦有客户端连接,它会返回一个 Socket 对象,代表与该客户端的连接通道。
  3. DataInputStream dis: 使用 DataInputStream 可以方便地读取基本数据类型,如 UTF 字符串和 long 整数。
  4. FileOutputStream fos: 用于将接收到的字节流写入到本地文件,文件名我们动态生成,以避免覆盖。
  5. 协议解析:
    • dis.readUTF(): 读取客户端发来的字符串,判断是否为 "FILE_UPLOAD_START"
    • dis.readUTF(): 读取文件名。
    • dis.readLong(): 读取文件大小。
  6. 文件接收循环:
    • 我们创建一个 4KB 的 buffer
    • while (remainingBytes > 0) 循环确保我们只读取文件规定大小的数据。
    • dis.read(buffer, ...) 从输入流中读取数据到 buffer 中。
    • fos.write(buffer, ...)buffer 中的数据写入本地文件。
    • remainingBytes 不断减少,直到为 0,表示文件接收完毕。

步骤 2:客户端代码

客户端负责连接服务器,读取本地文件,然后按照我们定义的协议将文件信息发送给服务器,最后发送文件内容。

FileUploadClient.java

import java.io.*;
import java.net.Socket;
public class FileUploadClient {
    private static final String SERVER_HOST = "127.0.0.1"; // 服务器地址
    private static final int SERVER_PORT = 8888;         // 服务器端口
    public static void main(String[] args) {
        // 要上传的文件路径
        String filePath = "C:\\path\\to\\your\\file.txt"; // 请替换为你的文件路径
        File file = new File(filePath);
        try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT);
             DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
             FileInputStream fis = new FileInputStream(file)) {
            System.out.println("已连接到服务器 " + SERVER_HOST + ":" + SERVER_PORT);
            // 1. 发送协议标识
            dos.writeUTF("FILE_UPLOAD_START");
            // 2. 发送文件名和文件大小
            dos.writeUTF(file.getName());
            dos.writeLong(file.length());
            System.out.println("开始上传文件: " + file.getName() + ", 大小: " + file.length() + " 字节");
            // 3. 循环读取本地文件并发送给服务器
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                dos.write(buffer, 0, bytesRead);
            }
            System.out.println("文件上传完成!");
        } catch (FileNotFoundException e) {
            System.err.println("错误: 文件未找到 - " + e.getMessage());
        } catch (IOException e) {
            System.err.println("上传文件时发生错误: " + e.getMessage());
        }
    }
}

代码解释:

  1. new Socket(SERVER_HOST, SERVER_PORT): 创建一个 Socket 客户端,尝试连接到指定地址和端口的服务器。
  2. DataOutputStream dos: 使用 DataOutputStream 方便地写入基本数据类型。
  3. FileInputStream fis: 用于读取本地文件的内容。
  4. 协议发送:
    • dos.writeUTF("FILE_UPLOAD_START"): 发送协议标识。
    • dos.writeUTF(file.getName()): 发送文件名。
    • dos.writeLong(file.length()): 发送文件大小。
  5. 文件发送循环:
    • fis.read(buffer) 从本地文件中读取数据到 buffer
    • dos.write(buffer, 0, bytesRead)buffer 中的数据通过 Socket 发送给服务器。
    • 循环直到文件读取完毕(fis.read() 返回 -1)。

如何运行

  1. 准备文件:在你的电脑上创建一个文本文件,C:\temp\my_test_file.txt,并在里面写一些内容,然后修改客户端代码中的 filePath 为这个路径。
  2. 编译代码:将两个 .java 文件放在同一个目录下,编译它们。
    javac FileUploadServer.java FileUploadClient.java
  3. 运行服务器:首先运行服务器端程序。
    java FileUploadServer

    你会看到控制台输出:服务器已启动,等待客户端连接...

  4. 运行客户端:在另一个新的终端窗口中,运行客户端程序。
    java FileUploadClient
  5. 观察结果
    • 客户端终端:会显示连接成功和上传完成的日志。
    • 服务器终端:会显示客户端已连接、开始接收文件和接收完成的日志。
    • 服务器所在目录:你会发现一个新文件,received_167....dat`,打开它,你会发现内容和你客户端的文件完全一样。

进阶与优化

这个基础示例已经可以实现文件上传,但在实际生产环境中,还需要考虑很多优化点:

  1. 关闭资源的顺序:在 try-with-resources 语句中,JVM 会自动关闭资源,关闭顺序与声明顺序相反,这确保了输出流在关闭前会先刷新缓冲区,输入流在关闭前会先读取完所有数据,是很好的实践。

  2. 更健壮的协议

    • 可以增加一个文件校验步骤,比如在文件传输前后都发送文件的 MD5 或 SHA-256 值,服务器接收完毕后进行校验,以确保文件在传输过程中没有损坏。
    • 可以增加错误处理机制,如果服务器无法创建文件(如磁盘已满),可以发送一个 "UPLOAD_FAILED" 消息给客户端。
  3. 多线程处理:上面的服务器是单线程的,一次只能处理一个客户端,为了支持高并发,你需要为每个客户端连接创建一个新的线程来处理,可以使用线程池来优化性能,避免频繁创建和销毁线程。

    多线程服务器示例:

    // 在 FileUploadServer 的 main 方法中,将 accept 循环部分修改为:
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 为每个客户端连接创建一个新线程
        new Thread(() -> {
            try (DataInputStream dis = new DataInputStream(clientSocket.getInputStream());
                 FileOutputStream fos = new FileOutputStream("received_" + System.currentTimeMillis() + ".dat")) {
                // ... (中间处理逻辑与之前相同)
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
  4. 大文件传输:对于非常大的文件(如 GB 级别),一次性读取整个文件到内存是不现实的,我们上面的代码已经通过 byte[] buffer 实现了分块读写,这是处理大文件的标准做法,内存占用很低。

  5. NIO (New I/O):对于更高性能的服务器,可以使用 Java NIO (基于 Channel 和 Buffer) 来替代传统的 BIO (基于 Stream),NIO 使用非阻塞 I/O 和多路复用技术(如 Selector),可以用一个线程处理成百上千的连接,性能远超传统的多线程模型,但这会增加编程的复杂性。

希望这个详细的解释和完整的代码示例能帮助你理解 Java Socket 文件上传的原理和实现!

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