您的位置:首页 > 理论基础 > 计算机网络

HttpURLConnection之多线程下载

2017-09-20 10:43 176 查看

一 前言

     
 在前面的一篇文章中已经简单介绍过HttpURLconnection(点击查看)的使用, 这篇文章主要使用HttpURLconnection实现多文件下载,在做App开发时,我们知道文件下载是很重要的一部分(例如apk的更新),有人会说那么多第三方框架可以为我们所用,封装的很好并且使用起来非常方便,何必再去探究它呢,以前我也是这么认为的,不过在你使用这些框架时,你有没有发现遇到问题总是那么的毫无头绪,不知道怎么解决,不知道其内部又是怎么实现的了。虽然开源的框架封装的很好,个人认为还是很有必要研究这些基本的东西,熟知了这些再去使用那些框架会得心应手,何况一些大的公司在安全开发的前提下,做项目基本还都是使用这些基本的东西。

        好,废话多了,现在进入正题,所谓多线程下载就是对同样的一个文件,使用N(0,1,2....)个线程去下载这个文件,这样就很好的提高下载效率。例如一个6M的文件,如果我使用单线程下载可能需要的时间30s,该线程所承担的下载任务为6M,但在相同的情况下如果使用三个线程下载,那么每个线程所承担的任务就是2M,下载的时间将会缩短到10s,这样一来就节省了大把时间。如下:



          每个线程任务少,干起活来也不累,当然也不是开的线程越多越好,那你的设备也得能够承受住啊,不然直接崩溃了,个人认为3~5还是可以的。

二 实现

       1 获取文件总的大小

      要想知道每个线程的下载任务,首先要获取要下载资源文件的总大小 ,代码如下:

       

public void download(){
try {
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
//            connection.setDoInput(true);
connection.setRequestProperty("Accept","image/gif, image/jpeg, image/pjpeg, image/pjpeg, " +
"application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, " +
"application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, " +
"application/vnd.ms-powerpoint, application/msword,text/xml , text/html ,application/json ,*/*");
connection.setRequestProperty("Accept-Language","zh-CN");
//            connection.setRequestProperty("Accept-Charset","UTF-8");
connection.setRequestProperty("Connection","Keep-Alive");
//            connection.setRequestProperty("Referer", path);// 先前网页的地址,当前请求网页紧随其后,即来路
connection.setRequestProperty("Charset", "UTF-8");
//            connection.connect();
if (connection.getResponseCode() == 200){
//得到文件大小
fileSize = connection.getContentLength();
}else {
Log.e("FileDownUtil======>","网络异常");
return;
}
.......
} catch (MalformedURLException e) {
Log.e("download=======>",e.getMessage());
} catch (IOException e) {
Log.e("download=======>",e.getMessage());
}catch (InterruptedException e) {
//                    e.printStackTrace();
Log.e("download=======>",e.getMessage());

}

}


说明,通过与服务器建立连接,再调用URLConnection#getContentLength方法获取资源文件的大小。

2 计算每个线程所需下载的任务

    代码如下:

int currentPartSize = fileSize % threadNum == 0 ? fileSize / threadNum : fileSize / threadNum + 1;

     说明,fileSize为资源文件总的大小,threadNum为线程数(这个数是自定义的),如果fileSize为threadNum整数倍,那么每个线程所需下载的文件的大小为fileSize / threadNum,否则就是 fileSize / threadNum + 1,必须得加上1,不然到最后虽然显示下载完了可还有极小的部分没有下载了。

3 定义子线程

          该线程的定义和单线程下载基本是一样的,区别是: 该线程要指定下载的位置和结束的位置,而单线程下载开始的位置默认是0,结束点就是文件的大小。核心代码如下:

