Skip to content

字节流实战:文件上传下载

本节综合运用字节流的各类组件,实现带进度显示的文件上传下载功能。

文件下载:从服务器读取到本地

java
public static void downloadFile(String serverUrl, String savePath)
        throws IOException {
    URL url = new URL(serverUrl);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestMethod("GET");
    conn.setConnectTimeout(5000);
    conn.setReadTimeout(30000);

    int fileSize = conn.getContentLength();  // 文件总大小
    try (
        InputStream in = new BufferedInputStream(conn.getInputStream());
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(savePath))
    ) {
        byte[] buffer = new byte[8192];
        int len;
        long downloaded = 0;

        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
            downloaded += len;

            // 显示进度
            if (fileSize > 0) {
                int percent = (int) (downloaded * 100 / fileSize);
                System.out.printf("\r下载进度: %d%%", percent);
            }
        }
        System.out.println();
    }
}

断点续传

中断后从上次下载的位置继续:

java
public static void downloadWithResume(String serverUrl, String savePath)
        throws IOException {
    File localFile = new File(savePath);
    long downloadedSize = localFile.exists() ? localFile.length() : 0;

    URL url = new URL(serverUrl);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestProperty("Range", "bytes=" + downloadedSize + "-");

    int responseCode = conn.getResponseCode();
    if (responseCode == 206) {
        System.out.println("支持断点续传,从 " + downloadedSize + " 字节处继续");
    } else if (responseCode == 200) {
        downloadedSize = 0;
        System.out.println("不支持断点续传,从头开始");
    }

    int fileSize = conn.getContentLength();
    if (fileSize > 0) {
        fileSize += downloadedSize;
    }

    try (
        InputStream in = new BufferedInputStream(conn.getInputStream());
        FileOutputStream fos = new FileOutputStream(savePath, true);  // 追加模式
        BufferedOutputStream out = new BufferedOutputStream(fos)
    ) {
        byte[] buffer = new byte[8192];
        int len;
        long total = downloadedSize;

        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
            total += len;

            if (fileSize > 0) {
                int percent = (int) (total * 100 / fileSize);
                System.out.printf("\r下载进度: %d%%", percent);
            }
        }
        System.out.println();
    }
}

关键点

  • Range: bytes=xxx- 请求头告诉服务器从哪个字节开始
  • 206 Partial Content 表示服务器支持断点续传
  • FileOutputStream(path, true) 以追加模式打开

大文件分片上传

对于超大文件,可以分片上传,每片独立重试:

java
public static void uploadInChunks(String uploadUrl, String filePath,
        int chunkSizeMB) throws IOException {
    File file = new File(filePath);
    long fileSize = file.length();
    int chunkSize = chunkSizeMB * 1024 * 1024;

    int totalChunks = (int) ((fileSize + chunkSize - 1) / chunkSize);

    try (FileInputStream fis = new FileInputStream(file)) {
        byte[] buffer = new byte[chunkSize];
        int chunkIndex = 0;
        int len;

        while ((len = fis.read(buffer)) != -1) {
            uploadChunk(uploadUrl, buffer, 0, len, chunkIndex, totalChunks);
            chunkIndex++;

            int percent = (int) (chunkIndex * 100 / totalChunks);
            System.out.printf("\r分片上传进度: %d%% (%d/%d)",
                percent, chunkIndex, totalChunks);
        }
        System.out.println();
    }
}

private static void uploadChunk(String url, byte[] data,
        int offset, int length, int chunkIndex, int totalChunks)
        throws IOException {
    // 实际实现中,这里是 HTTP 请求发送分片数据
    // 省略具体 HTTP 实现
}

内存占用监控

下载大文件时监控内存使用:

java
public static void downloadWithMemoryControl(String serverUrl, String savePath)
        throws IOException {
    URL url = new URL(serverUrl);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    int fileSize = conn.getContentLength();

    long startMemory = Runtime.getRuntime().totalMemory() -
                       Runtime.getRuntime().freeMemory();

    try (
        InputStream in = new BufferedInputStream(conn.getInputStream());
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(savePath))
    ) {
        byte[] buffer = new byte[8192];
        int len;
        long downloaded = 0;

        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
            downloaded += len;

            // 每 10MB 报告一次内存使用
            if (downloaded % (10 * 1024 * 1024) < 8192) {
                Runtime rt = Runtime.getRuntime();
                long used = rt.totalMemory() - rt.freeMemory();
                System.out.printf("内存使用: %.2f MB%n", used / 1024.0 / 1024.0);
            }
        }
    }
}

实战要点

  • 文件下载:BufferedInputStream + BufferedOutputStream + 进度显示
  • 断点续传:Range 请求头 + 追加写入模式
  • 大文件分片:分块读取上传,客户端和服务端协同
  • 内存控制:注意缓冲区大小和总内存占用

基于 VitePress 构建