目录
1、使用场景
因为最近在做把以前在百度公有云上的音视频和文档文件,需要迁移阿里云上。这里面还有一个小插曲;有位同事想出办法说邮递一个移动硬盘到百度云让直接Copy到移动硬盘之中。按照正规流程这个肯定是不可能的吧,作为一个大企业;必须的符合规范方式操作吧。个人意见应该可以通过对应销售人员或者公司里面专门联系一个百度的对接人员;到百度公司协商作为一个迁移项目形式;该付款付款。这样还可能实现。最后本人只能通过使用Baidu提供API文档;获得原始音视频文件的网络Url路径。自己写相关的多线程下载文件。最后分别把累加账号上230G音视频文件下载完毕;另外一个账号850G下载完毕。
2、多线程下载原理
- 客户端要下载一个文件, 首先请求服务器,服务器将这个文件传送给客户端,客户端保存到本地, 完成了一个下载的过程.
- 多线程下载的思想是客户端开启多个线程同时下载,每个线程只负责下载文件的一部分, 当所有线程下载完成的时候,文件下载完毕.
- 并不是线程越多下载越快, 与网络环境有很大的关系
- 在同等的网络环境下,多线程下载速度要高于单线程.
- 多线程下载占用资源比单线程多,相当于用资源换取速度
多线程下载技术是很常见的一种下载方案,这种方式充分利用了多线程的优势,在同一时间段内通过多个线程发起下载请求,将需要下载的数据分割成多个部分,每一个线程只负责下载其中一个部分,然后将下载后的数据组装成完整的数据文件,这样便大大加快了下载效率。常见的下载器,迅雷,QQ旋风等都采用了这种技术。
3、请求如何分段下载
3.1、需要请求的数据如何分段。
Range,是在 HTTP/1.1里新增的一个 header field,它允许客户端实际上只请求文档的一部分,或者说某个范围。
有了范围请求,HTTP 客户端可以通过请求曾获取失败的实体的一个范围(或者说一部分),来恢复下载该实体。当然这有一个前提,那就是从客户端上一次请求该实体到这次发出范围请求的时段内,该对象没有改变过。例如:
GET /bigfile.html HTTP/1.1
Host: www.joes-hardware.com
Range: bytes=4000-
User-Agent: Mozilla/4.61 [en] (WinNT; I)
上述请求中,客户端请求的是文档开头 4000 字节之后的部分(不必给出结尾字节数,因为请求方可能不知道文档的大小)。在客户端收到了开头的 4000 字节之后就失败的情况下,可以使用这种形式的范围请求。还可以用 Range 首部来请求多个范围(这些范围可以按任意顺序给出,也可以相互重叠)。
Range头域使用形式如下。例如:
表示头500个字节:bytes=0-499
表示第二个500字节:bytes=500-999
表示最后500个字节:bytes=-500
表示500字节以后的范围:bytes=500-
第一个和最后一个字节:bytes=0-0,-1
服务器接收到线程3的请求报文,发现这是一个带有Range头的GET请求,
如果一切正常,服务器的**响应报文会有下面这行:
HTTP/1.1 206 OK**
表示处理请求成功,响应报文还有这一行
Content-Range: bytes 200-299/403
斜杠后面的403表示文件的大小
Http协议的发展历程
HTTP协议到现在为止总共经历了3个版本的演化,第一个HTTP协议诞生于1989年3月。
xml属性 | 描述 |
---|---|
HTTP/0.9 | 1991年 |
HTTP/1.0 | 1992-1996年 |
HTTP/1.1 | 1997-1999年 |
HTTP/2.0 | 2012-2014年 |
也就是HTTP/1.1 从1997-1999 年就应用了,所以现在基本上是支持断点续传的。
3.2、分段下载的数据如何组装成完整的数据文件。
随机访问文件RandomAccessFile类
RandomAccessFile适用于由大小已知的记录组成的文件,所以我们可以使用seek()将记录从一处转移到另一处,然后读取或修改记录。
随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。
通过UrlConnection下载部分资源。
注意:
1.需要Range头,key:Range value:bytes:0-499
urlconnection.setRequestPropety("Range","bytes:0-499")
2.需要设置每个线程在本地文件的保存的开始位置
RandomAccessFile randomfile =new RandomAccessFile(File file,String mode)
randomfile.seek(int startPostion);//本次线程下载保存的开始位置。
创建从中读取和向其中写入(可选)的随机访问文件流,该文件由 File 参数指定。将创建一个新的 FileDescriptor 对象来表示此文件的连接。
mode 参数指定用以打开文件的访问模式。允许的值及其含意为:
“r“——以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。
“rw“——打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。
“rws“—— 打开以便读取和写入,对于 “rw”,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
“rwd“——打开以便读取和写入,对于 “rw”,还要求对文件内容的每个更新都同步写入到底层存储设备。
4、关键代码实现
DownloadConstans.java
package com.wdcloud.publiccloud.files.tool.download.filedownload;
import java.util.concurrent.*;
/**
* @Description
* @auther jianxiapc
* @create 2019-08-20 11:20
*/
public class DownloadConstans {
public static final int MAX_THREAD_COUNT = getSystemProcessCount();
private static final int MAX_IMUMPOOLSIZE = MAX_THREAD_COUNT;
/**
* 自定义线程池
*/
private static ExecutorService MY_THREAD_POOL;
/**
* 自定义线程池
*/
public static ExecutorService getMyThreadPool(){
if(MY_THREAD_POOL == null){
MY_THREAD_POOL = Executors.newFixedThreadPool(MAX_IMUMPOOLSIZE);
}
return MY_THREAD_POOL;
}
// 线程池
private static ThreadPoolExecutor threadPool;
/**
* 单例,单任务 线程池
* @return
*/
public static ThreadPoolExecutor getThreadPool(){
if(threadPool == null){
threadPool = new ThreadPoolExecutor(MAX_IMUMPOOLSIZE, MAX_IMUMPOOLSIZE, 3, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(16),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
return threadPool;
}
/**
* 获取服务器cpu核数
* @return
*/
private static int getSystemProcessCount(){
//int count =Runtime.getRuntime().availableProcessors();
//仅仅只启动4个线程进行下载
int count=4;
return count;
}
}
FileMultiPartDownLoad.java
package com.wdcloud.publiccloud.files.tool.download.filedownload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description
* @auther jianxiapc
* @create 2019-08-20 11:02
*/
public class FileMultiPartDownLoad {
private static Logger logger = LoggerFactory.getLogger(FileMultiPartDownLoad.class);
/**
* 线程下载成功标志
*/
private static int flag = 0;
/**
* 服务器请求路径
*/
private String netWorkFileUrlPath;
/**
* 本地路径
*/
private String localPath;
/**
* 线程计数同步辅助
*/
private CountDownLatch latch;
// 定长线程池
private static ExecutorService threadPool;
public FileMultiPartDownLoad(String netWorkFileUrlPath, String localPath) {
this.netWorkFileUrlPath = netWorkFileUrlPath;
this.localPath = localPath;
}
public boolean executeDownLoad() {
try {
URL url = new URL(netWorkFileUrlPath);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);//设置超时时间
conn.setRequestMethod("GET");//设置请求方式
conn.setRequestProperty("Connection", "Keep-Alive");
int code = conn.getResponseCode();
if (code != 200) {
logger.error(String.format("无效网络地址:%s", netWorkFileUrlPath));
return false;
}
//服务器返回的数据的长度,实际上就是文件的长度,单位是字节
// int length = conn.getContentLength(); //文件超过2G会有问题
long length = getRemoteFileSize(netWorkFileUrlPath);
logger.info("文件总长度:" + length + "字节(B)");
RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");
//指定创建的文件的长度
raf.setLength(length);
raf.close();
//分割文件
int partCount = DownloadConstans.MAX_THREAD_COUNT;
int partSize = (int)(length / partCount);
latch = new CountDownLatch(partCount);
threadPool = DownloadConstans.getMyThreadPool();
for (int threadId = 1; threadId <= partCount; threadId++) {
// 每一个线程下载的开始位置
long startIndex = (threadId - 1) * partSize;
// 每一个线程下载的结束位置
long endIndex = startIndex + partSize - 1;
if (threadId == partCount) {
//最后一个线程下载的长度稍微长一点
endIndex = length;
}
logger.info("线程" + threadId + "下载:" + startIndex + "字节~" + endIndex + "字节");
threadPool.execute(new DownLoadThread(threadId, startIndex, endIndex, latch));
}
latch.await();
if(flag == 0){
return true;
}
} catch (Exception e) {
logger.error(String.format("文件下载失败,文件地址:%s,失败原因:%s", netWorkFileUrlPath, e.getMessage()), e);
}
return false;
}
/**
* 内部类用于实现下载
*/
public class DownLoadThread implements Runnable {
private Logger logger = LoggerFactory.getLogger(DownLoadThread.class);
/**
* 线程ID
*/
private int threadId;
/**
* 下载起始位置
*/
private long startIndex;
/**
* 下载结束位置
*/
private long endIndex;
private CountDownLatch latch;
public DownLoadThread(int threadId, long startIndex, long endIndex, CountDownLatch latch) {
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.latch = latch;
}
@Override
public void run() {
try {
//logger.info("线程" + threadId + "正在下载...");
URL url = new URL(netWorkFileUrlPath);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestMethod("GET");
//请求服务器下载部分的文件的指定位置
conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
//logger.info("线程" + threadId + "请求返回code=" + code);
InputStream is = conn.getInputStream();//返回资源
RandomAccessFile raf = new RandomAccessFile(localPath, "rwd");
//随机写文件的时候从哪个位置开始写
raf.seek(startIndex);//定位文件
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
raf.write(buffer, 0, len);
}
is.close();
raf.close();
logger.info("线程" + threadId + "下载完毕");
} catch (Exception e) {
//线程下载出错
FileMultiPartDownLoad.flag = 1;
logger.error(e.getMessage(),e);
} finally {
//计数值减一
latch.countDown();
}
}
}
/**
* 内部方法,获取远程文件大小
* @param remoteFileUrl
* @return
* @throws IOException
*/
private long getRemoteFileSize(String remoteFileUrl) throws IOException {
long fileSize = 0;
HttpURLConnection httpConnection = (HttpURLConnection) new URL(remoteFileUrl).openConnection();
httpConnection.setRequestMethod("HEAD");
int responseCode = 0;
try {
responseCode = httpConnection.getResponseCode();
} catch (IOException e) {
e.printStackTrace();
}
if (responseCode >= 400) {
logger.debug("Web服务器响应错误!");
return 0;
}
String sHeader;
for (int i = 1;; i++) {
sHeader = httpConnection.getHeaderFieldKey(i);
if (sHeader != null && sHeader.equals("Content-Length")) {
fileSize = Long.parseLong(httpConnection.getHeaderField(sHeader));
break;
}
}
return fileSize;
}
/**
* 下载文件执行器
* @param netWorkFileUrlPath
* @param localDirPath
* @param fileName
* @return
*/
public synchronized static String downLoad(String netWorkFileUrlPath,String localDirPath,String fileName) {
ReentrantLock lock = new ReentrantLock();
lock.lock();
String[] names = netWorkFileUrlPath.split("\\.");
if (names == null || names.length <= 0) {
return null;
}
String fileTypeName = names[names.length - 1];
String localStorageDirPath =localDirPath+"/" +fileName;
System.out.println("localStorageDirPath: "+localStorageDirPath);
FileMultiPartDownLoad m = new FileMultiPartDownLoad(netWorkFileUrlPath, localStorageDirPath);
long startTime = System.currentTimeMillis();
boolean flag = false;
try{
flag = m.executeDownLoad();
long endTime = System.currentTimeMillis();
if(flag){
logger.info(fileName+" : 文件下载结束,共耗时" + (endTime - startTime)+ "ms");
return localStorageDirPath;
}
logger.warn("文件下载失败");
return null;
}catch (Exception ex){
logger.error(ex.getMessage(),ex);
return null;
}finally {
FileMultiPartDownLoad.flag = 0; // 重置 下载状态
if(!flag){
File file = new File(localStorageDirPath);
file.delete();
}
lock.unlock();
}
}
}
调用方法代码
/**
* 首先通过调用百度SDK API接口,获得基本信息,然后使用多线性 下载单个vod视频文件
* @param bceClient
* @param vodMediaId 视频id
* @param fileStorageDiskPath 存储下载文件路径
* @param excelFileName 下载后保存文件信息得excel
* @param expiredInSeconds 过期时间默认3600s
*/
public void downloadSingleVodMediaFile (VodClient bceClient, String vodMediaId,String fileStorageDiskPath,String excelFileName,long expiredInSeconds) {
logger.info("vodMediaId = " + vodMediaId);
GetMediaSourceDownloadResponse response = bceClient.getMediaSourceDownload(vodMediaId,expiredInSeconds);
String netWorkFileUrl = response.getSourceUrl();
logger.info("netWorkFileUrl = " + netWorkFileUrl);
//测试线程下载和多线线程下载
Date startDate = new Date();
long downLoadStartTime=System.currentTimeMillis();
//System.out.println("downLoadStartTime: "+downLoadStartTime);
logger.info("downLoadStartTime: "+sdf.format(startDate));
//OkHttpDownloadUtil.downNetWorkFile(netWorkFileUrl,fileStorageDiskPath,"single.mp4");
Map<String, Object> vodFileInfoMap = getVodFileInfoByVodId(bceClient, vodMediaId);
String fileName =vodFileInfoMap.get("title").toString();
//针对当个文件下载的函数调用
FileMultiPartDownLoad.downLoad(netWorkFileUrl,fileStorageDiskPath,fileName);
Date endDate = new Date();
long downLoadEndTime=System.currentTimeMillis();
long customDownloadTime=downLoadEndTime-downLoadStartTime;
String downloadTimeFormat=CommonConvertUtils.formatMillisTime(customDownloadTime);
//System.out.println("downloadTimeFormat: "+downloadTimeFormat);
logger.info("文件 "+vodMediaId+" 下载开始时间"+sdf.format(startDate)+" 下载完成时间:"+sdf.format(endDate));
logger.info("文件 "+vodMediaId+" downloadTimeFormat: "+downloadTimeFormat);
}
3、成果展现
4、总结
通过本次下载文件相关代码编写深刻认识了多线程下载的有点;同时也系统性学习一下如何多线程下载文件。同时也实践如何使用多线程去下载文件。