安卓开发笔记——关于文件断点续传
什么是断点续传? 客户端软件断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,节省流量,也提高速度。 ? 我写了个小Demo,先看下实现效果图: 断点续传的原理? 断点续传其实并没有那么神秘,它只不过是利用了HTTP传输协议中请求头(REQUEST HEADER)的不同来进行断点续传的。 ? 以下是我刚在百度音乐下载MP3的时候用Firebug截下来的HTTP请求头: Accept:image/webp,*/*;q=0.8
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-CN,zh;q=0.8
Connection:keep-alive
Cookie:BIDUPSID=6B8AF721169ED82B182A7EE22F75BB87; BAIDUID=6B8AF721169ED82B182A7EE22F75BB87:FG=1; BDUSS=1pWS14dzl6Ry02MVJoN0toT1RlTzRIdkdBRVlsN1JJdG9OVmQ5djAybTJ1a1JWQVFBQUFBJCQAAAAAAAAAAAEAAACkSkgjTXJfTGVlX-fiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALYtHVW2LR1VaW; __xsptplus188=188.1.1428024707.1428024712.2%234%7C%7C%7C%7C%7C%23%23QdAfR9H5KZHSIGakiCWebLQCd6CjKjz5%23; locale=zh; cflag=65279%3A3; BDRCVFR[-z8N-kPXoJt]=I67x6TjHwwYf0; H_PS_PSSID=11099_13386_1439_13425_13075_10902_12953_12868_13320_12691_13410_12722_12737_13085_13325_13203_12835_13161_8498
Host:b.hiphotos.baidu.com
Referer:http://music.baidu.com/song/124380645
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/39.0.2171.95 Safari/537.36
这是百度音乐服务器给我返回的信息: Age:347651 Cache-Control:max-age=31536000 Content-Length:16659 Content-Type:image/jpeg Date:Sat,11 Apr 2015 06:48:45 GMT Error-Message:OK ETag:"10487865676202532488" Expires:Wed,06 Apr 2016 06:02:50 GMT Last-Modified:Tue,07 Apr 2015 05:45:32 GMT Ohc-Cachable:1 Server:JSP3/2.0.7 这是一般的下载请求,服务器会给我们返回我们请求下载文件的长度(Content-Length),成功状态码为:200。 而断点续传是要从我们之前已经下载好的文件位置继续上一次的传输,顾名思义我们需要告诉服务器我们上一次已经下载到哪里了,所以在我们的HTTP请求头里要额外的包含一条信息,而这也就是断点续传的奥秘所在了。 这条信息为:(RANGE为请求头属性,X为从什么地方开始,Y为到什么地方结束) RANGE: bytes=X-Y
例如: Range : 用于客户端到服务器端的请求,可通过该字段指定下载文件的某一段大小,及其单位。典型的格式如: ? 以上是HTTP需要注意的地方,接下来讲讲Java里需要注意的几个类 关于这个HTTP请求,在Java的Net包下,给我们封装好了一系列的类,我们直接拿来使用就行了。 这是java.net.URLConnection类下的一个方法,用来设置HTTP请求头的,Key对应HTTP请求头属性,Value对应请求头属性的值。 这是java.io.RandomAccessFile类,这个类的实例支持对随机访问文件的读取和写入,这个类里有个很重要的seek(long pos)方法, 这个方法可以在你设置的长度的下一个位置进行写入,例如seek(599),那么系统下一次写入的位置就是该文件的600位置。 ? 既然是断点续传,那么我们自身肯定也要记录断点位置,所以这里数据库也是必不可少的。 ? 具体实现: 好了,了解了上面的知识点后,可以开始动手写程序了。 下面是我写的一个小Demo,用单线程来实现的断点续传的,注释非常全。 ?这是Demo的结构图:(实在不会画图,凑合着看哈) 主类(Activity): 1 package com.example.ui; 2 3 import android.app.Activity; 4 android.app.AlertDialog; 5 android.app.ProgressDialog; 6 android.app.AlertDialog.Builder; 7 android.content.BroadcastReceiver; 8 android.content.Context; 9 android.content.DialogInterface; 10 android.content.Intent; 11 android.content.IntentFilter; 12 android.os.Bundle; 13 android.util.Log; 14 android.view.View; 15 android.view.View.OnClickListener; 16 android.widget.Button; 17 android.widget.ProgressBar; 18 android.widget.TextView; 19 20 com.example.downloadfiletest.R; 21 com.example.entity.FileInfo; 22 com.example.logic.DownloadService; 23 24 public class MainActivity extends Activity { 25 26 private TextView textView; 27 ProgressBar progressBar; 28 Button bt_start; 29 Button bt_stop; 30 31 @Override 32 protected void onCreate(Bundle savedInstanceState) { 33 super.onCreate(savedInstanceState); 34 setContentView(R.layout.activity_main); 35 initView(); // 初始化控件 36 37 注册广播接收者 38 IntentFilter intentFilter = new IntentFilter(); 39 intentFilter.addAction(DownloadService.UPDATE); 40 registerReceiver(broadcastReceiver,intentFilter); 41 42 } 43 44 45 onDestroy() { 46 .onDestroy(); 47 解绑 48 unregisterReceiver(broadcastReceiver); 49 50 51 初始化控件 52 private initView() { 53 textView = (TextView) findViewById(R.id.textView); 54 progressBar = (ProgressBar) findViewById(R.id.progressBar); 55 bt_start = (Button) findViewById(R.id.bt_start); 56 bt_stop = (Button) findViewById(R.id.bt_stop); 57 progressBar.setMax(100); 58 59 final FileInfo fileInfo = new FileInfo(0,"Best Of Joy.MP3-Michael Jackson","http://music.baidu.com/data/music/file?link=http://yinyueshiting.baidu.com/data2/music/38264529/382643441428735661128.mp3?xcode=46e7c02e3acba184b6145f688bb9f2422c866f9e4969f410&song_id=38264344",0 60 61 点击开始下载 62 bt_start.setOnClickListener( OnClickListener() { 63 64 @Override 65 onClick(View v) { 66 Intent intent = new Intent(MainActivity.this,DownloadService.class 67 intent.setAction(DownloadService.START); 68 intent.putExtra("FileInfo",fileInfo); 69 startService(intent); 70 textView.setText("正在下载文件:" + fileInfo.getFileName()); 71 72 } 73 }); 74 75 点击停止下载 76 bt_stop.setOnClickListener( 77 78 79 80 Intent intent = 81 intent.setAction(DownloadService.STOP); 82 intent.putExtra("FileInfo" 83 84 textView.setText("任务已暂停,请点击下载继续" 85 86 87 88 89 90 广播接收者 91 BroadcastReceiver broadcastReceiver = BroadcastReceiver() { 92 93 @Override 94 onReceive(Context context,Intent intent) { 95 if (intent.getAction().equals(DownloadService.UPDATE)) { 96 int finished = intent.getIntExtra("finished",1)"> 97 progressBar.setProgress(finished); 98 99 用户界面友好,提醒用户任务下载完成 100 if (finished ==100) { 101 AlertDialog.Builder builder=new AlertDialog.Builder(MainActivity.this102 builder.setTitle("任务状态"103 builder.setMessage("文件下载已完成!"104 builder.setPositiveButton("确认", DialogInterface.OnClickListener() { 105 106 @Override 107 void onClick(DialogInterface dialog,1)">int which) { 108 progressBar.setProgress(0109 textView.setText("请点击下载"110 } 111 }); 112 builder.show(); 113 } 114 115 } 116 }; 117 } 后台服务类(Service) com.example.logic; java.io.File; java.io.IOException; java.io.RandomAccessFile; java.net.HttpURLConnection; java.net.URL; 8 org.apache.http.HttpStatus; org.apache.http.client.ClientProtocolException; 11 android.app.Service; android.os.Environment; android.os.Handler; android.os.IBinder; 18 19 20 class DownloadService Service { 22 23 按钮标志符 24 static final String START = "START"; 25 final String STOP = "STOP" 更新进度标志 final String UPDATE = "UPDATE" 下载路径(内存卡(SD)根目录下的/downloads/) final String DOWNLOADPATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/downloads/" 30 定义初始化文件操作标志 31 final int INIT = 0 32 33 DownloadTask downloadTask; 34 35 private Handler handler = Handler() { 36 handleMessage(android.os.Message msg) { 37 switch (msg.what) { 38 case INIT: 39 FileInfo fileInfo = (FileInfo) msg.obj; 40 Log.i("init" 41 进行下载任务操作 42 downloadTask = new DownloadTask(DownloadService. 43 downloadTask.download(); 44 break 45 46 }; 47 48 49 /** 50 * 当Service启动时会被调用,用来接收Activity传送过来的数据 51 */ 52 53 int onStartCommand(Intent intent,1)">int flags,1)"> startId) { 54 (intent.getAction().equals(START)) { 55 当点击开始下载操作时 56 接收Activity(putExtra)过来的数据 57 FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("FileInfo" 58 Log.i(START,1)"> 59 new Thread( InitFileThread(fileInfo)).start(); 61 } else (intent.getAction().equals(STOP)) { 62 当点击停止下载操作时 63 FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("FileInfo" Log.i(STOP,1)"> 暂定任务 66 if (downloadTask != null 67 downloadTask.flag = true 68 70 71 return .onStartCommand(intent,flags,startId); 73 74 75 public IBinder onBind(Intent intent) { 76 77 78 79 初始化文件操作获取网络资源大小长度,开辟子线程 80 class InitFileThread implements Runnable { 81 82 FileInfo fileInfo; 83 84 构造方法,获取文件对象 85 InitFileThread(FileInfo fileInfo) { 86 this.fileInfo = fileInfo; 87 88 89 90 run() { 91 /* 92 * 1、打开网络连接,获取文件长度 2、创建本地文件,长度和网络文件相等 93 94 HttpURLConnection httpURLConnection = 95 RandomAccessFile randomAccessFile = 96 try { 97 URL url = URL(fileInfo.getUrl()); 98 httpURLConnection = (HttpURLConnection) url.openConnection(); 知识点:除了下载文件,其他一律用POST 100 httpURLConnection.setConnectTimeout(3000101 httpURLConnection.setRequestMethod("GET"102 定义文件长度 103 int length = -1104 网络连接成功 105 if (httpURLConnection.getResponseCode() == HttpStatus.SC_OK) { 106 length = httpURLConnection.getContentLength(); 107 108 判断是否取得文件长度 109 if (length <= 0110 return112 113 创建文件目录对象 114 File dir = File(DOWNLOADPATH); 115 if (!dir.exists()) { 116 若目录不存在,创建 117 dir.mkdir(); 118 119 创建文件对象 120 File file = File(dir,fileInfo.getFileName()); 121 创建随机访问文件流 参数二为权限:读写删 122 randomAccessFile = new RandomAccessFile(file,"rwd"123 randomAccessFile.setLength(length); 124 fileInfo.setLength(length); 125 发送handler 126 handler.obtainMessage(INIT,fileInfo).sendToTarget(); 127 128 } catch (ClientProtocolException e) { 129 e.printStackTrace(); 130 } (IOException e) { 131 132 } finally133 if (randomAccessFile != 134 135 randomAccessFile.close(); 136 } 137 e.printStackTrace(); 138 } 139 140 141 142 143 144 145 146 } 下载任务类: java.io.InputStream; java.net.MalformedURLException; java.util.List; 13 17 com.example.dao.ThreadDAO; com.example.dao.ThreadDAOImpl; com.example.entity.ThreadInfo; 23 * 下载任务类 25 * 26 * @author Balla_兔子 27 28 29 DownloadTask { Context context; ThreadDAO dao; 初始化下载进度,默认为0 34 int finished = 0 35 36 是否暂停下载标识符 37 boolean flag = false 38 39 DownloadTask(Context context,FileInfo fileInfo) { 40 this.context = context; 41 42 dao = ThreadDAOImpl(context); 44 download() { 线程信息的url和文件的url对应 47 List<ThreadInfo> threadInfos = dao.getThreadInfo(fileInfo.getUrl()); 48 ThreadInfo threadInfo = 49 if (threadInfos.size() == 0 50 若数据库无此线程任务 51 threadInfo = new ThreadInfo(0,fileInfo.getUrl(),fileInfo.getLength(),1)"> 52 } else 53 threadInfo = threadInfos.get(0 54 55 创建子线程进行下载 56 DownloadThread(threadInfo)).start(); 57 59 执行下载任务,开辟子线程 60 class DownloadThread 61 62 ThreadInfo threadInfo; 64 DownloadThread(ThreadInfo threadInfo) { this.threadInfo = threadInfo; 66 67 69 70 HttpURLConnection urlConnection = 71 InputStream inputStream = 72 RandomAccessFile randomAccessFile = 74 75 * 执行下载任务 76 * 1、查询数据库,确定是否已存在此下载线程,便于继续下载 * 2、设置从哪个位置开始下载 * 3、设置文件的写入位置 79 * 4、开始下载 80 * 5、广播通知UI更新下载进度 * 6、暂停线程的操作 82 * 7、下载完毕,删除数据库信息 83 84 1、查询数据库 85 dao.isExists(threadInfo.getUrl(),threadInfo.getThread_id())) { 86 若不存在,插入新线程信息 dao.insertThread(threadInfo); 90 2、设置下载位置 92 URL url = URL(threadInfo.getUrl()); 93 urlConnection = 94 设置连接超时时间 95 urlConnection.setConnectTimeout(3000 96 urlConnection.setRequestMethod("GET" 97 98 设置请求属性 参数一:Range头域可以请求实体的一个或者多个子范围(一半用于断点续传),如果用户的请求中含有range ,则服务器的相应代码为206。 101 参数二:表示请求的范围:比如头500个字节:bytes=0-499 102 获取线程已经下载的进度 int start = threadInfo.getStart() + threadInfo.getFinished(); 105 urlConnection.setRequestProperty("range","bytes=" + start + "-" + threadInfo.getEnd()); 106 107 3、设置文件的写入位置 108 File file = File(DownloadService.DOWNLOADPATH,1)">109 randomAccessFile = 110 设置从哪里开始写入,如参数为100,那就从101开始写入 randomAccessFile.seek(start); 113 finished +=114 Intent intent = Intent(DownloadService.UPDATE); 4、开始下载 116 if (urlConnection.getResponseCode() == HttpStatus.SC_PARTIAL_CONTENT) { 117 inputStream = urlConnection.getInputStream(); 118 设置字节数组缓冲区 119 byte[] data = new byte[1024*4]; 120 读取长度 121 int len = -1122 取得当前时间 123 long time = System.currentTimeMillis(); 124 while ((len = inputStream.read(data)) != -1125 读取成功,写入文件 126 randomAccessFile.write(data,len); 128 避免更新过快,减缓主线程压力,让其0.5秒发送一次进度 129 if (System.currentTimeMillis() - time > 500130 time =131 把当前进度通过广播传递给UI 132 finished += len; 133 Log.i("finished:",finished+""134 Log.i("file:",fileInfo.getLength()+""135 intent.putExtra("finished",finished*100 / fileInfo.getLength()); 136 context.sendBroadcast(intent); 138 139 (flag) { 140 暂停下载,更新进度到数据库 141 dao.updateThread(threadInfo.getUrl(),threadInfo.getThread_id(),finished); 142 结束线程 143 146 147 当下载执行完毕时,删除数据库线程信息 148 dao.deleteThread(threadInfo.getUrl(),threadInfo.getThread_id()); 149 150 151 } (MalformedURLException e) { 152 153 } 154 155 } 156 if (urlConnection != 157 urlConnection.disconnect(); 158 159 if (inputStream != 160 161 inputStream.close(); 162 } 163 164 165 166 167 168 169 } 170 171 172 173 174 175 176 177 178 } 数据库帮助类: 1 com.example.db; 2 3 4 android.database.sqlite.sqliteDatabase; 5 android.database.sqlite.sqliteOpenHelper; 6 7 class DBHelper sqliteOpenHelper { 8 final String DBNAME = "download.db" 9 int VERSION = 110 final String TABLE="threadinfo"11 final String CREATE_DB = "create table threadinfo (_id integer primary key autoincrement,thread_id integer,url text,start integer,end integer,finished integer) "12 final String DROP_DB = "drop table if exists threadinfo"13 14 DBHelper(Context context) { 15 super(context,DBNAME,VERSION); 16 17 18 19 onCreate(sqliteDatabase db) { 20 db.execsql(CREATE_DB); 21 22 23 24 25 void onUpgrade(sqliteDatabase db,1)">int oldVersion,1)"> newVersion) { 26 db.execsql(DROP_DB); 27 28 29 30 } 数据库表接口类: com.example.dao; 4 interface ThreadDAO { 新增一条线程信息 insertThread(ThreadInfo threadInfo); 10 删除一条线程信息(多线程下载,可能一个url对应多个线程,所以需要2个条件) void deleteThread(String url,1)"> thread_id); 修改一条线程信息 15 void updateThread(String url,1)">int thread_id,1)"> finished); 16 17 查询线程有关信息(根据url查询下载该url的所有线程信息) 18 public List<ThreadInfo> getThreadInfo(String url); 19 20 判断线程是否已经存在 21 boolean isExists(String url,1)">22 24 } 数据库表接口实现类: java.util.ArrayList; 5 6 android.content.ContentValues; 8 android.database.Cursor; 9 11 com.example.db.DBHelper; 12 14 15 * ThreadDAO接口实现类 17 19 class ThreadDAOImpl 22 DBHelper dbHelper; 24 ThreadDAOImpl(Context context) { 25 dbHelper = DBHelper(context); 27 29 insertThread(ThreadInfo threadInfo) { 30 sqliteDatabase db = dbHelper.getWritableDatabase(); 31 ContentValues values = ContentValues(); 32 values.put("thread_id"33 values.put("url "34 values.put("start "35 values.put("end "36 values.put("finished "37 db.insert(DBHelper.TABLE,values); 38 db.close(); 39 40 41 42 thread_id) { 43 sqliteDatabase db =44 db.delete(DBHelper.TABLE,"url=? and thread_id=?",1)"> String[] { url,String.valueOf(thread_id) }); 45 46 47 48 49 finished) { 50 sqliteDatabase db =51 db.execsql("update threadinfo set finished = ? where url = ? and thread_id=?",1)"> Object[] { finished,url,thread_id }); 52 53 54 55 56 getThreadInfo(String url) { 57 List<ThreadInfo> list = new ArrayList<ThreadInfo>(); 58 sqliteDatabase db =59 Cursor cursor = db.query(DBHelper.TABLE,1)">null,"url=?",1)">new String[] { url },1)">60 while (cursor.moveToNext()) { 61 ThreadInfo threadInfo = ThreadInfo(); 62 threadInfo.setThread_id(cursor.getInt(cursor.getColumnIndex("thread_id"))); 63 threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url"64 threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start"65 threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex("end"66 threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished"67 list.add(threadInfo); 68 69 cursor.close(); 70 71 list; 72 73 74 75 76 sqliteDatabase db =77 Cursor cursor = db.query(DBHelper.TABLE,1)">new String[] { url,String.valueOf(thread_id) },1)">78 boolean isExists = cursor.moveToNext(); 79 80 isExists; 81 82 83 } 实体类:(文件,线程) 文件实体类: com.example.entity; java.io.Serializable; class FileInfo Serializable { 7 id; String fileName; String url; length; finished; 12 13 FileInfo() { 15 16 public FileInfo(int id,String fileName,String url,1)"> length,17 18 19 this.id =20 this.fileName = fileName; 21 this.url = url; 22 this.length =23 this.finished =25 26 getId() { 27 30 void setId( id) { 31 32 33 34 String getFileName() { 35 36 37 38 setFileName(String fileName) { 39 40 41 String getUrl() { 43 44 45 46 setUrl(String url) { 47 49 50 getLength() { 51 53 54 void setLength( length) { 55 56 57 58 getFinished() { 59 60 61 62 void setFinished(63 64 65 66 67 String toString() { 68 return "FileInfo [id=" + id + ",fileName=" + fileName + ",url=" + url 69 + ",length=" + length + ",finished=" + finished + "]"71 72 }View Code 线程实体类: class ThreadInfo thread_id; start; end; ThreadInfo() { public ThreadInfo(int start,1)">int end,1)">17 this.thread_id =this.start =this.end =23 24 getThread_id() { 26 28 void setThread_id(30 31 32 33 34 35 36 37 38 41 getStart() { 42 43 44 45 void setStart( start) { 46 47 48 getEnd() { 50 51 52 53 void setEnd( end) { 54 56 57 58 59 60 61 62 63 64 65 66 67 return "ThreadInfo [thread_id=" + thread_id + ",url=" + url + ",start=" + start + ",end=" + end + ",1)">69 70 }View Code ? 关于多线程的断点续传,其实原理差不多,还是一样把一个文件分成不同区域,然后每个区域各用一条线程去执行下载任务,然后再把文件合并,改天有时间再上个Demo给大家看吧。 ? ? 作者:Balla_兔子 (编辑:北几岛) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |