Java实现断点续传(Http)

转自 http://www.ibm.com/developerworks/cn/java/joy-down/index.html


实现思路:

(1) 创建一个下载任务类SiteInfoBean(用于记录文件保存的目录,文件下载URL,文件名)

(2) 创建一个FileAccessI 用于随机访问文件,向文件写入内容。

(3) 创建一个下载任务执行线程SiteFileFetch,此线程主要做如下工作

   (a) 接受指定 下载子线程个数

  (b) 首先判断下载的文件是否存在,

           如果下载的文件已经存在则继续判断它对应的xxx.info文件是否存在,

                           如果info文件存在则认为是上次下载没有完成。此时读取info文件中的下载信息,分配下载区段。

                           如果info文件不存在,则认为已经下载完了,重新命名一个文件xxx(1),如果重新命名的文件也存在则继续累加命名xxx(2)...

  (c)  判断临时文件xxx.info 文件(用于保存文件下载信息:下载子线程个数,子线程1开始位置,子线程1结束位置,子线程2开始位置,子线程2结束位置,...)

        是否存在,存在则说明是上次没有下载完成,不存在则创建此临时info文件记录下载信息。

  (d) 获取文件总长度,根据子线程个数将长度划分若干等分

  (e) 创建若干子线程,为每个子线程分配下载区段(文件开始位置,文件结束位置),启动子线程

  (f) 每隔500ms从各个子线程获取当前下载的进度位置,然后覆盖保存在xxx.info文件中。

  (g) 每隔500ms同时判断是否存在没有下载完分配区段内容的子线程,如果存在则认为整个下载任务是没有完成的,如果不存在则认为总的已经下载完成。

  (h) 每隔500ms同时统计出总的下载进度,当进度达到100%的时候,删除临时文件xx.info文件


(4) 下载子线程FileSplitterFetch,此线程主要做的就是根据任务线程SiteFileFetch划分的下载区段进行下载

       此子线程主要通过FileAccessI 向同一个文件写入内容(多个线程向同一个文件写入内容,写入的内容都是事先指定的区段)


断点续传的原理很简单,就是在Http的请求上和一般的下载有所不同而已。 

打个比方,浏览器请求服务器上的一个文时,所发出的请求如下: 
假设服务器域名为wwww.sjtu.edu.cn,文件名为down.zip。 
GET /down.zip HTTP/1.1 
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- 
excel, application/msword, application/vnd.ms-powerpoint, */* 
Accept-Language: zh-cn 
Accept-Encoding: gzip, deflate 
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) 
Connection: Keep-Alive

服务器收到请求后,按要求寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:

200 
Content-Length=106786028 
Accept-Ranges=bytes 
Date=Mon, 30 Apr 2001 12:56:11 GMT 
ETag=W/"02ca57e173c11:95b" 
Content-Type=application/octet-stream 
Server=Microsoft-IIS/5.0 
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT

所谓断点续传,也就是要从文件已经下载的地方开始继续下载。所以在客户端浏览器传给 Web服务器的时候要多加一条信息--从哪里开始。 
下面是用自己编的一个"浏览器"来传递请求信息给Web服务器,要求从2000070字节开始。 
GET /down.zip HTTP/1.0 
User-Agent: NetFox 
RANGE: bytes=2000070- 
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2

仔细看一下就会发现多了一行RANGE: bytes=2000070- 
这一行的意思就是告诉服务器down.zip这个文件从2000070字节开始传,前面的字节不用传了。 
服务器收到这个请求以后,返回的信息如下: 
206 
Content-Length=106786028 
Content-Range=bytes 2000070-106786027/106786028 
Date=Mon, 30 Apr 2001 12:55:20 GMT 
ETag=W/"02ca57e173c11:95b" 
Content-Type=application/octet-stream 
Server=Microsoft-IIS/5.0 
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT

和前面服务器返回的信息比较一下,就会发现增加了一行: 
Content-Range=bytes 2000070-106786027/106786028 
返回的代码也改为206了,而不再是200了。

知道了以上原理,就可以进行断点续传的编程了。

Java实现断点续传的关键几点

  1. (1)用什么方法实现提交RANGE: bytes=2000070-。 
    当然用最原始的Socket是肯定能完成的,不过那样太费事了,其实Java的net包中提供了这种功能。代码如下: 

    URL url = new URL("http://www.sjtu.edu.cn/down.zip"); 
    HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection(); 

    //设置User-Agent 
    httpConnection.setRequestProperty("User-Agent","NetFox"); 
    //设置断点续传的开始位置 
    httpConnection.setRequestProperty("RANGE","bytes=2000070"); 
    //获得输入流 
    InputStream input = httpConnection.getInputStream(); 

    从输入流中取出的字节流就是down.zip文件从2000070开始的字节流。 大家看,其实断点续传用Java实现起来还是很简单的吧。 接下来要做的事就是怎么保存获得的流到文件中去了。

  2. 保存文件采用的方法。 
    我采用的是IO包中的RandAccessFile类。 
    操作相当简单,假设从2000070处开始保存文件,代码如下: 
    RandomAccess oSavedFile = new RandomAccessFile("down.zip","rw"); 
    long nPos = 2000070; 
    //定位文件指针到nPos位置 
    oSavedFile.seek(nPos); 
    byte[] b = new byte[1024]; 
    int nRead; 
    //从输入流中读入字节流,然后写到文件中 
    while((nRead=input.read(b,0,1024)) > 0) 

    oSavedFile.write(b,0,nRead); 

怎么样,也很简单吧。 接下来要做的就是整合成一个完整的程序了。包括一系列的线程控制等等。

具体源码如下:

SiteInfoBean.java

package core;

/**
 * 要下载的文件信息(下载任务)<br>
 * 如文件保存的目录,名字,抓取文件的 URL 等
 * 
 * @author Lue
 * @since 2015-05-07
 */
public class SiteInfoBean
{
	/**
	 * 文件URL资源
	 */
	private String sSiteURL; // Site's URL
	
	/**
	 * 文件保存的路径(不包含文件名)
	 */
	private String sFilePath; // Saved File's Path
	
	/**
	 * 文件名
	 */
	private String sFileName; // Saved File's Name
	
	/** 下载线程个数 */
	private int nSplitter; 

	public SiteInfoBean()
	{
		// default value of nSplitter is 5
		this("", "", "", 5);
	}

	/**
	 * 
	 * @param sURL 文件资源URL
	 * @param sPath 文件保存的路径(不包含文件名)
	 * @param sName 文件名
	 * @param nSpiltter 下载线程个数
	 */
	public SiteInfoBean(String sURL, String sPath, String sName, int nSpiltter)
	{
		sSiteURL = sURL;
		sFilePath = sPath;
		sFileName = sName;
		this.nSplitter = nSpiltter;
	}

	public String getSSiteURL()
	{
		return sSiteURL;
	}

	public void setSSiteURL(String value)
	{
		sSiteURL = value;
	}

	/**
	 * 获取文件保存的路径
	 * @return
	 */
	public String getSFilePath()
	{
		return sFilePath;
	}

	public void setSFilePath(String value)
	{
		sFilePath = value;
	}

	/**
	 * 获取文件名
	 * @return
	 */
	public String getSFileName()
	{
		return sFileName;
	}

	public void setSFileName(String value)
	{
		sFileName = value;
	}

	/**
	 * 分割成的子文件个数
	 * @return
	 */
	public int getNSplitter()
	{
		return nSplitter;
	}

	public void setNSplitter(int nCount)
	{
		nSplitter = nCount;
	}
}

FileAccessI.java

package core;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;

/**
 * 负责文件的存储
 * @author Lue
 * @since 2015-05-07
 */
public class FileAccessI implements Serializable
{
	RandomAccessFile oSavedFile;
	long nPos;

	public FileAccessI() throws IOException
	{
		this("", 0);
	}

	public FileAccessI(String sName, long nPos) throws IOException
	{
		oSavedFile = new RandomAccessFile(sName, "rw");
		this.nPos = nPos;
		oSavedFile.seek(nPos);
	}

	public synchronized int write(byte[] b, int nStart, int nLen)
	{
		int n = -1;
		try
		{
			oSavedFile.write(b, nStart, nLen);
			n = nLen;
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		return n;
	}
}

SiteFileFetch.java

package core;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 文件下载线程(负责整个文件的抓取,控制内部线程)
 * 
 * @author Lue
 * 
 */
public class SiteFileFetch extends Thread
{
	/** 文件信息 Bean */
	SiteInfoBean siteInfoBean = null;

	/** 文件指针开始位置数组 */
	long[] nStartPos;

	/** 文件指针结束位置数组 */
	long[] nEndPos;

	/** 子下载线程数组 */
	FileSplitterFetch[] fileSplitterFetch;

	/** 文件长度 (字节byte) */
	long nFileLength;

	/** 是否第一次取文件 */
	boolean bFirst = true;

	/** 停止标志 */
	boolean bStop = false;

	/**
	 * 临时文件用于记录文件下载信息(下载线程个数,每个下载线程的当前文件开始指针,文件结束指针)
	 */
	private File tmpFile;

	// 输出到文件的输出流
	DataOutputStream output;
	
	private boolean isLoading;

	public SiteFileFetch(SiteInfoBean bean) throws IOException
	{
		siteInfoBean = bean;

		isLoading = true;

		adjustFileNameForDuplicate(bean);

		tmpFile = new File(bean.getSFilePath() + File.separator
				+ bean.getSFileName() + ".info");

		if (tmpFile.exists())
		{// 临时文件存在,则认为不是第一次下载,之前有下载过,但是没下载完成(断点续传下载)
			bFirst = false;

			read_nPos();
		}
		else
		{ // 文件指针开始位置数组个数取决于文件被分割成子文件的个数
			nStartPos = new long[bean.getNSplitter()];
			nEndPos = new long[bean.getNSplitter()];
		}
	}

	

	/**
	 * 文件名重复则重新命名
	 * 
	 * @param bean
	 */
	private void adjustFileNameForDuplicate(SiteInfoBean bean)
	{
		if (bean != null && bean.getSFileName() != null
				&& bean.getSFilePath() != null)
		{
			File file = new File(bean.getSFilePath() + File.separator
					+ bean.getSFileName());

			int lastDotIdx = bean.getSFileName().lastIndexOf(".");

			String prefix = bean.getSFileName().substring(0, lastDotIdx);

			String suffix = bean.getSFileName().substring(lastDotIdx + 1);

			int count = 1;

			while (file.exists())
			{
				
				File loadInfoFile = new File(bean.getSFilePath() + File.separator
						+ bean.getSFileName() + ".info");

				if (loadInfoFile.exists())
				{//如果临时文件存在,则认为是上次没有下载完成的,这是不用重新命名
					break;
				}

				String newPrefix = prefix + "(" + count + ")";

				bean.setSFileName(newPrefix + "." + suffix);

				file = new File(bean.getSFilePath() + File.separator
						+ bean.getSFileName());
				count++;
			}
		}
	}

	/**
	 * (1) 获得文件长度 <br>
	 * (2) 分割文件<br>
	 * (3) 创建文件下载线程 FileSplitterFetch<br>
	 * (4) 启动文件下载线程  FileSplitterFetch 线程<br> 
	 * (5) 等待子线程返回
	 */
	public void run()
	{
		try
		{
			nFileLength = getFileSize();
			
			if (nFileLength == -1)
			{
				isLoading = false;
				bStop = true;
				System.err.println("File Length is not known!");
				return;
			}
			else if (nFileLength == -2)
			{
				isLoading = false;
				bStop = true;
				System.err.println("File is not access!");
				
				return;
			}
			
			if (bFirst)
			{// 如果是第一次下载
				// 分配文件指针数组的起始结束位置
				for (int i = 0; i < nStartPos.length; i++)
				{
					nStartPos[i] = (long) (i * (nFileLength / nStartPos.length));
				}

				for (int i = 0; i < nEndPos.length - 1; i++)
				{
					nEndPos[i] = nStartPos[i + 1];
				}

				nEndPos[nEndPos.length - 1] = nFileLength;
			}

			// 创建 启动子线程数组
			fileSplitterFetch = new FileSplitterFetch[nStartPos.length];

			for (int i = 0; i < nStartPos.length; i++)
			{
				fileSplitterFetch[i] = new FileSplitterFetch(
						siteInfoBean.getSSiteURL(), siteInfoBean.getSFilePath()
								+ File.separator + siteInfoBean.getSFileName(),
						nStartPos[i], nEndPos[i], i);

				Utility.log("Thread " + i + " , nStartPos = " + nStartPos[i]
						+ ", nEndPos = " + nEndPos[i]);

				//启动子线程
				fileSplitterFetch[i].start();
			}

			boolean breakWhile = false;

			while (!bStop)
			{// 如果下载没有停止,则每隔500ms去保存一次文件指针信息到临时文件

				write_nPos();
				
				gatherLoadProgress();

				Utility.sleep(500);

				breakWhile = true;

				for (int i = 0; i < nStartPos.length; i++)
				{
					if (!fileSplitterFetch[i].bDownOver)
					{// 只要其中有一个没下载完成,
						breakWhile = false;
						break;
					}
				}

				if (breakWhile)
				{
					break;
				}
			}

			gatherLoadProgress();
			
			System.err.println("文件下载结束!");

			isLoading = false;
		}
		catch (Exception e)
		{
			isLoading = false;
			e.printStackTrace();
		}
	}

	/**
	 * 获得文件长度
	 * 
	 * @return
	 */
	public long getFileSize()
	{
		int nFileLength = -1;

		try
		{
			URL url = new URL(siteInfoBean.getSSiteURL());

			HttpURLConnection httpConnection = (HttpURLConnection) url
					.openConnection();
			httpConnection.setRequestProperty("User-Agent", "NetFox");

			int responseCode = httpConnection.getResponseCode();

			if (responseCode >= 400)
			{
				processErrorCode(responseCode);
				return -2; // -2 represent access is error
			}

			String sHeader;

			for (int i = 1;; i++)
			{
				sHeader = httpConnection.getHeaderFieldKey(i);
				if (sHeader != null)
				{
					if (sHeader.equals("Content-Length"))
					{
						nFileLength = Integer.parseInt(httpConnection
								.getHeaderField(sHeader));
						break;
					}
				}
				else
				{
					break;
				}
			}
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		Utility.log(nFileLength);
		return nFileLength;
	}

	/**
	 * 收集下载进度
	 */
	private void gatherLoadProgress()
	{
		// 剩余的字节数
		long laveLength = 0;

		for (int i = 0; i < nStartPos.length; i++)
		{
			laveLength += (fileSplitterFetch[i].nEndPos - fileSplitterFetch[i].nStartPos);
		}

		int percent = (int) ((nFileLength - laveLength) * 100 / nFileLength);

		if(percent == 100)
		{
			if(tmpFile != null && tmpFile.exists())
			{
				//全部下载完成,则删除临时文件,
				tmpFile.delete();
			}
			
			isLoading = false;
			
			bStop = true;
		}

		System.out.println("当前下载进度 " + percent + "%");
	}

	/**
	 * 保存下载信息(文件指针位置)
	 */
	private void write_nPos()
	{
		try
		{
			output = new DataOutputStream(new FileOutputStream(tmpFile));
			output.writeInt(nStartPos.length);

			for (int i = 0; i < nStartPos.length; i++)
			{
				output.writeLong(fileSplitterFetch[i].nStartPos);
				output.writeLong(fileSplitterFetch[i].nEndPos);
			}

			output.close();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	/**
	 * 读取之前下载保存下来的文件指针位置
	 */
	private void read_nPos()
	{
		try
		{
			DataInputStream input = new DataInputStream(new FileInputStream(
					tmpFile));

			// 个数(这里记录了文件被划分成几个子文件(子任务))
			int nCount = input.readInt();

			nStartPos = new long[nCount];
			nEndPos = new long[nCount];

			for (int i = 0; i < nStartPos.length; i++)
			{
				nStartPos[i] = input.readLong();
				nEndPos[i] = input.readLong();
			}

			input.close();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	private void processErrorCode(int nErrorCode)
	{
		System.err.println("Error Code : " + nErrorCode);
	}

	public boolean isLoading()
	{
		return isLoading;
	}

	/**
	 * 停止文件下载
	 */
	public void siteStop()
	{
		bStop = true;

		isLoading = false;
		
		for (int i = 0; i < nStartPos.length; i++)
		{
			fileSplitterFetch[i].splitterStop();
		}
	}
	
	public interface LoadProgressListener
	{
		void onstartLoad();
		void onProgressUpdate(int percent);
		void onCompleteLoad();
		void onStopLoad();
	}
}

FileSplitterFetch.java

package core;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 文件下载子线程(负责文件的其中一部分内容下载)
 * @author Lue
 * @since 2015-05-07
 */
public class FileSplitterFetch extends Thread
{
	String sURL; // File URL
	long nStartPos; // File Snippet Start Position
	long nEndPos; // File Snippet End Position
	int nThreadID; // Thread's ID
	boolean bDownOver = false; // Downing is over
	boolean bStop = false; // Stop identical
	FileAccessI fileAccessI = null; // File Access interface

	/**
	 * 
	 * @param sURL 文件资源URL
	 * @param sName 要保存的文件名(完整路径,绝对路径)
	 * @param nStart 文件指针开始位置
	 * @param nEnd 文件指针结束位置
	 * @param id 线程ID
	 * @throws IOException
	 */
	public FileSplitterFetch(String sURL, String sName, long nStart, long nEnd,
			int id) throws IOException
	{
		this.sURL = sURL;
		this.nStartPos = nStart;
		this.nEndPos = nEnd;
		nThreadID = id;
		fileAccessI = new FileAccessI(sName, nStartPos);
	}

	public void run()
	{
		while (nStartPos < nEndPos && !bStop)
		{
			try
			{
				URL url = new URL(sURL);
				
				HttpURLConnection httpConnection = (HttpURLConnection) url
						.openConnection();
				httpConnection.setRequestProperty("User-Agent", "NetFox");
				
				String sProperty = "bytes=" + nStartPos + "-";
				httpConnection.setRequestProperty("RANGE", sProperty);
				
				Utility.log(sProperty);
				
				InputStream input = httpConnection.getInputStream();
				
				byte[] b = new byte[1024];
				
				int nRead;
				
				while ((nRead = input.read(b, 0, 1024)) > 0
						&& nStartPos < nEndPos && !bStop)
				{//注意这里不用再判断 nRead+nStartPos<nEndPos,只需要 nStartPos<nEndPos就可以,
				 //因为是前面几个下载线程读取的内容超出了nEndPos,也会被它后面的子线程读取内容覆盖掉,
				 //最后一个子下载子线程最后读取到的字节个数小于1024的,所以总的结束位置不超过就可以
					nStartPos += fileAccessI.write(b, 0, nRead);
				}
				
				Utility.log("Thread " + nThreadID + " is over!"+",nStartPos="+nStartPos+",nEndPos="+nEndPos);
				
				bDownOver = true;
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
		}
	}

	// 打印回应的头信息
	public void logResponseHead(HttpURLConnection con)
	{
		for (int i = 1;; i++)
		{
			String header = con.getHeaderFieldKey(i);
			if (header != null)
				// responseHeaders.put(header,httpConnection.getHeaderField(header));
				Utility.log(header + " : " + con.getHeaderField(header));
			else
				break;
		}
	}

	public void splitterStop()
	{
		bStop = true;
	}
}

Utility.java

package core;

/**
 * 工具类
 * @author Lue
 * @since 2015-05-07
 */
public class Utility
{
	public Utility()
	{
	}

	public static void sleep(int nSecond)
	{
		try
		{
			Thread.sleep(nSecond);
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	public static void log(String sMsg)
	{
		System.err.println(sMsg);
	}

	public static void log(int sMsg)
	{
		System.err.println(sMsg);
	}
}


验证下载的代码

TestMethod.java

import core.SiteFileFetch;
import core.SiteInfoBean;

public class TestMethod
{
	public TestMethod()
	{
		try
		{
			SiteInfoBean bean = new SiteInfoBean(
					"http://banzou.cdn.aliyun.com/apk/changba_6093.apk",
					"E:\\工作任务以及日报", "changba_6093.apk", 3);

			SiteFileFetch fileFetch = new SiteFileFetch(bean);
			fileFetch.start();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	public static void main(String[] args)
	{
		new TestMethod();
	}
}




  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值