【Java EE 学习 22 下】【单线程下载】【单线程断点下载】【多线程下载】

一、文件下载简述

  1.使用浏览器从网页上下载文件,Servlet需要增加一些响应头信息

    (1)response.setContentType("application/force-download");

    (2)response.setContentLength(fis.available());

    (3)response.setHeader("Content-Disposition","attachment;filename="+filename);

  2.如果需要下载的文件名是中文,则还需要特殊对待

    (1)如果使用get方式向Servlet进行的请求,需要编码才能获取正确的文件名

      String filename=request.getParameter("filename");
      filename=new String(filename.getBytes("iso-8859-1"),"utf-8");

    (2)必须通知浏览器实际文件名是中文的,但是必须要经过编码才行。

      filename=URLEncoder.encode(filename,"utf-8");

      注:不经过编码的中文文件名能够成功下载,但是文件名是乱码。

  3.文件下载既能够是GET方式的请求,也可以是POST方式的请求。但是文件上传必须是GET方式的请求。

  4.使用多线程文件下载和断点下载都需要的核心类:RandomAccessFile类。

  API1.6对其描述为:   

   此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。

   通常,如果此类中的所有读取例程在读取所需数量的字节之前已到达文件末尾,则抛出 EOFException(是一种 IOException)。如果由于某些原因无法读取任何字节,而不是在读取所需数量的字节之前已到达文件末尾,则抛出 IOException,而不是 EOFException。需要特别指出的是,如果流已被关闭,则可能抛出 IOException。 

二、单线程文件下载(网页上从服务器下载)

  1.JSP文件

<a href="<c:url value='/downloadFromServer?filename=动漫.jpg'/>">动漫.jpg下载</a><br/>

  2.Servlet响应请求

package com.kdyzm.servlet.singlethread;
/*
 * 从服务器上进行单线程下载示例。
 * 非断点下载
 * 下载既可以是get方式也可以是post方式。
 */
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DownloadFromServer extends HttpServlet {
    
    private static final long serialVersionUID = 1L;
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doPost(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        
        String filename=request.getParameter("filename");
        filename=new String(filename.getBytes("iso-8859-1"),"utf-8");
        //第一步:设置相应类型
        response.setContentType("application/force-download");
        //第二步:读取文件
        String path=this.getServletContext().getRealPath("/resource")+"/"+filename;
        FileInputStream fis=new FileInputStream(path);
        
        //第三步:设置响应头,对文件名进行URL编码
        filename=URLEncoder.encode(filename,"utf-8");
        response.setContentLength(fis.available());
        response.setHeader("Content-Disposition","attachment;filename="+filename);
        
        //第三步:开始文件复制
        OutputStream os=response.getOutputStream();
        int length=-1;
        byte[]buff=new byte[1024*1024];
        while((length=fis.read(buff))!=-1)
        {
            os.write(buff, 0, length);
        }
        os.close();
        fis.close();
    }
}
DownloadFromServer.java

  3.运行结果:略

三、单线程断点下载(使用HttpURLConnection模拟浏览器向服务器发出请求)

  1.断点下载原理。

   (1)使用RandomAccessFile类对文件进行读写操作。

       使用seek方法进行文件指针的定位。

      使用已下载部分的文件大小确定剩余部分文件的字节数量以及请求的字节范围。

   (2)设置请求头部信息,确定请求范围大小

      conn.setRequestProperty("range", "bytes="+fileSize+"-");

      fileSize是已下载文件的大小。

   (3)使用range设置请求头部信息,响应码是206,而非普通的请求成功状态码200

  2.断点下载实现代码

package com.kdyzm.singlethread.breakpoint;

import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

/*
 * 单线程断点下载实现文件下载功能。
 */
