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

网络多线程断点下载

2011-09-20 15:39 281 查看
我们编写的是Andorid的HTTP多线程断点下载应用程序。因为之间我们学习的学习积累,直接使用单线程下载HTTP文件对我们来说是一件非常简单的事。那么,多线程断点下载的难点在哪里?1.多线程下载,2.支持断点。

多线程下载:



如何才能从文件的指定位置处开始下载文件?(比如从50MB开始)这一点我们可以通过HTTP请求信息头来设置,还记得HTTP请求信息头的“Range”属性吗?

断点:

首要问题(多线程下载)已经被我们解决了,支持断点下载想必大家也已经想到了。就是将下载的进度保存到文件中,但在Android中却不能这么做。通过老黎的试验,在Android平台中,我们需要向文件中写出下载的文件数据,还需要向另一个文件中写出下载进度,这样会出错。这样会导致有一个文件的内容没有被写出。所以我们就不能以文件的方式来保存下载进度,但可以通过数据库的方式保存下载进度。

这两大问题我们已经有了解决思路,那么就开始动手编写吧!

1.创建Android工程

Project name:MulThreadDownloader

BuildTarget:Android2.1

Application name:多线程断点下载

Package name:com.changcheng.download

Create Activity:MulThreadDownloader

Min SDK Version:7

2.AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.changcheng.download"

android:versionCode="1"

android:versionName="1.0">

<application android:icon="@drawable/icon" android:label="@string/app_name">

<activity android:name=".MulThreadDownloader"

android:label="@string/app_name">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

<uses-sdk android:minSdkVersion="7" />

<!-- 在SDCard中创建与删除文件权限 -->

<uses-permissionandroid:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

<!-- 往SDCard写入数据权限 -->

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

<!-- 访问internet权限 -->

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

</manifest>

3.strings.xml

<?xml version="1.0" encoding="utf-8"?>

<resources>

<string name="hello">Hello
World, DownloadActivity!</string>

<string name="app_name">多线程断点下载</string>

<string name="path">下载路径</string>

<string name="downloadbutton">下载</string>

<string name="sdcarderror">SDCard不存在或者写保护</string>

</resources>

4.main.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

>

<!-- 下载路径 -->

<TextView

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="@string/path"

/>

<EditText

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="http://www.winrar.com.cn/download/wrar380sc.exe"

android:id="@+id/path"

/>

<!-- 下载按钮 -->

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="@string/downloadbutton"

android:id="@+id/button"

/>

<!-- 进度条 -->

<ProgressBar

android:layout_width="fill_parent"

android:layout_height="20dip"

style="?android:attr/progressBarStyleHorizontal"

android:id="@+id/downloadbar"/>

<!-- 进度%
-->

<TextView

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:gravity="center"

android:id="@+id/resultView"

/>

</LinearLayout>

5.MulThreadDownloader

package com.changcheng.download;

import java.io.File;

import com.changcheng.net.download.DownloadProgressListener;

import com.changcheng.net.download.FileDownloader;

import com.changcheng.download.R;

import android.app.Activity;

import android.os.Bundle;

import android.os.Environment;

import android.os.Handler;

import android.os.Message;

import android.view.View;

import android.widget.Button;

import android.widget.EditText;

import android.widget.ProgressBar;

import android.widget.TextView;

import android.widget.Toast;

public class MulThreadDownloader extends Activity
{

private EditText pathText;

private ProgressBar progressBar;

private TextView resultView;

private Handler handler = new Handler(){

@Override

public void handleMessage(Message
msg) {

if(!Thread.currentThread().isInterrupted()){

switch (msg.what)
{

case 1:

// 获取当前文件下载的进度

int size
= msg.getData().getInt("size");

progressBar.setProgress(size);

int result
= (int)(((float)size/(float)progressBar.getMax())
* 100);

resultView.setText(result+ "%");

if(progressBar.getMax()
== size){

Toast.makeText(MulThreadDownloader.this, "文件下载完成",
1).show();

}

break;

case -1:

String
error = msg.getData().getString("error");

Toast.makeText(MulThreadDownloader.this,
error, 1).show();

break;

}

}

super.handleMessage(msg);

}

};

@Override

public void onCreate(Bundle
savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

pathText =
(EditText)this.findViewById(R.id.path);

progressBar =
(ProgressBar)this.findViewById(R.id.downloadbar);

resultView =
(TextView)this.findViewById(R.id.resultView);

Button button = (Button)this.findViewById(R.id.button);

button.setOnClickListener(new View.OnClickListener()
{

@Override

public void onClick(View
v) {

String
path = pathText.getText().toString();

if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){

//下载文件需要很长的时间,主线程是不能够长时间被阻塞,如果主线程被长时间阻塞,
那么Android被回收应用

download(path,
Environment.getExternalStorageDirectory());

}else{

Toast.makeText(MulThreadDownloader.this,
R.string.sdcarderror,
1).show();

}

}

});

}

