java 下载加速_Java实现大文件下载,提速30倍!想学?我教你啊!

前言

兄弟们看到这个标题可能会觉得是个标题党,为了解决疑虑,我们先来看下最终的测试结果:

测试云盘下载的文件 46M,自己本地最大下载速度 2M

1. 单线程下载,总耗时: 603s

e3f57a11d8c9d1101d91d9728f945033.png

img

2. 多线程下载,50个线程,总耗时:13s

04c72615c6d910ed5d8670cae4fb0c64.png

img

测试结果,提速46倍,我还是太谦虚了,只说提速30倍,此处我们觉得应该有掌声(我听不到,还是点赞实在)

b7efb2e138609a9eb14bd971b456d1a0.png

HTTP协议Range请求头

Range主要是针对只需要获取部分资源的范围请求,通过指定Range即可告知服务器资源的指定范围。格式: Range: bytes=start-end

比如:获取字节范围 5001-10000

Range: bytes=5001-10000

1

也可以指定开始位置不指定结束位置,表示获取开始位置之后的全部数据

Range: bytes=5001-

1

服务器接收到带有Range的请求,会在处理请求之后返回状态码为206 Partial Content的响应。

基于Range的特性,我们就可以实现文件的多线程下载,文件的断点续传

准备工作

本文我们使用的SpringMVC中的RestTemplate;由于云盘的链接是Https,所以我们需要设置RestTemplate绕过证书验证

pom.xml

2e2d75c950c1c51d1f70711f16399def.png

编写

RestTemplate的构造器,以及绕过https的证书验证

6e38afae2a0b639e680e4b4454525d36.png

在下载的过程中,我们需要知道当前下载的速度是多少,所以需要定义一个显示下载速度的接口

dcd966ed0f59f6852feaa4419864a970.png

因为计算下载速度,我们需要知道每秒传输的字节数是多少,为了监控传输数据的过程,我们需要了解SpringMVC中的接口ResponseExtractor

df1785a9330e585bdddc9e7a4cabdc6f.png

该接口只有一个方法,当客户端和服务器端连接建立之后,会调用这个方法,我们可以在这个方法中监控下载的速度。

DisplayDownloadSpeed接口的抽象实现

AbstractDisplayDownloadSpeedResponseExtractor

a702941466318b8830bd71c3eae92a79.png

整个项目主要涉及到的类图

b05f53e8d9ce078728f681dffad1a9a8.png

简单的文件下载器

这里使用的是restTemplate调用execute, 先文件获取到字节数组, 再将字节数组直接写到目标文件。

这里我们需要注意的点是: 这种方式会将文件的字节数组全部放入内存中, 及其消耗资源;我们来看看如何实现。

创建

ByteArrayResponseExtractor类继承

AbstractDisplayDownloadSpeedResponseExtractor

be962a70fb3a5432414282158692daca.png

调用

restTemplate.execute执行下载,保存字节数据到文件中

9c69a393d9b7f2bad884920b0986207e.png

测试下载819M的idea

4f24c5c8f5f666a0743d8e9e7387d7db.png

58d728f76efa8f581fd6a0622d4d1954.png

执行一段时间之后,我们可以看到内存已经使用了800M左右,所以这种方式只能使用于小文件的下载,如果我们下载几G的大文件,内存肯定是不够用的。至于下载时间,因为文件太大也没有等下载完成就结束了程序。

单线程大文件下载

上面的方式只能下载小的文件,那大文件的下载我们该用什么方式呢?我们可以把流输出到文件而不是内存中。接下来我们来实现我们大文件的下载。

创建

FileResponseExtractor类继承

AbstractDisplayDownloadSpeedResponseExtractor,把流输出到文件中

4bef7f84dc0ed5e9b45bfef251c2c579.png

文件下载器,先把流输出到临时下载文件(xxxxx.download),下载完成后在重命名文件

0c2044f60380a3d72584857b36de9e53.png

测试下载819M的idea

b9ef3312d2b351623278fef687789f22.png

执行一段时间之后,我们再看看下内存的使用情况,发现这种方式内存消耗较少,效果比较理想,下载时间:199s

7c66e6f5d5e024d5139cdf10f0d9d476.png

00bda4eedd88c60c7bb00a08cf159cc8.png

多线程文件下载

如果服务器不限速的话,通常能够把自己本地的带宽给跑满,那么使用单线程下载就够了,但是如果遇到服务器限速,下载速度远小于自己本地的带宽,那么可以考虑使用多线程下载。多线程我们使用CompletableFuture(可以参考文章 CompletableFuture让你的代码免受阻塞之苦)。

