如何使用Java NIO通道分块下载文件并处理剩余字节与终止条件
分片下载实现方案
服务端单连接限速10MB/s场景下,基于HTTP Range 头的分片下载逻辑可按如下方式修复,解决循环终止条件、尾分片处理问题:
原有代码问题说明
- 状态码判断逻辑错误:Range分片请求的成功响应码是
206 Partial Content,返回200代表服务端不支持Range请求、返回了完整文件,原有逻辑会错误接收全量数据触发限流 - Range参数计算错误:HTTP Range指定的字节区间为闭区间,原逻辑
end = start + size会导致单分片多下载1字节,且首字母大写的Bytes不符合规范写法,应使用小写bytes - 未提前获取文件总大小,无法判断下载终止时机
FileChannel.transferFrom不保证单次调用完成当前分片所有字节的写入,直接单次调用会出现数据截断- 流、通道未做安全释放,存在资源泄漏风险
实现逻辑说明
- 下载前先发送HEAD请求,从响应头
Content-Length获取文件总字节数 - 循环终止条件:当前分片的起始位置
start小于文件总字节数时,持续发起分片请求 - 每轮分片的结束位置自动取
Math.min(start + CHUNK_SIZE - 1, totalLength - 1),最后一个不足10MB的分片会自动截断到文件末尾,不需要额外分支处理 - 写入分片数据时循环调用
transferFrom,直到当前分片所有字节写入完成 - 所有流、通道使用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
相关产品推荐
相关产品推荐

