通过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 的方法。使他们同时具有写入和读取功能。
这个类仿佛存在一个类似文件指针的东西,可以随意执行文件的任意一个位置开始读写.
因此此类的实例支持对随机访问文件的读取和写入.
例如:
- File file = new File("1.txt");
- RandomAccessFile accessFile = new RandomAccessFile(file,"rwd");
- 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.
实验代码:
- public static void main(String[] args) throws Exception {
- File file = new File("1.txt");
- RandomAccessFile accessFile = new RandomAccessFile(file,"rwd");
- /* 设置文件为 3 个字节大小 */
- accessFile.setLength(3);
- /* 向第二个位置写入 '2' */
- accessFile.seek(1);
- accessFile.write("2".getBytes());
- /* 向第一个位置写入 '1' */
- accessFile.seek(0); accessFile.write("1".getBytes());
- /* 向第三个位置写入 '3' */
- accessFile.seek(2);
- accessFile.write("3".getBytes()); accessFile.close();
- // 期待文件的内容为 :123
- }
以上实验成功,虽然我们写入字符串的顺序为 "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. 准备工作应该十分充分了。接下来上代码。
- import java.io.File;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.RandomAccessFile;
- import java.net.HttpURLConnection;
- import java.net.URL;
- /**
- * 多线程方式文件下载
- */
- public class MulThreadDownload {
- /* 下载的URL */
- private URL downloadUrl;
- /* 用于保存的本地文件 */
- private File localFile;
- /* 没条线程下载的数据长度 */
- private int block;
- public static void main(String[] args) {
- /* 可以为网络上任意合法下载地址 */
- String downPath = "http://192.168.1.102:8080/myvideoweb/down.avi";
- MulThreadDownload threadDownload = new MulThreadDownload();
- /* 开 10 条线程下载下载 */
- try {
- threadDownload.download(downPath, 10);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- /**
- * 多线程文件下载
- *
- * @param path 下载地址
- * @param threadCount 线程数
- */
- public void download(String path, int threadCount) throws Exception {
- downloadUrl = new URL(path);
- HttpURLConnection conn = (HttpURLConnection) downloadUrl
- .openConnection();
- /* 设置 GET 请求方式 */
- conn.setRequestMethod("GET");
- /* 设置响应时间超时为 5 秒 */
- conn.setConnectTimeout(5 * 1000);
- /* 获取本地文件名 */
- String filename = parseFilename(path);
- /* 获取下载文件的总大小 */
- int dataLen = conn.getContentLength();
- if (dataLen < 0) {
- System.out.println("获取数据失败");
- return;
- }
- /* 创建本地目标文件,并设置其大小为准备下载文件的总大小 */
- localFile = new File(filename);
- RandomAccessFile accessFile = new RandomAccessFile(localFile, "rwd");
- /* 这时候,其实本地目录下,已经创建好了一个大小为下载文件的总大小的文件 */
- accessFile.setLength(dataLen);
- accessFile.close();
- /* 计算每条线程要下载的数据大小 */
- block = dataLen % threadCount == 0 ? dataLen / threadCount : dataLen / threadCount + 1;
- /* 启动线程下载文件 */
- for (int i = 0; i < threadCount; i++) {
- new DownloadThread(i).start();
- }
- }
- /**
- * 解析文件
- */
- private String parseFilename(String path) {
- return path.substring(path.lastIndexOf("/") + 1);
- }
- /**
- * 内部类: 文件下载线程类
- */
- private final class DownloadThread extends Thread {
- /* 线程 id */
- private int threadid;
- /* 开始下载的位置 */
- private int startPosition;
- /* 结束下载的位置 */
- private int endPosition;
- /**
- * 新建一个下载线程
- * @param threadid 线程 id
- */
- public DownloadThread(int threadid) {
- this.threadid = threadid;
- startPosition = threadid * block;
- endPosition = (threadid + 1) * block - 1;
- }
- @Override
- public void run() {
- System.out.println("线程 '" + threadid + "'启动下载..");
- RandomAccessFile accessFile = null;
- try {
- /* 设置从本地文件的什么位置开始写入数据 ,"rwd" 表示对文件具有读写删权限 */
- accessFile = new RandomAccessFile(localFile, "rwd");
- accessFile.seek(startPosition);
- HttpURLConnection conn = (HttpURLConnection) downloadUrl.openConnection();
- conn.setRequestMethod("GET");
- conn.setReadTimeout(5 * 1000);
- /* 为 HTTP 设置 Range 属性,可以指定服务器返回数据的范围 */
- conn.setRequestProperty("Range", "bytes=" + startPosition + "-"
- + endPosition);
- /* 将数据写往本地文件 */
- writeTo(accessFile, conn);
- System.out.println("线程 '" + threadid + "'完成下载");
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if(accessFile != null) {
- accessFile.close();
- }
- } catch (IOException ex) {
- ex.printStackTrace();
- }
- }
- }
- /**
- * 将下载数据写往本地文件
- */
- private void writeTo(RandomAccessFile accessFile,
- HttpURLConnection conn){
- InputStream is = null;
- try {
- is = conn.getInputStream();
- byte[] buffer = new byte[1024];
- int len = -1;
- while ((len = is.read(buffer)) != -1) {
- accessFile.write(buffer, 0, len);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if(is != null) {
- is.close();
- }
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- }
- }
- }
- }
通过HTTP协议实现多线程下载的更多相关文章
- java编写的Http协议的多线程下载器
断点下载器还在实现中...... //////////////////////////////////界面/////////////////////////////////////////// pac ...
- <基于Qt与POSIX线程>多线程下载器的简易搭建
原创博客,转载请联系博主! 本项目已托管到本人Git远程库:https://github.com/yue9944882/Snow 项目目标 Major Functionality 开发环境: Ce ...
- 我的Android进阶之旅------>Android基于HTTP协议的多线程断点下载器的实现
一.首先写这篇文章之前,要了解实现该Android多线程断点下载器的几个知识点 1.多线程下载的原理,如下图所示 注意:由于Android移动设备和PC机的处理器还是不能相比,所以开辟的子线程建议不要 ...
- Java--使用多线程下载,断点续传技术原理(RandomAccessFile)
一.基础知识 1.什么是线程?什么是进程?它们之间的关系? 可以参考之前的一篇文章:java核心知识点学习----并发和并行的区别,进程和线程的区别,如何创建线程和线程的四种状态,什么是线程计时器 简 ...
- android 学习随笔十三(网络:多线程下载)
多线程断点续传下载1.多线程:快* 原理:抢占服务器资源* 单线程下载:线程从第0个字节开始下,下到最后一个字节,在本地硬盘的临时文件中从第0个字节开始写,写到最后一个字节,下载完成时,临时文件也写完 ...
- JAVA基础知识之网络编程——-网络基础(Java的http get和post请求,多线程下载)
本文主要介绍java.net下为网络编程提供的一些基础包,InetAddress代表一个IP协议对象,可以用来获取IP地址,Host name之类的信息.URL和URLConnect可以用来访问web ...
- CentOS6.4 安装aria2多线程下载工具
aria2是一个Linux下的多线程下载工具,支持HTTP/HTTPS.FTP.BitTorrent.Metalink协议. 平时在linux上下载http上的东西常用如wget.curl命令,但是他 ...
- python10min系列之多线程下载器
今天群里看到有人问关于python多线程写文件的问题,联想到这是reboot的架构师班的入学题,我想了一下,感觉坑和考察的点还挺多,可以当成一个面试题来问,简单说一下我的想法和思路吧,涉及的代码和注释 ...
- [Mac] mac linux 多线程下载利器 axel
> 之前做过一些文件下载的统计,发现谷歌浏览器chrome和火狐firefox, 一般都是单线程的下载文件,360浏览器却是多线程的下载. 现在切换到了mac上,发现没有360哪个浏览器,就像 ...
随机推荐
- 关于表格前面checkbox复选框不打勾的问题
当点击左边的树节点的时候,让右边的表格自动选中相应的行,但是选中的行前面如果有checkbox,可能复选框虽然选中了但是不打上勾,解决方案,将遍历表格数据那段代码用延时器包裹一下.
- C++中各种数据类型占据字节长度
准备校招笔试的时候经常遇到C++某个数据类型占据多少个字节的问题,查阅了下资料,总结如下: 首先罗列一下C++中的数据类型都有哪些: 1.整形:int.long 2.字符型:char.wchar_t ...
- 004医疗项目-逆向工程-pojo类的创建和对应xml的生成
我们使用mybatis的逆向工程来生成pojo类,省去很多不必要的工作. 我把逆向工程需要的项目如下:
- [转]php返回json数据中文显示的问题
转自 : http://blog.csdn.net/superbirds/article/details/8091910 解决方法: <?php function Notice(){ ...
- [tools]google神器浏览器下载
google神器下载 这是一款优化了的google浏览器 http://www.ccav1.me/chromegae.html
- 基于jquery实现拆分姓名的方法
jquery拆分姓名处理程序如下,纯js实现的,感兴趣的朋友可以参考下哈,希望对你有所帮助 之前已经分享过一个在dom中用户输入姓名后自动用js拆分成姓与名到表单中的jquery插件,由于项目的需要, ...
- 在matlab中实现遥感影像和shp文件的结合显示
clc;close all;clear; road=shaperead('boston_roads.shp'); %读取shape文件 figure, mapshow('boston.tif'); % ...
- listview向下滑动过程中背景色变成黑色和一些奇怪问题
ListView是一个经常要用到的android控件,现总结遇到过的一些美化的小细节. 1.listview在拖动的时候背景图片消失变成黑色背景,等到拖动完毕我们自己的背景图片才显示出来 这个问题是我 ...
- 淘宝SKU组合查询算法实现
淘宝SKU组合查询算法实现 2015-11-14 16:18 1140人阅读 评论(0) 收藏 举报 分类: JavaScript(14) 目录(?)[+] 前端有多少事情可以做,能做到多 ...
- LeetCode:Pascal's Triangle I II
LeetCode:Pascal's Triangle Given numRows, generate the first numRows of Pascal's triangle. For examp ...