实现多线程文件下载的基本流程:

首先我们通过Http协议的Head方法获取到文件的总大小

然后根据设置的线程数均分文件的大小,计算每个线程的下载的字节数据开始位置和结束位置

开启线程,设置HTTP请求头Range信息,开始下载数据到临时文件

下载完成后把每个线程下载完成的临时文件合并成一个文件

完成代码如下:

22ee0fed1c337da0b5011e582945eff7.png

开启30个线程测试下载819M的idea

ea13e1147855b0a9b5132ba499d86088.png

img

de014d95c9c80e383fce613b6fcf6a5f.png

d38fe305f52726b8542884197a010b57.png

从执行的结果上来看,因为开启了30个线程同时在下载,内存的占用要比单线程消耗的多,但是也在接受范围内,下载时间:81s,速度提升2.5倍,这是因为idea的下载服务器没有限速,本次多线程速度的提升仅仅是在充分的压榨本地的带宽,所以提示的幅度不大。

单线程下载和对线程下载对比测试

因为云盘对单个线程的下载速度做了限制,大概是在100kb,所以我们使用云盘的下载链接,来测试多线程和单线程的下载速度。

测试 云盘中 46M 的文件的下载速度,自己本地最大下载速度 2M

获取文件的下载地址

e4ae1b5337c4e25c1e6e38bcc9483f1d.png

注意:从浏览器中获取的链接需要先使用URLDecode解码,否则下载会失败,并且云盘文件的下载链接是有时效性的,过期后就不能在下载,需要重新生成下载链接

测试单线程下载文件

0a1b284a24b132dd0e6b3b590409ba36.png

执行的结果可以看出,云盘对单线程的下载限速真的是丧心病狂, 46M的文件下载需要耗时:600s

测试多线程下载文件

为了充分的压榨网速,找出最合适的线程数,所以测试了不同线程数的下载速度

线程数

下载总耗时

10

60s

20

30s

30

21s

40

15s

50

13s

从测试的结果上来看,对于自己的运行环境把线程数设置在30个左右比较合适

19a6a13953241b72f17c93c890d75f7c.png

文件断点续传如何实现,欢迎在大家评论区说出自己的思路。

源码地址:https://gitee.com/silently9527/fast-download

5d181aa35a857c3ab6e5b8594c0802b0.png

8acca911615258812b79711921839466.gif

有热门推荐👇

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了Java文件上传的度,我们可以使用多线程上传。以下是实现多线程上传的步骤: 1.将大文件分割成多个小文件,每个小文件的大小可以根据实际情况进行设置。 2.使用多线程同时上传这些小文件,可以使用Java的CompletableFuture来实现。 3.在服务器端,将这些小文件合并成一个大文件。 下面是一个Java多线程上传大文件的示例代码: ```java import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; public class MultiThreadUpload { private static final int THREAD_NUM = 5; // 线程数 private static final int BUFFER_SIZE = 1024 * 1024; // 缓冲区大小 private static final String UPLOAD_URL = "http://example.com/upload"; // 上传地址 private static final String FILE_PATH = "/path/to/large/file"; // 大文件路径 public static void main(String[] args) throws IOException { File file = new File(FILE_PATH); long fileSize = file.length(); long blockSize = fileSize / THREAD_NUM + 1; // 每个线程上传的块大小 List<CompletableFuture<Void>> futures = new ArrayList<>(); for (int i = 0; i < THREAD_NUM; i++) { long start = i * blockSize; long end = Math.min(start + blockSize, fileSize); CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { uploadBlock(file, start, end); } catch (IOException e) { e.printStackTrace(); } }); futures.add(future); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); System.out.println("Upload finished."); } private static void uploadBlock(File file, long start, long end) throws IOException { URL url = new URL(UPLOAD_URL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/octet-stream"); conn.setRequestProperty("Content-Range", "bytes " + start + "-" + (end - 1) + "/" + file.length()); try (InputStream in = new FileInputStream(file); OutputStream out = conn.getOutputStream()) { byte[] buffer = new byte[BUFFER_SIZE]; long pos = 0; while (pos < start) { long n = in.skip(start - pos); if (n <= 0) { throw new IOException("Unexpected EOF"); } pos += n; } while (pos < end) { int n = in.read(buffer, 0, (int) Math.min(buffer.length, end - pos)); if (n < 0) { throw new IOException("Unexpected EOF"); } out.write(buffer, 0, n); pos += n; } } if (conn.getResponseCode() != HttpURLConnection.HTTP_CREATED) { throw new IOException("Failed to upload block: " + conn.getResponseCode() + " " + conn.getResponseMessage()); } } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值