Java网络编程从入门到精通(24):实现HTTP断点续传下载工具(附源代码)

简介: 本文为原创,如需转载,请注明作者和出处,谢谢! 上一篇:Java网络编程从入门到精通(23):HTTP消息头字段 源代码下载:download.rar    在前面的文章曾讨论了HTTP消息头的三个和断点继传有关的字段。

使用至少一个空格来分隔这三部分。这个下载工具逐个下载这些文件,在这些文件全部下载完后程序退出。

l         断点续传的工作原理

“断点续传”顾名思义,就是一个文件下载了一部分后,由于服务器或客户端的原因,当前的网络连接中断了。在中断网络连接后,用户还可以再次建立网络连接来继续下载这个文件还没有下完的部分。

要想实现单线程断点续传,必须在客户断保存两个数据。

1.       已经下载的字节数。

2.       下载文件的URL

一但重新建立网络连接后,就可以利用这两个数据接着未下载完的文件继续下载。在本下载工具中第一种数据就是文件已经下载的字节数,而第二个数据在上述的下载文件中保存。

在继续下载时检测已经下载的字节数,假设已经下载了3000个字节,那么HTTP请求消息头的Range字段被设为如下形式:

Range: bytes = 3000 -

HTTP响应消息头的Content-Range字段被设为如下的形式:

Content-Range: bytes  3000 - 10000 / 10001

l         实现断点续传下载工具

一个断点续传下载程序可按如下几步实现:

1.       输入要下载文件的URL和要保存的本地文件名,并通过Socket类连接到这个URL

所指的服务器上。

2.       在客户端根据下载文件的URL和这个本地文件生成HTTP请求消息。在生成请求

消息时分为两种情况:

(1)第一次下载这个文件,按正常情况生成请求消息,也就是说生成不包含Range

字段的请求消息。

2)以前下载过,这次是接着下载这个文件。这就进入了断点续传程序。在这种情况生成的HTTP请求消息中必须包含Range字段。由于是单线程下载,因此,这个已经下载了一部分的文件的大小就是Range的值。假设当前文件的大小是1234个字节,那么将Range设成如下的值:

Range:bytes = 1234 -

3.       向服务器发送HTTP请求消息。

4.       接收服务器返回的HTTP响应消息。

5.       处理HTTP响应消息。在本程序中需要从响应消息中得到下载文件的总字节数。如

果是第一次下载,也就是说响应消息中不包含Content-Range字段时,这个总字节数也就是Content-Length字段的值。如果响应消息中不包含Content-Length字段,则这个总字节数无法确定。这就是为什么使用下载工具下载一些文件时没有文件大小和下载进度的原因。如果响应消息中包含Content-Range字段,总字节数就是Content-Rangebytes m-n/k中的k,如Content-Range的值为:

Content-Range:bytes  1000 - 5000 / 5001

则总字节数为5001。由于本程序使用的Range值类型是得到从某个字节开始往后的所有字节,因此,当前的响应消息中的Content-Range总是能返回还有多少个字节未下载。如上面的例子未下载的字节数为5000-1000+1=4001

6. 开始下载文件,并计算下载进度(百分比形式)。如果网络连接断开时,文件仍未下载完,重新执行第一步。也果文件已经下载完,退出程序。

分析以上六个步骤得知,有四个主要的功能需要实现:

1. 生成HTTP请求消息,并将其发送到服务器。这个功能由generateHttpRequest方法来完成。

2. 分析HTTP响应消息头。这个功能由analyzeHttpHeader方法来完成。

3. 得到下载文件的实际大小。这个功能由getFileSize方法来完成。

4. 下载文件。这个功能由download方法来完成。

以上四个方法均被包含在这个断点续传工具的核心类HttpDownload.java中。在给出HttpDownload类的实现之前先给出一个接口DownloadEvent接口,从这个接口的名字就可以看出,它是用来处理下载过程中的事件的。下面是这个接口的实现代码:

  package download;
  
  
public   interface  DownloadEvent
  {
      
void  percent( long  n);              //  下载进度
       void  state(String s);               //  连接过程中的状态切换
       void  viewHttpHeaders(String s);     //  枚举每一个响应消息字段
  }

从上面的代码可以看出,DownloadEvent接口中有三个事件方法。在以后的主函数中将实现这个接口,来向控制台输出相应的信息。下面给出了HttpDownload类的主体框架代码:

   001    package download;
  