public class DownloadFromServerByBreakpoint {
    public static void main(String[] args) throws Exception {
        String fileName="video.mp4";
        String path="http://localhost:8080/day22_2/resource/"+fileName;
        File file=new File("d://download/"+fileName);
        long fileSize=file.length();
        URL url=new URL(path);
        HttpURLConnection conn=(HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("range", "bytes="+fileSize+"-");
        conn.setDoInput(true);
        conn.connect();
        int code=conn.getResponseCode();
        System.out.println("响应结果是:"+code);
        if(code==206)
        {
            InputStream is=conn.getInputStream();
            System.out.println("服务器返回的字节数:"+conn.getContentLength());
            System.out.println("这次从哪里开始写入:"+fileSize);
            //必须要使用RandomAccessFile进行读写才行
            RandomAccessFile out=new RandomAccessFile(file,"rw");
            out.seek(fileSize);
            byte []buff=new byte[100];
            int length=-1;
            while((length=is.read(buff))!=-1)
            {
                out.write(buff, 0, length);
            }
            out.close();
            System.out.println("下载成功!");
        }
    }
}
DownloadFromServerByBreakpoint.java

四、多线程文件下载

  1.实现原理

   模拟迅雷,在下载的时候不管有没有下载完成都要在磁盘上创建一个相同大小的文件,在该文件对应着多个RandomAccessFile对象,利用多线程可以创建出多个RandomAccessFile对象并分别拥有不同的文件指针对该文件进行写入操作。