/**

* 下载文件

* @param path 下载路径

* @param saveDir 文件保存目录

*/

//对于Android的UI控件,只能由主线程负责显示界面的更新,其他线程不能直接更新UI控件的显示

public void download(final String
path, final File
saveDir){

new Thread(new Runnable()
{

@Override

public void run()
{

FileDownloader
downer = newFileDownloader(MulThreadDownloader.this,
path, saveDir, 3);

progressBar.setMax(downer.getFileSize());//设置进度条的最大刻度

try {

downer.download(new DownloadProgressListener(){

@Override

public void onDownloadSize(int size)
{

Message
msg = new Message();

msg.what =
1;

msg.getData().putInt("size",
size);

handler.sendMessage(msg);//发送消息

}});

} catch (Exception
e) {

Message
msg = new Message();

msg.what =
-1;

msg.getData().putString("error", "下载失败");

handler.sendMessage(msg);

}

}

}).start();

}

}

6.FileDownload

package com.changcheng.net.download;

import java.io.File;

import java.io.RandomAccessFile;

import java.net.HttpURLConnection;

import java.net.URL;

import java.util.LinkedHashMap;

import java.util.Map;

import java.util.UUID;

import java.util.concurrent.ConcurrentHashMap;

import java.util.regex.Matcher;

import java.util.regex.Pattern;

import com.changcheng.download.service.FileService;

import android.content.Context;

import android.util.Log;

/**

* 文件下载器

* @author lihuoming@sohu.com

*

*/

public class FileDownloader
{

private Context context;

private FileService fileService;

private static final String TAG = "FileDownloader";

/* 已下载文件大小 */

private int downloadSize =
0;

/* 原始文件大小 */

private int fileSize =
0;

/* 线程数 */

private DownloadThread[] threads;

/* 下载路径 */

private URL url;

/* 本地保存文件 */

private File saveFile;

/* 下载记录文件 */

private File logFile;

/* 缓存各线程最后下载的位置*/

private Map<Integer,
Integer> data = new ConcurrentHashMap<Integer,
Integer>();

/* 每条线程下载的大小 */

private int block;

private String downloadUrl;//下载路径

/**

* 获取线程数

*/

public int getThreadSize()
{

return threads.length;

}

/**

* 获取文件大小

* @return

*/

public int getFileSize()
{

return fileSize;

}

/**

* 累计已下载大小

* @param size

*/

protected synchronized void append(int size)
{

downloadSize +=
size;

}

/**

* 更新指定线程最后下载的位置

* @param threadId 线程id

* @param pos 最后下载的位置

*/

protected void update(int threadId, int pos)
{

this.data.put(threadId,
pos);

}

/**

* 保存记录文件

*/

protected synchronized void saveLogFile()
{

this.fileService.update(this.downloadUrl, this.data);

}

/**

* 构建文件下载器

* @param downloadUrl 下载路径

* @param fileSaveDir 文件保存目录

* @param threadNum 下载线程数

*/

public FileDownloader(Context
context, String downloadUrl, File fileSaveDir, intthreadNum)
{

try {

this.context =
context;

this.downloadUrl =
downloadUrl;

fileService = new FileService(context);

this.url = new URL(downloadUrl);

if(!fileSaveDir.exists())
fileSaveDir.mkdirs();

this.threads = newDownloadThread[threadNum];

HttpURLConnection
conn = (HttpURLConnection)url.openConnection();

conn.setConnectTimeout(6*1000);

conn.setRequestMethod("GET");

conn.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, */*");

conn.setRequestProperty("Accept-Language", "zh-CN");

conn.setRequestProperty("Referer",
downloadUrl);

conn.setRequestProperty("Charset", "UTF-8");

conn.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)");

conn.setRequestProperty("Connection", "Keep-Alive");

conn.connect();

printResponseHeader(conn);

if (conn.getResponseCode()==200)
{

this.fileSize =
conn.getContentLength();//根据响应获取文件大小

if (this.fileSize <=
0) throw new RuntimeException("无法获知文件大小 ");

String
filename = getFileName(conn);

this.saveFile = new File(fileSaveDir,
filename);/* 保存文件 */

Map<Integer,
Integer> logdata =fileService.getData(downloadUrl);

if(logdata.size()>0){

data.putAll(logdata);

}

this.block = this.fileSize / this.threads.length +
1;

if(this.data.size()==this.threads.length){

for (int i
= 0; i < this.threads.length;
i++) {

this.downloadSize += this.data.get(i+1)-(this.block *
i);

}

print("已经下载的长度"+ this.downloadSize);

}

}else{

throw new RuntimeException("服务器响应错误 ");

}

} catch (Exception
e) {

print(e.toString());

throw new RuntimeException("连接不到下载路径 ");

}

}