002   
  
003    import  java.net. * ;
  
004    import  java.io. * ;
  
005    import  java.util. * ;
  
006   
  
007    public   class  HttpDownload
  
008   {
  
009        private  HashMap httpHeaders  =   new  HashMap();
  
010        private  String stateCode;
  
011   
  
012        //  generateHttpRequest方法
   013       
  
014        /*   ananlyzeHttpHeader方法
  015       *  
  016       *  addHeaderToMap方法
  017       * 
  018       *  analyzeFirstLine方法
  019       
*/      
  
020   
  
021        //  getFileSize方法
   022   
  023        //  download方法
   024           
  
025        /*   getHeader方法
  026       *  
  027       *  getIntHeader方法
  028       
*/
  
029   }

上面的代码只是HttpDownload类的框架代码,其中的方法并未直正实现。我们可以从中看出第012014021023行就是上述的四个主要的方法。在016018行的addHeaderToMapanalyzeFirstLine方法将在analyzeHttpHeader方法中用到。而025027行的getHeadergetIntHeader方法在getFileSizedownload方法都会用到。上述的八个方法的实现都会在后面给出。

   001    private   void  generateHttpRequest(OutputStream out, String host,
  
002           String path,  long  startPos)  throws  IOException
  
003   {
  
004       OutputStreamWriter writer  =   new  OutputStreamWriter(out);
  
005       writer.write( " GET  "   +  path  +   "  HTTP/1.1/r/n " );
  
006       writer.write( " Host:  "   +  host  +   " /r/n " );
  
007       writer.write( " Accept: */*/r/n " );
  
008       writer.write( " User-Agent: My First Http Download/r/n " );
  
009        if  (startPos  >   0 //  如果是断点续传,加入Range字段
   010           writer.write( " Range: bytes= "   +  String.valueOf(startPos)  +   " -/r/n " );
  
011       writer.write( " Connection: close/r/n/r/n " );
  
012       writer.flush();
  
013   }

这个方法有四个参数:

1.   OutputStream out

使用Socket对象的getOutputStream方法得到的输出流。

2.  String host

下载文件所在的服务器的域名或IP

3.  String path

       下载文件在服务器上的路径,也就跟在GET方法后面的部分。

4.  long startPos

       从文件的startPos位置开始下载。如果startPos0,则不生成Range字段。

   001    private   void  analyzeHttpHeader(InputStream inputStream, DownloadEvent de)
  
002          throws  Exception
  
003   {
  
004       String s  =   "" ;
  
005        byte  b  =   - 1 ;
  
006        while  ( true )
  
007       {
  
008           b  =  ( byte ) inputStream.read();
  
009            if  (b  ==   ' /r ' )
  
010           {
  
011               b  =  ( byte ) inputStream.read();
  
012                if  (b  ==   ' /n ' )
  
013               {
  
014                    if  (s.equals( "" ))
  
015                        break ;
  
016                   de.viewHttpHeaders(s);
  
017                   addHeaderToMap(s);
  
018                   s  =   "" ;
  
019               }
  
020           }
  
021            else
  
022               s  +=  ( char ) b;
  
023       }
  
024   }
  
025
  
026    private   void  analyzeFirstLine(String s)
  
027   {
  
028       String[] ss  =  s.split( " [ ]+ " );
  
029        if  (ss.length  >   1 )
  
030           stateCode  =  ss[ 1 ];
  
031   }
  
032    private   void  addHeaderToMap(String s)
  
033   {
  
034        int  index  =  s.indexOf( " : " );
  
035        if  (index  >   0 )
  
036           httpHeaders.put(s.substring( 0 , index), s.substring(index  +   1 ) .trim());
  037        else
  
038           analyzeFirstLine(s);
  
039   }

001 024行:analyzeHttpHeader方法的实现。这个方法有两个参数。其中inputStream是用Socket对象的getInputStream方法得到的输入流。这个方法是直接使用字节流来分析的HTTP响应头(主要是因为下载的文件不一定是文本文件;因此,都统一使用字节流来分析和下载),每两个""r"n"之间的就是一个字段和字段值对。在016行调用了DownloadEvent接口的viewHttpHeaders事件方法来枚举每一个响应头字段。

026 031行:analyzeFirstLine方法的实现。这个方法的功能是分析响应消息头的第一行,并从中得到状态码后,将其保存在stateCode变量中。这个方法的参数s就是响应消息头的第一行。

032 039行:addHeaderToMap方法的实现。这个方法的功能是将每一个响应请求消息字段和字段值加到在HttpDownload类中定义的httpHeaders哈希映射中。在第034行查找每一行消息头是否包含":",如果包含":",这一行必是消息头的第一行。因此,在第038行调用了analyzeFirstLine方法从第一行得到响应状态码。

   001    private  String getHeader(String header)
  
002   {
  
003        return  (String) httpHeaders.get(header);
  
004   }
  
005    private   int  getIntHeader(String header)
  
006   {
  
007        return  Integer.parseInt(getHeader(header));
  
008   }

    这两个方法将会在getFileSizedownload中被调用。它们的功能是从响应消息中根据字段字得到相应的字段值。getHeader得到字符串形式的字段值,而getIntHeader得到整数型的字段值。

   001    public   long  getFileSize()
  
002   {
  
003        long  length  =   - 1 ;
  
004        try
  
005       {
  
006           length  =  getIntHeader( " Content-Length " );
  
007           String[] ss  =  getHeader( " Content-Range " ).split( " [/] " );
  
008            if  (ss.length  >   1 )
  
009               length  =  Integer.parseInt(ss[ 1 ]);
  
010            else
  
011               length  =   - 1 ;
  
012       }
  
013        catch  (Exception e)
  
014       {
  
015       }
  
016        return  length;
  
017   }

    getFileSize方法的功能是得到下载文件的实际大小。首先在006行通过Content-Length得到了当前响应消息的实体内容大小。然后在009行得到了Content-Range字段值所描述的文件的实际大小("""后面的值)。如果Content-Range字段不存在,则文件的实际大小就是Content-Length字段的值。如果Content-Length字段也不存在,则返回-1,表示文件实际大小无法确定。

   001    public   void  download(DownloadEvent de, String url, String localFN,
  
002            int  cacheSize)  throws  Exception
  
003   {
  
004       File file  =   new  File(localFN); 
  
005        long  finishedSize  =   0 ;
  
006        long  fileSize  =   0 ;   //  localFN所指的文件的实际大小
   007       FileOutputStream fileOut  =   new  FileOutputStream(localFN,  true );
  
008       URL myUrl  =   new  URL(url);
  
009       Socket socket  =   new  Socket();
  
010        byte [] buffer  =   new   byte [cacheSize];  //  下载数据的缓冲
   011   
  
012        if  (file.exists())
  
013           finishedSize  =  file.length();        
  
014       
  
015        //  得到要下载的Web资源的端口号,未提供,默认是80
   016        int  port  =  (myUrl.getPort()  ==   - 1 ?   80  : myUrl.getPort();
  
017       de.state( " 正在连接 "   +  myUrl.getHost()  +   " : "   +  String.valueOf(port));
  
018       socket.connect( new  InetSocketAddress(myUrl.getHost(), port),  20000 );
  
019       de.state( " 连接成功! " );
  
020       
  
021        //  产生HTTP请求消息
   022       generateHttpRequest(socket.getOutputStream(), myUrl.getHost(), myUrl
  
023               .getPath(), finishedSize);
  
024         
  
025       InputStream inputStream  =  socket.getInputStream();
  
026        //  分析HTTP响应消息头
   027       analyzeHttpHeader(inputStream, de);
  
028       fileSize  =  getFileSize();   //  得到下载文件的实际大小
   029        if  (finishedSize  >=  fileSize)  
  
030            return ;
  
031        else
  
032       {
  
033            if  (finishedSize  >   0   &&  stateCode.equals( " 200 " ))
  
034                return ;
  
035       }
  
036        if  (stateCode.charAt( 0 !=   ' 2 ' )
  
037            throw   new  Exception( " 不支持的响应码 " );
  
038        int  n  =   0 ;
  
039        long  m  =  finishedSize;
  
040        while  ((n  =  inputStream.read(buffer))  !=   - 1 )
  
041       {
  
042           fileOut.write(buffer,  0 , n);
  
043           m  +=  n;
  
044            if  (fileSize  !=   - 1 )
  
045           {
  
046               de.percent(m  *   100   /  fileSize);
  
047           }
  
048       }
  
049       fileOut.close();
  
050       socket.close();
  
051   }

download方法是断点续传工具的核心方法。它有四个参数:

1. DownloadEvent de

用于处理下载事件的接口。

2. String url

要下载文件的URL

3. String localFN

要保存的本地文件名,可以用这个文件的大小来确定已经下载了多少个字节。

4. int cacheSize

下载数据的缓冲区。也就是一次从服务器下载多个字节。这个值不宜太小,因为,频繁地从服务器下载数据,会降低网络的利用率。一般可以将这个值设为81928K)。

为了分析下载文件的url,在008行使用了URL类,这个类在以后还会介绍,在这里只要知道使用这个类可以将使用各种协议的url(包括HTTPFTP协议)的各个部分分解,以便单独使用其中的一部分。

029行:根据文件的实际大小和已经下载的字节数(finishedSize)来判断是否文件是否已经下载完成。当文件的实际大小无法确定时,也就是fileSize返回-1时,不能下载。

033行:如果文件已经下载了一部分,并且返回的状态码仍是200(应该是206),则表明服务器并不支持断点续传。当然,这可以根据另一个字段Accept-Ranges来判断。

036行:由于本程序未考虑重定向(状态码是3xx)的情况,因此,在使用download时,不要下载返回3xx状态码的Web资源。

040 048行:开始下载文件。第046行调用DownloadEventpercent方法来返回下载进度。

   001    package download;
  
002   
  
003    import  java.io. * ;
  
004   
  
005    class  NewProgress  implements  DownloadEvent
  
006   {
  
007        private   long  oldPercent  =   - 1 ;
  
008        public   void  percent( long  n)
  
009       {
  
010            if  (n  >  oldPercent)
  
011           {
  
012               System.out.print( " [ "   +  String.valueOf(n)  +   " %] " );
  
013               oldPercent  =  n;
  
014           }
  
015       }
  
016        public   void  state(String s)
  
017       {
  
018           System.out.println(s);
  
019       }
  
020        public   void  viewHttpHeaders(String s)
  
021       {
  
022           System.out.println(s);
  
023       }
  
024   }
  
025   
  
026    public   class  Main
  
027   {
  
028        public   static   void  main(String[] args)  throws  Exception
  
029       {
  
030           
  
031           DownloadEvent progress  =   new  NewProgress();
  
032            if  (args.length  <   1 )
  
033           {
  
034               System.out.println( " 用法:java class 下载文件名 " );
  
035                return ;
  
036           }
  
037           FileInputStream fis  =   new  FileInputStream(args[ 0 ]);
  
038           BufferedReader fileReader  =   new  BufferedReader( new  InputStreamReader(
  
039                           fis));
  
040           String s  =   "" ;
  
041           String[] ss;
  
042            while  ((s  =  fileReader.readLine())  !=   null )
  
043           {
  
044                try
  
045               {
  
046                   ss  =  s.split( " [ ]+ " );
  
047                    if  (ss.length  >   2 )
  
048                   {
  
049                       System.out.println( " /r/n--------------------------- " );
  
050                       System.out.println( " 正在下载: "   +  ss[ 0 ]);
  
051                       System.out.println( " 文件保存位置: "   +  ss[ 1 ]);
  
052                       System.out.println( " 下载缓冲区大小: "   +  ss[ 2 ]);
  
053                       System.out.println( " --------------------------- " );
  054                       HttpDownload httpDownload  =   new  HttpDownload();
  
055                       httpDownload.download( new  NewProgress(), ss[ 0 ], ss[ 1 ],
  
056                                       Integer.parseInt(ss[ 2 ]));
  
057                   }
  
058               }
  
059               catch  (Exception e)
  
060               {
  
061                   System.out.println(e.getMessage());
  
062               }
  
063           }
  
064           fileReader.close();
  
065       }
  
066   }

005 024行:实现DownloadEvent接口的NewDownloadEvent类。用于在Main函数里接收相应事件传递的数据。

026 065 行:下载工具的Main方法。在这个Main方法里,打开下载资源列表文件,逐行下载相应的Web资源。

测试

假设download.txt在当前目录中,内容如下:

http://files.cnblogs.com/nokiaguy/HttpSimulator.rar HttpSimulator.rar  8192
http://files.cnblogs.com/nokiaguy/designpatterns.rar designpatterns.rar 
4096
http://files.cnblogs.com/nokiaguy/download.rar download.rar 8192

这两个URL是在本机的Web服务器(IIS)的虚拟目录中的两个文件,将它们下载在D盘根目录。

运行下面的命令:

java download.Main download.txt

    运行的结果如图1所示。


图1

目录
相关文章
|
3月前
|
存储 监控 安全
单位网络监控软件:Java 技术驱动的高效网络监管体系构建
在数字化办公时代,构建基于Java技术的单位网络监控软件至关重要。该软件能精准监管单位网络活动,保障信息安全,提升工作效率。通过网络流量监测、访问控制及连接状态监控等模块,实现高效网络监管,确保网络稳定、安全、高效运行。
87 11
|
27天前
|
Linux 网络安全 Docker
尼恩一键开发环境: vagrant+java+springcloud+redis+zookeeper镜像下载(&制作详解)
尼恩提供了一系列文章,旨在帮助开发者轻松搭建一键开发环境,涵盖Java分布式、高并发场景下的多种技术组件安装与配置。内容包括但不限于Windows和CentOS虚拟机的安装与排坑指南、MySQL、Kafka、Redis、Zookeeper等关键组件在Linux环境下的部署教程,并附带详细的视频指导。此外,还特别介绍了Vagrant这一虚拟环境部署工具,
尼恩一键开发环境: vagrant+java+springcloud+redis+zookeeper镜像下载(&制作详解)
|
13天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
134 60
【Java并发】【线程池】带你从0-1入门线程池
|
1天前
|
存储 监控 Java
《从头开始学java,一天一个知识点》之:数组入门:一维数组的定义与遍历
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。明日预告:《多维数组与常见操作》。 通过实例讲解数组的核心认知、趣味场景应用、企业级开发规范及优化技巧,帮助你快速掌握Java数组的精髓。
42 23
|
2天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
46 23
|
2月前
|
自然语言处理 Java
Java中的字符集编码入门-增补字符(转载)
本文探讨Java对Unicode的支持及其发展历程。文章详细解析了Unicode字符集的结构,包括基本多语言面(BMP)和增补字符的表示方法,以及UTF-16编码中surrogate pair的使用。同时介绍了代码点和代码单元的概念,并解释了UTF-8的编码规则及其兼容性。
117 60
|
2月前
|
Linux 网络性能优化 网络安全
Linux(openwrt)下iptables+tc工具实现网络流量限速控制(QoS)
通过以上步骤,您可以在Linux(OpenWrt)系统中使用iptables和tc工具实现网络流量限速控制(QoS)。这种方法灵活且功能强大,可以帮助管理员有效管理网络带宽,确保关键业务的网络性能。希望本文能够为您提供有价值的参考。
140 28
|
21小时前
|
缓存 安全 Java
【Java并发】【synchronized】适合初学者体质入门的synchronized
欢迎来到我的Java线程同步入门指南!我不是外包员工,梦想是写高端CRUD。2025年我正在沉淀中,博客更新速度加快,欢迎点赞、收藏、关注。 本文介绍Java中的`synchronized`关键字,适合初学者。`synchronized`用于确保多个线程访问共享资源时不会发生冲突,避免竞态条件、保证内存可见性、防止原子性破坏及协调多线程有序访问。
34 7
【Java并发】【synchronized】适合初学者体质入门的synchronized
|
1月前
|
安全 网络协议 Java
Java网络编程封装
Java网络编程封装原理旨在隐藏底层通信细节,提供简洁、安全的高层接口。通过简化开发、提高安全性和增强可维护性,封装使开发者能更高效地进行网络应用开发。常见的封装层次包括套接字层(如Socket和ServerSocket类),以及更高层次的HTTP请求封装(如RestTemplate)。示例代码展示了如何使用RestTemplate简化HTTP请求的发送与处理,确保代码清晰易维护。
|
2月前
|
数据采集 人工智能 自然语言处理
FireCrawl:开源 AI 网络爬虫工具,自动爬取网站及子页面内容,预处理为结构化数据
FireCrawl 是一款开源的 AI 网络爬虫工具,专为处理动态网页内容、自动爬取网站及子页面而设计,支持多种数据提取和输出格式。
505 19
FireCrawl:开源 AI 网络爬虫工具,自动爬取网站及子页面内容,预处理为结构化数据

热门文章

最新文章