You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Java NIO通道分块下载文件并处理剩余字节与终止条件

分片下载实现方案

服务端单连接限速10MB/s场景下,基于HTTP Range 头的分片下载逻辑可按如下方式修复,解决循环终止条件、尾分片处理问题:

原有代码问题说明

  • 状态码判断逻辑错误:Range分片请求的成功响应码是206 Partial Content,返回200代表服务端不支持Range请求、返回了完整文件,原有逻辑会错误接收全量数据触发限流
  • Range参数计算错误:HTTP Range指定的字节区间为闭区间,原逻辑end = start + size会导致单分片多下载1字节,且首字母大写的Bytes不符合规范写法,应使用小写bytes
  • 未提前获取文件总大小,无法判断下载终止时机
  • FileChannel.transferFrom 不保证单次调用完成当前分片所有字节的写入,直接单次调用会出现数据截断
  • 流、通道未做安全释放,存在资源泄漏风险

实现逻辑说明

  1. 下载前先发送HEAD请求,从响应头Content-Length获取文件总字节数
  2. 循环终止条件:当前分片的起始位置start小于文件总字节数时,持续发起分片请求
  3. 每轮分片的结束位置自动取Math.min(start + CHUNK_SIZE - 1, totalLength - 1),最后一个不足10MB的分片会自动截断到文件末尾,不需要额外分支处理
  4. 写入分片数据时循环调用transferFrom,直到当前分片所有字节写入完成
  5. 所有流、通道使用try-with-resources语法自动关闭

修正后代码

// 分片大小:10MB,若按二进制单位10MiB计算分片大小,可将值改为10 * 1024 * 1024
private static final long CHUNK_SIZE = 10 * 1_000_000;

private InputStream createRequest(final String source, final long start, final long end)
        throws URISyntaxException, IOException, InterruptedException {
    final HttpRequest.Builder builder = HttpRequest.newBuilder();
    builder.uri(new URI(source));
    builder.header("User-Agent", "Mozilla/5.0 (Linux; Android 7.0; SM-G892A Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/67.0.3396.87 Mobile Safari/537.36");
    // 修正Range头格式,使用小写bytes,遵循闭区间规则
    builder.header("Range", "bytes=%d-%d".formatted(start, end));

    final HttpResponse<InputStream> stream = HTTP_CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream());
    // 修正状态码判断:分片请求成功返回206
    if (stream.statusCode() == 206) {
        return stream.body();
    }
    // 兼容不支持Range的服务端降级逻辑,可根据业务场景选择保留
    if (stream.statusCode() == 200) {
        return stream.body();
    }
    throw new AssertionError("Failed to download data from URL, status code: " + stream.statusCode());
}

// 获取文件总长度
private long getFileTotalSize(final String source) throws URISyntaxException, IOException, InterruptedException {
    HttpRequest headRequest = HttpRequest.newBuilder()
            .uri(new URI(source))
            .method("HEAD", HttpRequest.BodyPublishers.noBody())
            .build();
    HttpResponse<Void> response = HTTP_CLIENT.send(headRequest, HttpResponse.BodyHandlers.discarding());
    return response.headers().firstValueAsLong("Content-Length")
            .orElseThrow(() -> new AssertionError("Cannot get file content length"));
}

private void downloadInChunks(@NotNull final String source, @NotNull final Path path)
        throws IOException, URISyntaxException, InterruptedException {
    final long totalLength = getFileTotalSize(source);
    long start = 0;

    // 自动关闭文件通道
    try (FileChannel out = new FileOutputStream(path.toFile()).getChannel()) {
        // 未到文件末尾时持续下载分片
        while (start < totalLength) {
            // 自动计算当前分片end,最后一个分片自动适配剩余长度
            long end = Math.min(start + CHUNK_SIZE - 1, totalLength - 1);
            long currentChunkLength = end - start + 1;
            // 分片读取完成后自动关闭流
            try (ReadableByteChannel in = Channels.newChannel(this.createRequest(source, start, end))) {
                long transferred = 0;
                // 循环写入,保证当前分片所有字节传输完成
                while (transferred < currentChunkLength) {
                    long count = out.transferFrom(in, start + transferred, currentChunkLength - transferred);
                    if (count == 0) break;
                    transferred += count;
                }
            }
            // 更新下一分片起始位置
            start = end + 1;
        }
    }
}

尾分片处理逻辑说明

当start + CHUNK_SIZE -1超过文件总长度时,Math.min会自动将end设置为文件最后一个字节的位置(totalLength -1),此时当前分片长度为totalLength - start,自然就是剩余不足10MB的部分,不需要单独写分支判断。当本轮分片写入完成后,start更新为end +1 = totalLength,循环条件start < totalLength不成立,自动终止循环。


内容的提问来源于stack exchange,提问作者Brandon Li

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:45:31