  2.实现代码。

package com.kdyzm.multiplethreaddownload;
/*
 * 这种方式的多线程下载才是真正的多线程下载
 * 没有加上同步代码块,几个线程同时向文件中写入内容,
 * 但是为什么效率反而变低了???
 */
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
/*
 * 该小练习演示多线程下载文件的方法与技巧。
 */
public class MultipleThreadDownloadx {
    public static void main(String[] args) throws Exception {
        System.out.println("没有同步代码块的多线程下载!");
//        String path="http://danbooru.donmai.us/data/8fc288b237cfcc0a8e026f79b65733ed.jpg";
        String path="http://localhost:8080/day22_2/resource/video.mp4";
        URL url=new URL(path);
        HttpURLConnection conn=(HttpURLConnection) url.openConnection();
        //对connection对象进行一系列的设置处理。
        conn.setRequestMethod("GET");//设置GET请求
        conn.setDoInput(true);
        conn.connect();
        //获取状态码
        int code=conn.getResponseCode();
        System.out.println(code);
        if(code==200)//如果请求成功,则开启多线程下载文件
        {
            String fileName="d://download/video.mp4";
            long fileSize=conn.getContentLength();
            RandomAccessFile out=new RandomAccessFile(new File(fileName),"rw");
            out.setLength(fileSize);
            System.out.println("文件的总大小为:"+fileSize);
            long threadCount=8;//将会有四个线程参与多线程下载任务。
            long threadPerDownloadSize=fileSize/threadCount+(fileSize%threadCount==0?0:1);
            System.out.println("每个线程将会下载的数据量是:"+threadPerDownloadSize);
            //下面计算每个具体的线程将会下载的数据量并创建线程对象开启线程下载。
            for(long i=0;i<threadCount;i++)
            {
                //计算开始的字节
                long start=i*threadPerDownloadSize;
                long end=start+(threadPerDownloadSize-1);
                System.out.println("第"+(i+1)+"个线程下载的内容为"+start+"-"+end);
                new DownloadThreadx(fileName,url.toString(),start,end).start();
            }
        }
        conn.disconnect();//连接断开
    }
}
class DownloadThreadx extends Thread
{
    private String  fileName;
    private String url;
    private long start;
    private long end;
    public DownloadThreadx(String fileName, String url, long start, long end) {
        this.fileName=fileName;
        this.url=url;
        this.start=start;
        this.end=end;
    }
    //重写run方法。
    @Override
    public void run() {
            try {
                HttpURLConnection conn=(HttpURLConnection) new URL(url).openConnection();
                conn.setRequestMethod("GET");
                conn.setDoInput(true);
                conn.setRequestProperty("range","bytes="+start+"-"+end);
                conn.connect();
                int code=conn.getResponseCode();
                System.out.println("响应状态码为:"+code);
                if(code==206)
                {
                    long size=conn.getContentLength();
                    InputStream in=conn.getInputStream();
                    System.out.println("线程"+this.getName()+"下载的数据量为:"+size);
                    RandomAccessFile out=new RandomAccessFile(new File(fileName),"rw");
                    out.seek(start);
                    byte []buff=new byte[1024*1024];
                    int length=-1;
                    while((length=in.read(buff))!=-1)
                    {
                        out.write(buff, 0, length);
                    }
                    in.close();
                    out.close();
                }
                conn.disconnect();//连接断开
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
}
MultipleThreadDownloadx.java

  3.思考

   (1)两种多线程实现方式?不!!

      使用2改代码可以实现真正意义上的多线程文件下载,因为每个线程都是用不同的RandomAccessFile对象对文件进行的读写操作。如果改写成为使用同一个RandomAccessFile对象有如何呢?这需要对run方法进行加锁,但是一旦加锁线程将会顺序执行写入操作,这样一来就和单线程下载文件想通了,甚至效率更低,而且由于需要对服务器进行多次请求,如果服务器不稳定的话非常有可能出现其中一个或者几个线程连接超时的情况,这时候其他线程所做的努力就白费了,这种方式应当坚决杜绝出现

   代码错误示例:

package com.kdyzm.multiplethreaddownload;
/*
 * 这种方式是多线程下载的方式,但是这种方式的多线程和单线程并没有任何区别,甚至更耗时间,降低了效率
 * 不应当适用这种方式
 */
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

class Resource
{
    public static final Object obj=new Object();
}
/*
 * 该小练习演示多线程下载文件的方法与技巧。
 */
public class MultipleThreadDownload {
    public static void main(String[] args) throws Exception {
        String path="http://danbooru.donmai.us/data/8fc288b237cfcc0a8e026f79b65733ed.jpg";
//        String path="http://localhost:8080/day22_2/resource/video.mp4";
        URL url=new URL(path);
        HttpURLConnection conn=(HttpURLConnection) url.openConnection();
        //对connection对象进行一系列的设置处理。
        conn.setRequestMethod("GET");//设置GET请求
        conn.setDoInput(true);
        conn.connect();
        //获取状态码
        int code=conn.getResponseCode();
        System.out.println(code);
        if(code==200)//如果请求成功,则开启多线程下载文件
        {
            String fileName="d://download/video.mp4";
            long fileSize=conn.getContentLength();
            RandomAccessFile out=new RandomAccessFile(new File(fileName),"rw");
            out.setLength(fileSize);
            System.out.println("文件的总大小为:"+fileSize);
            long threadCount=8;//将会有四个线程参与多线程下载任务。
            long threadPerDownloadSize=fileSize/threadCount+(fileSize%threadCount==0?0:1);
            System.out.println("每个线程将会下载的数据量是:"+threadPerDownloadSize);
            //下面计算每个具体的线程将会下载的数据量并创建线程对象开启线程下载。
            for(long i=0;i<threadCount;i++)
            {
                //计算开始的字节
                long start=i*threadPerDownloadSize;
                long end=start+(threadPerDownloadSize-1);
                System.out.println("第"+(i+1)+"个线程下载的内容为"+start+"-"+end);
                new DownloadThread(out,url.toString(),start,end).start();
            }
        }
        conn.disconnect();//连接断开
    }
}
class DownloadThread extends Thread
{
    private RandomAccessFile out;
    private String url;
    private long start;
    private long end;
    public DownloadThread(RandomAccessFile out, String url, long start, long end) {
        this.url=url;
        this.out=out;
        this.start=start;
        this.end=end;
    }
    //重写run方法。
    @Override
    public void run() {
        synchronized(Resource.obj)
        {
            try {
                HttpURLConnection conn=(HttpURLConnection) new URL(url).openConnection();
                conn.setRequestMethod("GET");
                conn.setDoInput(true);
                conn.setRequestProperty("range","bytes="+start+"-"+end);
                conn.connect();
                
                int code=conn.getResponseCode();
                System.out.println("响应状态码为:"+code);
                
                if(code==206)
                {
                    long size=conn.getContentLength();
                    InputStream in=conn.getInputStream();
                    System.out.println("线程"+this.getName()+"下载的数据量为:"+size);
                    out.seek(start);
                    byte []buff=new byte[1024*1024];
                    int length=-1;
                    while((length=in.read(buff))!=-1)
                    {
                        out.write(buff, 0, length);
                    }
                    in.close();
                }
                conn.disconnect();//连接断开
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
MultipleThreadDownload.java

    曾经的博客中:http://www.cnblogs.com/kuangdaoyizhimei/p/4048015.html 出现过这种错误。

  (2)为什么多线程下载文件的效率比单线程下载文件的效率更为低下?

    待续。

原文地址:https://www.cnblogs.com/kuangdaoyizhimei/p/4604564.html