每日一讲
我不知道我应当成为一个什么样的人,但我知道我应当不成为什么样的人!
目录
3.2创建CountDownLatch类及调用MergeFile类
1.多线程文件分区下载目的和原理
目的:正常我们下载文件通常是一下到底,只使用一个线程进行下载。但是通常情况下使用多个线程下载的效率会更高,迅雷下载中也是推荐使用三个线程进行下载,当然这也取决于网络,网络不好,线程多也未必是好事。
原理:将一个文件分成多端进行下载,每个线程下载文件的一部分,就好比一个苹果咱们三个好基友一起吃。用这么一张图就能很好的了解了。
2.多线程文件下载的实现过程
实际上想要实现简单的多线程任务设计的技术不难,但也有许多,比如多线程、I/O流、参数传递、构造函数、类和对象等等。
文件链接:https://dldir1.qq.com/qqfile/qq/QQNT/Windows/QQ_9.9.7_240305_x64_01.exe
2.1获取下载文件的大小
要分区间下载我们就必然要文件的总大小,才能更好的分配嘛。
URL url =new URL(address);
HttpURLConnection httpURLConnection= (HttpURLConnection) url.openConnection();
long fileSize=httpURLConnection.getContentLengthLong();//获取要下载文件的大小
System.out.println("文件大小为:"+fileSize);
2.2计算每个线程需要下载的范围
既然大厨小林煮好一锅饭了,那么必然要合理的分配好每个小朋友要吃的饭咯。
long size=fileSize/threadNum;//每个线程下载的部分
//startPos:起始位置 endPos:结束位置
for(int i=0;i<threadNum;i++)
{
long startPos= i* size;
long endPos;
if(i==threadNum-1){
endPos=0;
}else{
endPos=startPos+size;
}
if(startPos!=0){
startPos++;
}
System.out.println("第"+(i+1)+"个线程下载的区间为:"+startPos+"----"+endPos);
}
2.3创建线程进行下载任务
哥们,饭已经给你分完了,那接下来就是吃光这包分配的饭勒。
//每循环一次就启动一个线程
DownloadThread downloadThread=new DownloadThread(startPos,endPos,url,i+1,address,threadNum,countDownLatch);
downloadThread.start();
class DownloadThread extends Thread{
long startPos;
long endPos;
URL url;
int index;
String address;
int threadNum;
CountDownLatch countDownLatch;
//创建构造函数传递参数进来,(需要哪些参数就传哪些过来)
public DownloadThread(long startPos,long endPos,URL url,int index,String address,int threadNum,CountDownLatch countDownLatch)
{
this.startPos=startPos;
this.endPos=endPos;
this.url=url;
this.index=index;
this.address=address;
this.threadNum=threadNum;
this.countDownLatch=countDownLatch;
}
public void run(){
}
}
2.4在线程内进行读写操作实现文件下载
虽然说饭已经吃下去了,但是哥们得消化呀,光吃不拉早晚得成小胖呀,那么接下来我们就需要进行文件下载咯,先读后写,先读后写,先读后写--IO流秘诀可不能忘。
//文件下载--先读后写
try {
//这里用于断点续传,每次只请求文件的一部分
URLConnection urlConnection = (HttpURLConnection) url.openConnection();
if(index!=threadNum){
urlConnection.setRequestProperty("Range","byte="+startPos+"-"+endPos);
}else{
urlConnection.setRequestProperty("Range","byte="+startPos);
}
InputStream inputStream = urlConnection.getInputStream();
byte data[]=new byte[1024*8];
int num=0;
String fileName=address.substring(address.lastIndexOf("/")+1);
FileOutputStream fileOutputStream=new FileOutputStream(fileName+"_0"+index);
while((num=inputStream.read(data))!=-1){
//开始写
fileOutputStream.write(data,0,num);
}
fileOutputStream.close();
inputStream.close();
countDownLatch.countDown();
} catch (IOException e) {
throw new RuntimeException(e);
}
完整下载代码和运行结果
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
public class Test {
public static void main(String[] args) throws IOException {
String address="https://dldir1.qq.com/qqfile/qq/QQNT/Windows/QQ_9.9.7_240305_x64_01.exe";
int threadNum=3;
URL url=new URL(address);
HttpURLConnection httpURLConnection= (HttpURLConnection)url.openConnection();
long fileSize = httpURLConnection.getContentLengthLong();
System.out.println("文件大小为:"+fileSize);
//获取每个线程要下载的区间大小
long size = fileSize/threadNum;
for (int i = 0; i < threadNum; i++) {
long startPos = i*size;
long endPos;
if(i==threadNum-1)
{
endPos=0;
}else{
endPos=startPos+size;
}
if(startPos!=0)
{
startPos++;
}
System.out.println("第"+(i+1)+"个线程下载的的区间为:"+startPos+"----"+endPos);
//每循环一次启动一个线程
TestDownloadThread testDownloadThread=new TestDownloadThread(startPos,endPos,url,(i+1),threadNum,address);
testDownloadThread.start();
}
}
}
class TestDownloadThread extends Thread{
long startPos;
long endPos;
URL url;
int index;
int threadNum;
String address;
public TestDownloadThread(long startPos,long endPos,URL url,int index,int threadNum,String address){
this.startPos=startPos;
this.endPos=endPos;
this.url=url;
this.index=index;
this.threadNum=threadNum;
this.address=address;
}
public void run(){
//先读后写
try {
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
if(index!=threadNum)
{
urlConnection.setRequestProperty("Range","byte="+startPos+"-"+endPos);
}else{
urlConnection.setRequestProperty("Range","byte="+startPos);
}
InputStream inputStream = urlConnection.getInputStream();
byte data[]=new byte[1024*8];
int num=0;
String fileName=address.substring(address.lastIndexOf("/")+1);
FileOutputStream fileOutputStream=new FileOutputStream(fileName+"_0"+index);
while((num=inputStream.read(data))!=-1){
//开始写
fileOutputStream.write(data,0,num);
}
fileOutputStream.close();
inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
很好进行到这里你已经能够得到n个区间的下载文件了,已经成功了一小部分,但是这还远远不够,文件分段下载完了肯定是不能用的,所以我们还需要将这些小文件合并成一个能用的文件。
3.文件合成
文件合成实际上用到的主要技术还是IO流,接下来我们就进行文件的合成任务。
3.1创建新类MergeFile合并文件
在DownLoad类中我们已经下载好文件了,那么此时我们再新建一个文件类MergeFile进行合并文件。
public class MergeFile {
//构造函数的特点:1.没有返回值 2.函数名字和类名一样
String fileNames[];
public MergeFile(String fileNames[]){
this.fileNames=fileNames;
}
public void Merge() throws IOException {
//具体的合并代码
byte data[]=new byte[1024*8];
int len=0;
//用于获取文件名
String fileName=fileNames[0].substring(0,fileNames[0].lastIndexOf("_"));
FileOutputStream fileOutputStream=new FileOutputStream(fileName);
for (int i = 0; i < fileNames.length; i++) {
FileInputStream fileInputStream=new FileInputStream(fileNames[i]);
while((len=fileInputStream.read(data))!=-1){
fileOutputStream.write(data,0,len);
}
fileInputStream.close();
}
fileOutputStream.close();
}
}
3.2创建CountDownLatch类及调用MergeFile类
为了能够在DownLoad类中运行代码,实现文件合并,我们需要在原本的Download类中创建一个倒数计数器类CountDownLatch以及调用MergeFile类。
//倒数计数器
CountDownLatch countDownLatch=new CountDownLatch(threadNum);
//写在每个线程进行读写完成后位置
countDownLatch.countDown();
//调用MergeFile类
//新建一个文件名数组
String fileNames[]=new String[threadNum];
String fileName=address.substring(address.lastIndexOf("/")+1);
fileNames[i]= fileName+"_0"+(i+1);//添加到第2步for循环中给每个文件赋值
countDownLatch.await();//线程阻塞(等到所有线程任务都结束后才开始运行这句代码后面的代码)
//调用MergeFile类用来合并下载的文件(前面新建一个名字数组用来传参)
MergeFile mergeFile=new MergeFile(fileNames);
mergeFile.Merge();
3.3改善线程个数和下载地址写死问题
为了不固定死下载的地址以及线程的个数,我们使用main方法的参数传递,相信很多人都没有用过这个功能。
String address=args[0];
int threadNum=Integer.parseInt(args[1]);
运行前需要设置配置(Edit Configurations)
设置参数传递,中间留有空格
4.完整代码和运行结果
4.1运行结果
4.2完整代码
DownLoad类
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.CountDownLatch;
public class Download {
public static void main(String[] args) throws IOException, InterruptedException {
//为了不写死下载的地址以及线程的个数,使用main方法的参数传递
String address=args[0];
int threadNum=Integer.parseInt(args[1]);
// String address="https://dldir1.qq.com/qqfile/qq/QQNT/Windows/QQ_9.9.7_240305_x64_01.exe";
// int threadNum=3;
//倒数计数器
CountDownLatch countDownLatch=new CountDownLatch(threadNum);
URL url =new URL(address);
HttpURLConnection httpURLConnection= (HttpURLConnection) url.openConnection();
long fileSize=httpURLConnection.getContentLengthLong();//获取要下载文件的大小
System.out.println("文件大小为:"+fileSize);
long size=fileSize/threadNum;//每个线程下载的部分
//新建一个文件名数组
String fileNames[]=new String[threadNum];
String fileName=address.substring(address.lastIndexOf("/")+1);
//startPos:起始位置 endPos:结束位置
for(int i=0;i<threadNum;i++)
{
fileNames[i]= fileName+"_0"+(i+1);
long startPos= i* size;
long endPos;
if(i==threadNum-1){
endPos=0;
}else{
endPos=startPos+size;
}
if(startPos!=0){
startPos++;
}
System.out.println("第"+(i+1)+"个线程下载的区间为:"+startPos+"----"+endPos);
//每循环一次就启动一个线程
DownloadThread downloadThread=new DownloadThread(startPos,endPos,url,i+1,address,threadNum,countDownLatch);
downloadThread.start();
}
//线程阻塞(等到所有线程任务都结束后才开始运行这句代码后面的代码)
countDownLatch.await();
//调用MergeFile类用来合并下载的文件(前面新建一个名字数组用来传参)
MergeFile mergeFile=new MergeFile(fileNames);
mergeFile.Merge();
}
}
class DownloadThread extends Thread{
long startPos;
long endPos;
URL url;
int index;
String address;
int threadNum;
CountDownLatch countDownLatch;
public DownloadThread(long startPos,long endPos,URL url,int index,String address,int threadNum,CountDownLatch countDownLatch){
this.startPos=startPos;
this.endPos=endPos;
this.url=url;
this.index=index;
this.address=address;
this.threadNum=threadNum;
this.countDownLatch=countDownLatch;
}
public void run(){
//先读后写
try {
URLConnection urlConnection = (HttpURLConnection) url.openConnection();
if(index!=threadNum){
urlConnection.setRequestProperty("Range","byte="+startPos+"-"+endPos);
}else{
urlConnection.setRequestProperty("Range","byte="+startPos);
}
InputStream inputStream = urlConnection.getInputStream();
byte data[]=new byte[1024*8];
int num=0;
String fileName=address.substring(address.lastIndexOf("/")+1);
FileOutputStream fileOutputStream=new FileOutputStream(fileName+"_0"+index);
while((num=inputStream.read(data))!=-1){
//开始写
fileOutputStream.write(data,0,num);
}
fileOutputStream.close();
inputStream.close();
countDownLatch.countDown();
System.out.println("第" + index + "个线程任务结束");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
MergeFile类
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class MergeFile {
String fileNames[];
public MergeFile(String fileNames[]){
this.fileNames=fileNames;
}
public void Merge() throws IOException {
//具体的合并代码
byte data[]=new byte[1024*8];
int len=0;
//用于获取文件名
String fileName=fileNames[0].substring(0,fileNames[0].lastIndexOf("_"));
FileOutputStream fileOutputStream=new FileOutputStream(fileName);
for (int i = 0; i < fileNames.length; i++) {
FileInputStream fileInputStream=new FileInputStream(fileNames[i]);
while((len=fileInputStream.read(data))!=-1){
fileOutputStream.write(data,0,len);
}
fileInputStream.close();
}
fileOutputStream.close();
}
}
结果不难,过程不简单,学习必定要保持耐心,加油!!!