【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)为什么多线程下载文件的效率比单线程下载文件的效率更为低下?

    待续。

时间: 2024-10-14 13:16:34

【Java EE 学习第22天 文件下载】【单线程下载】【单线程断点下载】【多线程下载】的相关文章

Java EE学习--Quartz基本用法

新浪博客完全不适合写技术类文章.本来是想找一个技术性的博客发发自己最近学的东西,发现博客园起源于咱江苏,一个非常质朴的网站,行,咱要养成好习惯,以后没事多总结总结经验吧.很多时候都在网上搜索别人的总结,我自己也总结些东西,或许多多少少能帮得上别人. 首先提到的是Quartz,一个开源的定期执行计划任务的框架.其实我内心好奇这个框架很久了,像那些能定时修改数据库数据,定时分配任务的功能一直觉得很神奇.心动不如行动,今天我就小小的学习了一下用法,力求言简意赅,大家都懂的我就不说了. 第一步:下载Qu

【Java EE 学习第16天】【dbcp数据库连接池】【c3p0数据库连接池】

零.回顾之前使用的动态代理的方式实现的数据库连接池: 代码: 1 package day16.utils; 2 3 import java.io.IOException; 4 import java.lang.reflect.InvocationHandler; 5 import java.lang.reflect.Method; 6 import java.lang.reflect.Proxy; 7 import java.sql.Connection; 8 import java.sql.D

Java EE学习之旅2——Ant

上次也了解了一下Ant是啥了,就是管理Java生成的工具嘛. 然后今天来学习一下如何使用Ant. 首先是指令: -find和-s Ant会一直到上级目录搜索build.xml,直到到达文件系统根目录: -buildfile.-file和-f 使用其他生成文件,例如a.xml: -quiet和-q 运行时只输出少数提示信息: -verbose和-v 输出多一点的提示信息. 2014-07-12 22:17:14 Java EE学习之旅2--Ant

Java EE 学习(7):IDEA + maven + spring 搭建 web(3)- 配置数据库

参考: https://my.oschina.net/gaussik/blog/513444 注:在阅读本文前,请先阅读: Java EE 学习(5):IDEA + maven + spring 搭建 web(1) Java EE 学习(6):IDEA + maven + spring 搭建 web(2) 5 数据库配置 下面,就要通过一个简单的例子,来介绍 SpringMVC 如何集成 Spring Data JPA(由 Hibernate JPA 提供),来进行强大的数据库访问,并通过本章节

JAVA EE 学习笔记[V1 jsp编程]

在三月初学校开设了javaee的课程,也就此展开了对javaee基础的学习.然后老师也对这次的课程有一定要求.前面的基础就为最终的作业做准备啦~ 在上学期我们学习了java相关知识,也对java se 的安装使用有了一定的认知,而java ee则是构建于java se 平台之上的一套多层的,可扩展的的网络应用. 学习java ee我们首先进行环境的搭建.无非就是使用 tomcat进行服务器的搭建和jdk环境变量配置.而IDE这方面我们选择myeclipse 2016 CI(这个编译器自带tomc

Java EE 学习(5):IDEA + maven + spring 搭建 web(1)

参考:http://www.cnblogs.com/lonelyxmas/p/5397422.html http://www.ctolib.com/docs-IntelliJ-IDEA-c--159047.html 孔老师的<SpringMVC视频教程> 记录: 本节主要完成 使用 maven 管理 spring + 项目 包,搭建 maven+spring 的 web 项目平台. 前提: 已安装并配置好 - Intellij IDEA 16.3.5 Ultimate - JDK 1.8.0_

Java EE 学习(8):IDEA + maven + spring 搭建 web(4)- 用户管理

转载:Gaussic(一个致力于AI研究却不得不兼顾项目的研究生) 注:在阅读本文前,请先阅读: Java EE 学习(5):IDEA + maven + spring 搭建 web(1) ava EE 学习(6):IDEA + maven + spring 搭建 web(2)- 配置 Spring Java EE 学习(7):IDEA + maven + spring 搭建 web(3)- 配置数据库 记录: 通过对用户表的管理,更加深入地讲解SpringMVC的操作. 6 用户管理 既然我们

Java EE 学习(9):IDEA + maven + spring 搭建 web(5)- 博客文章管理

转载:Gaussic(一个致力于AI研究却不得不兼顾项目的研究生) . 注:在阅读本文前,请先阅读: Java EE 学习(5):IDEA + maven + spring 搭建 web(1) Java EE 学习(6):IDEA + maven + spring 搭建 web(2)- 配置 Spring Java EE 学习(7):IDEA + maven + spring 搭建 web(3)- 配置数据库 Java EE 学习(8):IDEA + maven + spring 搭建 web(

Java EE学习之旅1——HeadFirstJavaEE

因为找到的实习是用Java开发的公司,所以来学习一下Java EE的知识. 首先找来了书<轻量级Java EE企业应用实战>. 啊不得不说学了Java之后直接看这个还是完全不行呢,好多名词看都没有看过啊哈哈. 首先来看看都些啥看不懂的词... 1.JSP.Servlet和JavaBean JSP和Servlet都是用在表现层的东西,而实质上JSP编译成Servlet才运行. 但Servlet开发成本太大,所以用JSP. JavaBean用来通信交换表现层和底层数据. 2.MVC和Struts