`
helpbs
  • 浏览: 1161567 次
文章分类
社区版块
存档分类
最新评论

通过HTTP协议实现多线程下载

 
阅读更多

1. 基本原理,每条线程从文件不同的位置开始下载,最后合并出完整的数据。

2. 使用多线程下载的好处
下载速度快。为什么呢?很好理解,以往我是一条线程在服务器上下载。也就是说,对应在服务器上,有一个我的下载线程存在。
这时候肯定不只我一个人在下载,服务器上肯定同时存在多条下载线程,在下载服务器资源。对于 CPU 来说,不可能实现并发执行。
CPU 会公平的为这些线程划分时间片,轮流执行,a线程十毫秒 , b线程十毫秒...
假设运用了本文这种手法,意味着我的下载应用,可以同时使用服务器端的任意多条线程同时下载(理论上).
假设这个线程数目是 50 条,本应用就将更多的得到服务器 CPU 的照顾超过 50 倍.
但是总归会受本地网络速度的限制。

3. 每条线程要负责下载的数据长度可以用 “下载数据的总长度” 除以 “参与下载的线程总数” 来计算。但是要考虑到不能整除的情况。
假设有 5 条线程参与下载,那么计算公式应该为 :
int block = 数据总长度%线程数 == 0? 10/3 : 10/3+1; (不能整除,则加一)

4. 和数据库分页查询类型。每条线程需要知道自己从数据的什么位置开始下载,下载到什么位置为止。
首先,为每一个线程配备一个 id , id 从零开始,为 0 1 2 3...
开始位置:线程 id 乘以每条线程负责下载的数据长度.
结束位置:下一个线程开始位置的前一个位置。
如:
int startPosition = 线程id * 每条线程下载的数据长度
int endPosition = (线程id + 1) * 每条线程下载的数据长度 -1;

5. HTTP 协议的 Range 头可以指定从文件的什么位置开始下载,下载到什么位置结束。单位为 1byte
Range:bytes=2097152-4194304 表示从文件的 2M 的位置开始下载,下载到 4M 处结束
假如 Range 指定要读取到 文件的 5104389 的字节数位置,但是下载的文件本身只有 4104389 个长度。那么下载操作自动会在 4104389 处停止。
因此不会下载到多余的无效数据.

6. 另一个难题是如何按顺序将数据写往本地文件。因为,线程是同步执行的,它们同时在往本地目标文件写入数据。
而线程于线程之间写入的数据并没有按照下载数据本身的顺序。若按照普通的 OutputStream 的写入方式,最后的本地下载文件将失真。
于是我们将用到下面这个类:
java.io.RandomAccessFile
因为此类同时实现了 DataOutput 和 DataInput 的方法。使他们同时具有写入和读取功能。
这个类仿佛存在一个类似文件指针的东西,可以随意执行文件的任意一个位置开始读写.
因此此类的实例支持对随机访问文件的读取和写入.

例如:
Java代码收藏代码
  1. Filefile=newFile("1.txt");
  2. RandomAccessFileaccessFile=newRandomAccessFile(file,"rwd");
  3. accessFile.setLength(1024);

虽然,执行完这段代码后,我们还没有向目标文件 "1.txt" 写入任何数据。但是如果此时查看其大小,已经为 1kb 了。这是我们自己设置的大小。
这个操作类似于向这个文件存储了一个大型的 byte 数组。这个数组将这个文件撑到指定大小。等待被填满。
既然是这样,好处就在于,我们可以通过 “索引” 随机访问此文件系统的某个部分。
例如,可能这个文件大小为 500
那么,我的业务需求可能需要第一次 从 300 位置开始写数据,写到 350 为止。
第二次,我又从 50 开始写数据,写到 100 为止。
总之,我不是 “一次性” 的 “按顺序” 的将这个文件写完。
那么,RandomAccessFile 可以支持这种操作。

API
void setLength(long newLength)
Sets the length of this file. (设置文件的预计大小)
void seek(long pos)
Sets the file-pointer offset, measured from the beginning of this file, at which the next read or write occurs.
假设为这个方法传入 1028 这个参数,表示,将从文件的 1028 位置开始写入。
void write(byte[] b, int off, int len)
Writes len bytes from the specified byte array starting at offset off to this file.
write(byte[] b)
Writes b.length bytes from the specified byte array to this file, starting at the current file pointer.
void writeUTF(String str)
Writes a string to the file using modified UTF-8 encoding in a machine-independent manner.
String readLine()
Reads the next line of text from this file.

实验代码:


Java代码收藏代码
  1. publicstaticvoidmain(String[]args)throwsException{
  2. Filefile=newFile("1.txt");
  3. RandomAccessFileaccessFile=newRandomAccessFile(file,"rwd");
  4. /*设置文件为3个字节大小*/
  5. accessFile.setLength(3);
  6. /*向第二个位置写入'2'*/
  7. accessFile.seek(1);
  8. accessFile.write("2".getBytes());
  9. /*向第一个位置写入'1'*/
  10. accessFile.seek(0);accessFile.write("1".getBytes());
  11. /*向第三个位置写入'3'*/
  12. accessFile.seek(2);
  13. accessFile.write("3".getBytes());accessFile.close();
  14. //期待文件的内容为:123
  15. }



以上实验成功,虽然我们写入字符串的顺序为 "2"、"1"、"3",但是因为设置了文件偏移量的关系,文件最终保存的数据为 : 123
另一个疑问,写完这三个数据,文件的大小已经为 3 个字节大小了。已经撑满了写入的数据,那么我们继续往里面放数据会有什么效果?

/* 向超出大小的第四个字节位置写入数据 */
accessFile.seek(3);
accessFile.write("400".getBytes());

以上代码无论 seek 方法指定的文件指针偏移量以及存入的数据,都已经超出了最开始为文件设定的 3 个字节的大小。
按照我的猜测,至少 “accessFile.seek(3)” 位置会抛出 "ArrayIndexOutOfBoundsException" 异常,表示下标越界。
而,单独执行 "accessFile.write("400".getBytes())" 应该可以成功。因为这个需求属于合理的,应该有执行它的机制。
实验结果是两句代码都是成功的。貌似是说明,文件隐含的大型的字节数组,可以自动撑大。

但是要注意的问题是,必须要保证所设定的文件大小的每一个位置都具有合法的数据,至少不能为空。
例如:
/* 向第三个位置写入 '3' */
accessFile.seek(2);
accessFile.write("3".getBytes());

accessFile.seek(5);
accessFile.write("400".getBytes());
那么结合之前的代码,最后的结果为:
123口口400
在空白的两个位置处出现了乱码。这是理所应当的。

另外,假设我们为文件指定了一百个长度:
accessFile.setLength(100);
而,实际上,我们只为其前五个位置设置了值。那么理所当然的是,文件保存的数据,最后会缀上 95 个乱码。

7. 准备工作应该十分充分了。接下来上代码。



Java代码收藏代码
  1. importjava.io.File;
  2. importjava.io.IOException;
  3. importjava.io.InputStream;
  4. importjava.io.RandomAccessFile;
  5. importjava.net.HttpURLConnection;
  6. importjava.net.URL;
  7. /**
  8. *多线程方式文件下载
  9. */
  10. publicclassMulThreadDownload{
  11. /*下载的URL*/
  12. privateURLdownloadUrl;
  13. /*用于保存的本地文件*/
  14. privateFilelocalFile;
  15. /*没条线程下载的数据长度*/
  16. privateintblock;
  17. publicstaticvoidmain(String[]args){
  18. /*可以为网络上任意合法下载地址*/
  19. StringdownPath="http://192.168.1.102:8080/myvideoweb/down.avi";
  20. MulThreadDownloadthreadDownload=newMulThreadDownload();
  21. /*开10条线程下载下载*/
  22. try{
  23. threadDownload.download(downPath,10);
  24. }catch(Exceptione){
  25. e.printStackTrace();
  26. }
  27. }
  28. /**
  29. *多线程文件下载
  30. *
  31. *@parampath下载地址
  32. *@paramthreadCount线程数
  33. */
  34. publicvoiddownload(Stringpath,intthreadCount)throwsException{
  35. downloadUrl=newURL(path);
  36. HttpURLConnectionconn=(HttpURLConnection)downloadUrl
  37. .openConnection();
  38. /*设置GET请求方式*/
  39. conn.setRequestMethod("GET");
  40. /*设置响应时间超时为5秒*/
  41. conn.setConnectTimeout(5*1000);
  42. /*获取本地文件名*/
  43. Stringfilename=parseFilename(path);
  44. /*获取下载文件的总大小*/
  45. intdataLen=conn.getContentLength();
  46. if(dataLen<0){
  47. System.out.println("获取数据失败");
  48. return;
  49. }
  50. /*创建本地目标文件,并设置其大小为准备下载文件的总大小*/
  51. localFile=newFile(filename);
  52. RandomAccessFileaccessFile=newRandomAccessFile(localFile,"rwd");
  53. /*这时候,其实本地目录下,已经创建好了一个大小为下载文件的总大小的文件*/
  54. accessFile.setLength(dataLen);
  55. accessFile.close();
  56. /*计算每条线程要下载的数据大小*/
  57. block=dataLen%threadCount==0?dataLen/threadCount:dataLen/threadCount+1;
  58. /*启动线程下载文件*/
  59. for(inti=0;i<threadCount;i++){
  60. newDownloadThread(i).start();
  61. }
  62. }
  63. /**
  64. *解析文件
  65. */
  66. privateStringparseFilename(Stringpath){
  67. returnpath.substring(path.lastIndexOf("/")+1);
  68. }
  69. /**
  70. *内部类:文件下载线程类
  71. */
  72. privatefinalclassDownloadThreadextendsThread{
  73. /*线程id*/
  74. privateintthreadid;
  75. /*开始下载的位置*/
  76. privateintstartPosition;
  77. /*结束下载的位置*/
  78. privateintendPosition;
  79. /**
  80. *新建一个下载线程
  81. *@paramthreadid线程id
  82. */
  83. publicDownloadThread(intthreadid){
  84. this.threadid=threadid;
  85. startPosition=threadid*block;
  86. endPosition=(threadid+1)*block-1;
  87. }
  88. @Override
  89. publicvoidrun(){
  90. System.out.println("线程'"+threadid+"'启动下载..");
  91. RandomAccessFileaccessFile=null;
  92. try{
  93. /*设置从本地文件的什么位置开始写入数据,"rwd"表示对文件具有读写删权限*/
  94. accessFile=newRandomAccessFile(localFile,"rwd");
  95. accessFile.seek(startPosition);
  96. HttpURLConnectionconn=(HttpURLConnection)downloadUrl.openConnection();
  97. conn.setRequestMethod("GET");
  98. conn.setReadTimeout(5*1000);
  99. /*为HTTP设置Range属性,可以指定服务器返回数据的范围*/
  100. conn.setRequestProperty("Range","bytes="+startPosition+"-"
  101. +endPosition);
  102. /*将数据写往本地文件*/
  103. writeTo(accessFile,conn);
  104. System.out.println("线程'"+threadid+"'完成下载");
  105. }catch(IOExceptione){
  106. e.printStackTrace();
  107. }finally{
  108. try{
  109. if(accessFile!=null){
  110. accessFile.close();
  111. }
  112. }catch(IOExceptionex){
  113. ex.printStackTrace();
  114. }
  115. }
  116. }
  117. /**
  118. *将下载数据写往本地文件
  119. */
  120. privatevoidwriteTo(RandomAccessFileaccessFile,
  121. HttpURLConnectionconn){
  122. InputStreamis=null;
  123. try{
  124. is=conn.getInputStream();
  125. byte[]buffer=newbyte[1024];
  126. intlen=-1;
  127. while((len=is.read(buffer))!=-1){
  128. accessFile.write(buffer,0,len);
  129. }
  130. }catch(IOExceptione){
  131. e.printStackTrace();
  132. }finally{
  133. try{
  134. if(is!=null){
  135. is.close();
  136. }
  137. }catch(Exceptionex){
  138. ex.printStackTrace();
  139. }
  140. }
  141. }
  142. }
  143. }

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics