Java网络编程:实现HTTP断点续传下载工具(附源代码)

本文为原创,如需转载,请注明作者和出处,谢谢!

源代码下载:download.rar

    在前面的文章曾讨论了HTTP消息头的三个和断点继传有关的字段。一个是请求消息的字段Range,另两个是响应消息字段Accept-Ranges和Content-Range。其中Accept-Ranges用来断定Web服务器是否支持断点继传功能。在这里为了演示如何实现断点继传功能,假设Web服务器支持这个功能;因此,我们只使用Range和Content-Range来完成一个断点继传工具的开发。

l         要实现一个什么样的断点续传工具?

这个断点续工具是一个单线程的下载工具。它通过参数传入一个文本文件。这个文件的格式如下:

http://www.ishare.cc/d/1174254-2/106.jpg  d:/ok1.jpg  8192
http://www.ishare.cc/d/1174292-2/156.jpg   d:/ok2.jpg   12345
http://www.ishare.cc/d/1174277-2/147.jpg   d:/ok3.jpg  3456

这个文本文件的每一行是一个下载项,这个下载项分为三部分:

  • 要下载的Web资源的URL。
  • 要保存的本地文件名。
  • 下载的缓冲区大小(单位是字节)。

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

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-Range:bytes 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类的框架代码,其中的方法并未直正实现。我们可以从中看出第012、014、021和023行就是上述的四个主要的方法。在016和018行的addHeaderToMap和analyzeFirstLine方法将在analyzeHttpHeader方法中用到。而025和027行的getHeader和getIntHeader方法在getFileSize和download方法都会用到。上述的八个方法的实现都会在后面给出。

  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位置开始下载。如果startPos为0,则不生成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  }

 

    这两个方法将会在getFileSize和download中被调用。它们的功能是从响应消息中根据字段字得到相应的字段值。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

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

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

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

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

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

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

  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

时间: 2024-11-09 03:45:55

Java网络编程:实现HTTP断点续传下载工具(附源代码)的相关文章

Java网络编程从入门到精通

Hibernate从入门到精通(十一)多对多双向关联映射 Hibernate从入门到精通(十)多对多单向关联映射 Hibernate从入门到精通(九)一对多双向关联映射 Hibernate从入门到精通(八)一对多单向关联映射 Hibernate从入门到精通(七)多对一单向关联映射 Hibernate从入门到精通(六)一对一双向关联映射 Hibernate从入门到精通(五)一对一单向关联映射 Hibernate从入门到精通(四)基本映射 Hibernate从入门到精通(三)Hibernate配置文

java网络编程HttpsURLConnection

问题描述 java网络编程HttpsURLConnection 我写了一个多线程下载测试程序 照着教程做的 但运行提示错误这个语句HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();提示错误 sun.net.www.protocol.http.HttpURLConnection cannot be cast to javax.net.ssl.HttpsURLConnection其中rul 为 URL url =

Java网络编程(一)

关于JAVA网络编程的技术非常繁多,如:SOCKET.RMI.EJB.WEBSERVICE.MQ.中间数据等等方法,但是万变都是源于基础中通信原理,有些是轻量级的.有重量级的:有实时调用.有异步调用:这么多的技术可以说什么都可以用,关键在什么场合用什么最适合你,这些技术主要用于多个子系统之间相互通信的方法,如一个大型的软件应用分多个子系统,它们可能由不同的厂商来完成,这些子系统最终需要整合为一个系统,那么整合的基础就是交互,要么是通过数据交互,要么是通过接口调用,要么通过中间数据等等.本文从最基

Java网络编程从入门到精通(34)

Java网络编程从入门到精通(34):读写缓冲区中的数据---使用get和put方法按顺序读写单个数据 对于缓冲区来说,最重要的操作就是读写操作.缓冲区提供了两种方法来读写缓冲区中的数据:get.put方法和array方法.而get.put方法可以有三种读写数据的方式:按顺序读写单个数据.在指定位置读写单个数据和读写数据块.除了上述的几种读写数据的方法外,CharBuffer类还提供了用于专门写字符串的put和append方法.在本文及后面的文章中将分别介绍这些读写缓冲区的方法. 虽然使用all

java网络编程的小疑惑

问题描述 java网络编程的小疑惑 在服务器的循环语句中,这段代码只能进行服务器的一次响应 try{ while(true){ **serversocket=new ServerSocket(6000);** socket=serversocket.accept(); Thread workthread=new Thread(new ClientRun(socket)); workthread.start(); } } catch(IOException e){ } finally{ } 当将s

JAVA网络编程服务器多线程接受套接字,如何能使服务器的静态常量,与客户端的数据进行同步?

问题描述 JAVA网络编程服务器多线程接受套接字,如何能使服务器的静态常量,与客户端的数据进行同步? 服务器Server客户端CLientServer静态常量num创建服务器,ServerSocket的端口号为8000,连接套接字.每创建一个客户端,客户端就创建一个Socket,端口号为8000,与服务器进行连接,与此同时,客户端新建ServerSocket,端口号为3000+Server.num,连接套接字.服务器与客户端连接后,服务器新建子线程Handler.子线程Handler,新建Ser

遇到一个棘手的问题,需要java网络编程大神帮忙解答下~

问题描述 遇到一个棘手的问题,需要java网络编程大神帮忙解答下~ 问题是这样的: 我把MINA核心的非阻塞轮训方式的的代码用JDK7的AIO异步IO替换了,现在已经包装完成,测试的时候遇到两个问题: 测试的是这样的,服务端启动后20秒后释放所有资源关闭,客户端启动15秒后释放所有资源关闭,大部分情况下测试都是正常的,但是偶尔会出现客户端服务端都关闭后,再次启动服务的过程后,要么服务端抛出AsynchronousCloseException,客户端抛出远程主机强迫关闭一个现有连接:要么是客户端的

java-初学Java网络编程socket,为何我的服务器打不开?

问题描述 初学Java网络编程socket,为何我的服务器打不开? 初学Java网络编程socket,写了个服务器,但是打不开,求指导.代码如下 import java.io.*; import java.net.*; import java.util.*; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scen

服务器-JAVA网络编程问题请大神指导

问题描述 JAVA网络编程问题请大神指导 面试被问到了,请教大神:同一服务器相同Server是否可以共用一个端口?同一服务器不同Server是否可以共用一个端口? 解决方案 一个TOMCAT 可以有多个项目 占一个端口, 多个TOMCAT 需要各自使用不同端口.一个端口只能被一个服务使用. 解决方案二: linux内核中有端口reuse技术,这样可以多个应用绑定到同一个端口,然后内核来调度把连接转发给某个应用.nginx中worker有采用这个