如何实现下载⼤⽂件,解决⽹络中断等问题
下载东西很容易,但是如何优化?
先抛出⼏个问题
1.下载完后,⽂件都要存在内存吗?  ⽐如我下载两个g的⽂件需要两个g的内存?
2.下载⽂件后,如何存进硬盘,需要拷贝⼏次? 能不能实现零拷贝?
3.下载的过程中多线程下载会提⾼速度吗?
4.下载的过程中如果⽹络中断了怎么办?
分别回答这⼏个问题
1.没必要存在内存,我们可以⽤流来下载,但是⽤流来下载的痛点是:⽹络断开了怎么办?我们知道流只能读取⼀次,⽹络断开后,其实是要往回读取的,这样就会抛出错误。
public static File getApkByUrl(String urlString) throws IOException {
URL url = null;
ReadableByteChannel readableByteChannel = null;
FileOutputStream fileOutputStream = null;
File apkFile = ateTempFile("source", "apk");
try{
url = new URL(urlString);
//通过http请求获得⽂件的⼤⼩
HttpURLConnection conn = null;
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("HEAD");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows 7; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36            //System.out.ContentLength());
//url通道
readableByteChannel = wChannel(url.openStream());
fileOutputStream = new FileOutputStream(apkFile);
FileChannel Channel();
//下载连接两个通道
//ansferFrom(readableByteChannel, 0 , Long.MAX_VALUE);
//fileCopy_TransferFrom(fileChannel,  readableByteChannel);
long index=0;
long Memory=20971520;
while(fileChannel.size()&ContentLength()){
index=fileChannel.size();
System.out.println(fileChannel.size());
}
} catch (IOException ioException) {
System.out.println("Problem Occured While Downloading the File = " + Message());
return null;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(fileOutputStream != null ) {
fileOutputStream.close();
}
if(readableByteChannel != null) {
readableByteChannel.close();
}
} catch (IOException ioException){
System.out.println("Problem Occured While Closing The Object= " + Message());
return null;
}
}
return apkFile;
}
上⾯代码的逻辑是先通过http请求获得⽂件的总⼤⼩,之后再⽤流来下载⽂件,这⾥我是⼀段⼀段来请求的
while(fileChannel.size()&ContentLength()){
index=fileChannel.size();
System.out.println(fileChannel.size());
}
这⾥如果我们打断点,先下载⼀段后拔⽹线,⽹络恢复之后就会报错。
原因就是流只能⼀次读,举个例⼦我上⼀次请求的访问时0-10,现在⽹络断开我只下载到5,那下次我请求应该是5开始对吧?可是之前已经请求⼀次了。
2.零拷贝问题
我们能不能从下载后存在⽹卡的缓存直接转移到内核的缓存,再从内核的缓存存进硬盘?
图从⽹上的,就是我们现在可以不⽤经过⽤户空间来拷贝了。⽤得就是
这个⽅法。
3.下载过程中⽤多线程是会提速的。
我们下载的速度实际就是实时抢占⽹络宽带的⼤⼩。实际上
⽤户进程实时抢占的带宽 ≤实时⽹络可⽤带宽
那现在我们要做的就是⽆限接近实时⽹络可⽤带宽对吧?
可是!TCP有流量探测机制,⼀旦检测到有丢包就会减速!来个图!
很显然,指数级降速,线性增速,这很不公平!降速很快,但升速却很漫长!造成的直接恶果就是真实的传输速率远远⼩于实时可⽤带宽。  这没办法,为了避免⽹络拥塞对吧?这就有点牺牲⼩我成全⼤家的精神,我的减速是为了⼤家能⽤。
多线程相⽐单线程的优势是,由于有多个线程在竞争实时可⽤带宽。尽管多线程逻辑上是并⾏的,但其实还是按时序的串⾏处理。所以每个线程处于的阶段并不⼀致。 在任意时刻,有的线程处于丢包被罚1/2降速,有的线程处于2倍增速阶段(SlowStart),⽽有的线程处于线性增长阶段。通过多个线程的下载速率的加权平均,得到的是⼀根相对平滑的下载曲线。这条平滑曲线在⼤多数时候应该位于单线程下载速率的上⽅。这就是多线程下载速率更有优势的体现。 但是,如果TCP流量探测机制更加智能,⽐如BBR算法。BBR算法最⼤的进步,就是摒弃传统TCP流量调度算法(基于是否丢包⽽升速或降速), BBR采取的是,实时测量⽹络最⼤的可⽤带宽,并将发送速率与之相匹配,⼀直在实时可⽤带宽附近⼩范围徘徊,避免⼤起⼤落的情况发⽣。测量速率能⽆限接近实时可⽤带宽,多线程相⽐单线程,优势就体现不出来了。
所以在是TCP是传统的拥塞算法的情况下,多线程还是有优势的。
4.下载的过程中如果⽹络中断了怎么办?
因为我们⽤的是流嘛,所以不能重复读取。简单的解决办法就是,如果是⼀次性获取,⽽不是像我上
⾯那样分段获取,是不会报错的注意! 所以我们要判断⽂件是否下载完整,我们可以先发个请求获取⽂件的⼤⼩,下载完成后再判断⼀下。
上⾯是简单的⽅法。那我肯定希望不要重新下载,如果下载⼀个100g的,⽹络断开就要重新下载,那我要疯了吧?
那如何实现?
其实最核⼼的⽅法就是,不要在下载链接中请求⽂件的全部,分段请求,举个例⼦,0-9,我分5段请求,第⼀段0-1第⼆段2-3.…… 这样如果其中⼀段⽹络断开,那我就重新请求那⼀段就⾏,流重新读。
package company;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.HttpURLConnection;
import java.MalformedURLException;
import java.URL;
import urrent.CountDownLatch;
import urrent.ExecutorService;
import urrent.Executors;
import urrent.Executors;
/*
* Encode:UTF-8
*
* Author:GFC
*
* 根据输⼊的url和设定的线程数,来完成断点续传功能。
*
* 每个线程⽀负责某⼀⼩段的数据下载;再通过RandomAccessFil  e完成数据的整合。
*/
public class MultiTheradDownLoad {
//下载地址
private String urlStr = null;
private String filename = null;
//临时⽂件名
private String tmpfilename = null;
private File file=null;
private int threadNum = 0;
private CountDownLatch latch = null;//设置⼀个计数器,代码内主要⽤来完成对缓存⽂件的删除
private long fileLength = 0l;
private long threadLength = 0l;
private long[] startPos;//保留每个线程下载数据的起始位置。
private long[] endPos;//保留每个线程下载数据的截⽌位置。
private boolean bool = false;
private URL url = null;
//有参构造函数,先构造需要的数据
public MultiTheradDownLoad(String urlStr, int threadNum,File file) {
this.urlStr = urlStr;
this.threadNum = threadNum;
startPos = new long[this.threadNum];
endPos = new long[this.threadNum];
latch = new CountDownLatch(this.threadNum);
this.file=file;
this.Name();
}
/*
* 组织断点续传功能的⽅法
*/
public File downloadPart() {
//File file = null;
File tmpfile = null;
//设置HTTP⽹络访问代理
System.setProperty("http.proxySet", "true");
大文件发送System.setProperty("http.proxyHost", "proxy3.bj.petrochina");
System.setProperty("http.proxyPort", "8080");
//从⽂件链接中获取⽂件名,此处没考虑⽂件名为空的情况,此种情况可能需使⽤UUID来⽣成⼀个唯⼀数来代表⽂件名。//        filename = urlStr.substring(urlStr.lastIndexOf('/') + 1, urlStr
//                .contains("?") ? urlStr.lastIndexOf('?') : urlStr.length());
tmpfilename = filename + "_tmp";