/**

* 获取文件名

*/

private String
getFileName(HttpURLConnection conn) {

String filename =this.url.toString().substring(this.url.toString().lastIndexOf("/")
+ 1);

if(filename==null || "".equals(filename.trim())){//如果获取不到文件名称

for (int i
= 0;; i++) {

String
mine = conn.getHeaderField(i);

if (mine
== null) break;

if("content-disposition".equals(conn.getHeaderFieldKey(i).toLowerCase())){

Matcher
m = Pattern.compile(".*filename=(.*)").matcher(mine.toLowerCase());

if(m.find()) return m.group(1);

}

}

filename
= UUID.randomUUID()+ ".tmp";//默认取一个文件名

}

return filename;

}

/**

* 开始下载文件

* @param listener 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null

* @return 已下载文件大小

* @throws Exception

*/

public int download(DownloadProgressListener
listener) throws Exception{

try {

if(this.data.size()
!= this.threads.length){

this.data.clear();

for (int i
= 0; i < this.threads.length;
i++) {

this.data.put(i+1, this.block *
i);

}

}

for (int i
= 0; i < this.threads.length;
i++) {

int downLength
= this.data.get(i+1)
- (this.block *
i);

if(downLength
< this.block && this.data.get(i+1)<this.fileSize){//该线程未完成下载时,继续下载

RandomAccessFile
randOut = newRandomAccessFile(this.saveFile, "rw");

if(this.fileSize>0)
randOut.setLength(this.fileSize);

randOut.seek(this.data.get(i+1));

this.threads[i]
= new DownloadThread(this, this.url,
randOut, this.block, this.data.get(i+1),
i+1);

this.threads[i].setPriority(7);

this.threads[i].start();

}else{

this.threads[i]
= null;

}

}

this.fileService.save(this.downloadUrl, this.data);

boolean notFinish
= true;//下载未完成

while (notFinish)
{// 循环判断是否下载完毕

Thread.sleep(900);

notFinish
= false;//假定下载完成

for (int i
= 0; i < this.threads.length;
i++){

if (this.threads[i]
!= null &&
!this.threads[i].isFinish())
{

notFinish
= true;//下载没有完成

if(this.threads[i].getDownLength()
== -1){//如果下载失败,再重新下载

RandomAccessFile
randOut = newRandomAccessFile(this.saveFile, "rw");

randOut.seek(this.data.get(i+1));

this.threads[i]
= new DownloadThread(this,this.url,
randOut, this.block, this.data.get(i+1),
i+1);

this.threads[i].setPriority(7);

this.threads[i].start();

}

}

}

if(listener!=null)
listener.onDownloadSize(this.downloadSize);

}

fileService.delete(this.downloadUrl);

} catch (Exception
e) {

print(e.toString());

throw new Exception("下载失败");

}

return this.downloadSize;

}

/**

* 获取Http响应头字段

* @param http

* @return

*/

public static Map<String,
String> getHttpResponseHeader(HttpURLConnection http) {

Map<String, String>
header = new LinkedHashMap<String,
String>();

for (int i
= 0;; i++) {

String mine
= http.getHeaderField(i);

if (mine
== null) break;

header.put(http.getHeaderFieldKey(i),
mine);

}

return header;

}

/**

* 打印Http头字段

* @param http

*/

public static void printResponseHeader(HttpURLConnection
http){

Map<String, String>
header = getHttpResponseHeader(http);

for(Map.Entry<String,
String> entry : header.entrySet()){

String key
= entry.getKey()!=null ?
entry.getKey()+ ":" : "";

print(key+
entry.getValue());

}

}

private static void print(String
msg){

Log.i(TAG,
msg);

}

}

7.DownloadProgressListener

package com.changcheng.net.download;

public interface DownloadProgressListener
{

public void onDownloadSize(int size);

}

8.FileService

package com.changcheng.download.service;

import java.util.HashMap;

import java.util.Map;

import android.content.Context;

import android.database.Cursor;

import android.database.sqlite.SQLiteDatabase;

/**

* 业务bean

*

*/