public void run() {
super.run();
try {
URL url = new URL(path);
HttpURLConnection connection  = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
//                connection.setDoInput(true);
connection.setRequestProperty("Accept","image/gif, image/jpeg, image/pjpeg, image/pjpeg, " +
"application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, " +
"application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, " +
"application/vnd.ms-powerpoint, application/msword,text/xml , text/html ,application/json ,*/*");
connection.setRequestProperty("Accept-Language","zh-CN");
connection.setRequestProperty("Accept-Charset","UTF-8");
connection.setRequestProperty("Connection","Keep-Alive");
connection.setRequestProperty(
"User-Agent",
"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; " +
".NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30;" +
" .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
//                connection.setRequestProperty("Range", "bytes=" + startPos + "-" + (startPos + currentPartSize));//设置获取实体数据的范围
//                connection.setRequestProperty("Referer", path);// 先前网页的地址,当前请求网页紧随其后,即来路
connection.setRequestProperty("Charset", "UTF-8");
InputStream is = connection.getInputStream();
//跳过startPos个字节,表明该线程只下载自己负责哪部分文件
is.skip(this.startPos);
byte[] buffer = new byte[1024];
int hasRead = 0;
//读取网络数据,并写入本地文件
while (length < currentPartSize && (hasRead = is.read(buffer)) > 0){
currentPart.write(buffer,0 ,hasRead);
//累计该线程下载的总大小
length += hasRead;
}
currentPart.close();
is.close();
isFinish = true;
} catch (MalformedURLException e) {
length = -1;//下载失败
Log.e("DownThread=======>",e.getMessage());
} catch (IOException e) {
Log.e("DownThread=======>",e.getMessage());
length = -1;//下载失败
}
}

       说明,startPos为该线程开始下载的位置,length为该线程已经下载的长度,通过调用skip()函数,直接跳转到该线程的下载点,忽略前面的内容,length 到currentPartSize由该线程负责下载,length 大于等于currentPartSize的由下面的复杂下载,其实我们还可以在获取资源时,再请求头部设置获取资源的范围,这样就不必做上述判断,直接进行I/O操作就可以了,需要添加的代码如下:

connection.setRequestProperty("Range", "bytes=" + startPos + "-" + (startPos + currentPartSize));//设置获取实体数据的范围

4 启动线程组开始下载

核心代码如下:

for (int i = 0; i < threadNum; i++) {
//计算每条线程的下载开始位置
int startPos = i * currentPartSize;
//每个线程使用一个RandomAccessFile进行下载
RandomAccessFile currentPart = new RandomAccessFile(targetFile,"rwd");
//定位该线程的下载位置
currentPart.seek(startPos);
//创建下载线程
threads[i] = new DownThread(startPos,currentPartSize,currentPart);
//开始下载
threads[i].start();
}

说明,threads就是线程组,计算每个线程下载的位置i * currentPartSize,在创建子线程时,把下载位置startPos传递过去,再调用seek()函数设置资源在本地保存的位置,这样每个线程就可以开始工作了。

5 自定义接口监听资源下载进度

接口代码如下:

/**
*  下载进度监听
*/
public interface DownloadProgressListener {
/**
* @param downloadSize 已经下载的文件的大小
* @param fileSize 文件总的大小
*/
void onDownloadSize(int downloadSize,int fileSize);
}

/**
* 设置下载进度监听
* @param lintener
*/
public void setOnDownloadProgressListener(DownloadProgressListener lintener){
downloadProgressListener = lintener;
}

进度监听的核心代码:

//文件是否下载完成 false 下载完成 否则为下载完成
boolean isNotComplete = true;
/*已经下载的文件的长度*/
int downloadLength = 0 ;
while (isNotComplete){
downloadLength = 0;
Thread.sleep(500);
isNotComplete = false;//假定全部线程下载完成
for (int i = 0; i < threads.length; i++) {
if (this.threads[i] != null && !this.threads[i].isFinish()) {//如果发现线程未完成下载
isNotComplete = true;
if (this.threads[i].getLength() == -1) {//如果下载失败,再重新下载
//计算每条线程的下载开始位置
int startPos = i * currentPartSize;
//每个线程使用一个RandomAccessFile进行下载
RandomAccessFile currentPart = new RandomAccessFile(targetFile,"rwd");
//定位该线程的下载位置
currentPart.seek(startPos);
//创建下载线程
threads[i] = new DownThread(startPos,currentPartSize,currentPart);
this.threads[i].setPriority(7);
this.threads[i].start();
}
}
//计算已下载文件的大小
if (this.threads[i] != null && this.threads[i].getLength() != -1){
downloadLength += this.threads[i].getLength();
}
}
if (downloadProgressListener != null){
downloadProgressListener.onDownloadSize(downloadLength,fileSize);
}
}
//走到这里证明文件已经下载完成 那么已经下载的大小就是文件的大小
if (downloadProgressListener != null){
downloadProgressListener.onDownloadSize(fileSize,fileSize);
}

说明,为保存文件的完好的下载(可能由于某些原因某个线程下载失败),使用了一个循环,一旦发现某个线程有问题就重新下载,计算加载的进度通过如下代码:

//计算已下载文件的大小
if (this.threads[i] != null && this.threads[i].getLength() != -1){
downloadLength += this.threads[i].getLength();
}

        在每个线程开始工作时,就开始记录每个线程下载的长度,成员变量length,这时只需要把每个线程已下载的长度累加在一起就是整个线程组下载的总长度。因此回调代码可以如下写:

if (downloadProgressListener != null){
downloadProgressListener.onDownloadSize(downloadLength,fileSize);
}

       好,到这里多线程下载实现的步骤算是完成了,本人再贡献出完整代码,点击链接可以查看。

多线程下载实现完整代码

三 使用

      使用一个示例看看上面的工具类能否正确工作,这个示例也很简单就是使用一个TextView显示总文件的大小和下载进度。

1 动态判断权限

      自android 6.0以后有些权限需要动态权限,例如这里的访问手机的储存空间,先在配置文件中添加如下权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

     然后就是动态判断权限:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){//如果api 23 需要判断用户是否已授权
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_EXTERNAL_STORAGE) == -1 &&
ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE) == -1  ){
startDownload();//用户已授权 开始下载
}else {//未授权 申请所需权限
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE},0x123);
}
}else {//用户已授权 开始下载
startDownload();
}

     处理结果,需要重写onRequestPermissionsResult方法:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 0x123){
if (grantResults[0] == 0 && grantResults[1] == 0){
Log.e("已授权: ","存储空间读取权限!");
startDownload();
}
}
}

2 开始下载,并设置监听

/**
* 开始下载
*/
private void startDownload() {
saveFilePath = new File(getCachePath(),"temp.jpg").getAbsolutePath();
fileDownUtil = FileDownUtil.getInstance(path,saveFilePath,3);
//设置下载进度监听
fileDownUtil.setOnDownloadProgressListener(new FileDownUtil.DownloadProgressListener() {
@Override
public void onDownloadSize(int downloadSize, int fileSize) {
updateUi("文件总大小:"+ fileSize + "\n" + "已下载:" + downloadSize);
}
});
download();
}

/**
* 获取缓存路径
* @return
*/
private String getCachePath(){
String appPath = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
appPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "com.ecric.http";
}else {
appPath = Environment.getDataDirectory().getAbsolutePath() + File.separator + "com.ecric.http";
}
File file = new File(appPath);
if (!file.exists()){
file.mkdirs();
}
Log.e("文件路径====》",appPath);
return appPath;
}

    说明,示例完整代码点击查看。

示例完整代码

                
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息