public class FileService
{

private DBOpenHelper openHelper;

public FileService(Context
context) {

openHelper = new DBOpenHelper(context);

}

/**

* 获取线程最后下载位置

* @param path

* @return

*/

public Map<Integer,
Integer> getData(String path){

SQLiteDatabase db
= openHelper.getReadableDatabase();

Cursor cursor = db.rawQuery("select
threadid, position from filedown where downpath=?", new String[]{path});

Map<Integer, Integer>
data = new HashMap<Integer,
Integer>();

while(cursor.moveToNext()){

data.put(cursor.getInt(0),
cursor.getInt(1));

}

cursor.close();

db.close();

return data;

}

/**

* 保存下载线程初始位置

* @param path

* @param map

*/

public void save(String
path, Map<Integer, Integer> map){//int threadid, intposition

SQLiteDatabase db
= openHelper.getWritableDatabase();

db.beginTransaction();

try{

for(Map.Entry<Integer,
Integer> entry : map.entrySet()){

db.execSQL("insert
into filedown(downpath, threadid, position) values(?,?,?)",

new Object[]{path,
entry.getKey(), entry.getValue()});

}

db.setTransactionSuccessful();

}finally{

db.endTransaction();

}

db.close();

}

/**

* 实时更新线程的最后下载位置

* @param path

* @param map

*/

public void update(String
path, Map<Integer, Integer> map){

SQLiteDatabase db
= openHelper.getWritableDatabase();

db.beginTransaction();

try{

for(Map.Entry<Integer,
Integer> entry : map.entrySet()){

db.execSQL("update
filedown set position=? where downpath=? and threadid=?",

new Object[]{entry.getValue(),
path, entry.getKey()});

}

db.setTransactionSuccessful();

}finally{

db.endTransaction();

}

db.close();

}

/**

* 当文件下载完成后,清掉该文件对应的下载记录

* @param path

*/

public void delete(String
path){

SQLiteDatabase db
= openHelper.getWritableDatabase();

db.execSQL("delete
from filedown where downpath=?", new Object[]{path});

db.close();

}

}

9.DownloadThread

package com.changcheng.net.download;

import java.io.InputStream;

import java.io.RandomAccessFile;

import java.net.HttpURLConnection;

import java.net.URL;

import android.util.Log;

public class DownloadThread extends Thread
{

private static final String TAG = "DownloadThread";

private RandomAccessFile saveFile;

private URL downUrl;

private int block;

/* 下载开始位置 */

private int threadId =
-1;

private int startPos;

private int downLength;

private boolean finish = false;

private FileDownloader downloader;

public DownloadThread(FileDownloader
downloader, URL downUrl, RandomAccessFile saveFile, int block, int startPos, int threadId)
{

this.downUrl =
downUrl;

this.saveFile =
saveFile;

this.block =
block;

this.startPos =
startPos;

this.downloader =
downloader;

this.threadId =
threadId;

this.downLength =
startPos - (block * (threadId - 1));

}

@Override

public void run()
{

if(downLength < block){//未下载完成

try {

HttpURLConnection
http = (HttpURLConnection)downUrl.openConnection();

http.setRequestMethod("GET");

http.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, */*");

http.setRequestProperty("Accept-Language", "zh-CN");

http.setRequestProperty("Referer", downUrl.toString());

http.setRequestProperty("Charset", "UTF-8");

http.setRequestProperty("Range", "bytes=" + this.startPos + "-");

http.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)");

http.setRequestProperty("Connection", "Keep-Alive");

InputStream
inStream = http.getInputStream();

int max
= block>1024
? 1024 : (block>10
? 10 : 1);

byte[]
buffer = new byte[max];

int offset
= 0;

print("线程 " + this.threadId + "从位置"+ this.startPos+ "开始下载 ");

while (downLength < block &&
(offset = inStream.read(buffer, 0, max)) != -1) {

saveFile.write(buffer,
0, offset);

downLength +=
offset;

downloader.update(this.threadId, block *
(threadId -
1) + downLength);

downloader.saveLogFile();

downloader.append(offset);

int spare
= block-downLength;//求剩下的字节数

if(spare
< max) max = (int)
spare;

}

saveFile.close();

inStream.close();

print("线程 " + this.threadId + "完成下载 ");

this.finish = true;

this.interrupt();

} catch (Exception
e) {

this.downLength =
-1;

print("线程"+ this.threadId+ ":"+
e);

}

}

}

private static void print(String
msg){

Log.i(TAG,
msg);

}

/**

* 下载是否完成

* @return

*/

public boolean isFinish()
{

return finish;

}

/**

* 已经下载的内容大小

* @return 如果返回值为-1,代表下载失败

*/

public long getDownLength()
{

return downLength;

}

}

11.DBOpenHelper

package com.changcheng.download.service;

import android.content.Context;

import android.database.sqlite.SQLiteDatabase;

import android.database.sqlite.SQLiteOpenHelper;

public class DBOpenHelper extends SQLiteOpenHelper
{

private static final String DBNAME = "download.db";

private static final int VERSION =
2;

public DBOpenHelper(Context
context) {

super(context, DBNAME, null, VERSION);

}

@Override

public void onCreate(SQLiteDatabase
db) {

db.execSQL("CREATE
TABLE IF NOT EXISTS filedown (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, position INTEGER)");

}

@Override

public void onUpgrade(SQLiteDatabase
db, int oldVersion, int newVersion)
{

db.execSQL("DROP
TABLE IF EXISTS filedown");

onCreate(db);

}

}

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