java 多线程下载 开源_android 下载大文件开源包(多线程 断点续传)

端点续传其实是一个很简单的原理。需要服务器端支持才行。客户端实现很简单,就算基本的读写文件。

HTTP头示例:

# The first 500 bytes:

Content-Range: bytes 0-499/1234

# The second 500 bytes:

Content-Range: bytes 500-999/1234

# All except for the first 500 bytes:

Content-Range: bytes 500-1233/1234

# The last 500 bytes:

Content-Range: bytes 734-1233/1234

下载的时候查看HTTP头,如果HTTP头里面包含了Range等就提取出来,计算出下一块读取的位置和长度,然后用Content-Range头返回。

客户端直接读取和保存文件

读取的时候先跳过已经下载的长度。

if (!downloadPath.exists())

downloadPath.mkdirs();

if (outputFileCache.exists())

{

downloadedSize = outputFileCache.length();

connection.setAllowUserInteraction(true);

connection.setRequestProperty("Range", "bytes=" + downloadedSize + "-");

connection.setConnectTimeout(14000);

connection.connect();

input = new BufferedInputStream(connection.getInputStream());

output = new FileOutputStream(outputFileCache, true);

input.skip(downloadedSize); //Skip downloaded size

}

else

{

connection.setConnectTimeout(14000);

connection.connect();

input = new BufferedInputStream(url.openStream());

output = new FileOutputStream(outputFileCache);

}

fileLength = connection.getContentLength();

byte data[] = new byte[1024];

int count = 0;

int __progress = 0;

long total = downloadedSize;

while ((count = input.read(data)) != -1 && !this.isInterrupted())

{

total += count;

output.write(data, 0, count);

__progress = (int) (total * 100 / fileLength);

}

output.flush();

output.close();

input.close();

服务器端使用Servlet支持端点续传下载:

/*

* net/balusc/webapp/FileServlet.java

*

* Copyright (C) 2009 BalusC

*

* This program is free software: you can redistribute it and/or modify it under the terms of the

* GNU Lesser General Public License as published by the Free Software Foundation, either version 3

* of the License, or (at your option) any later version.

*

* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without

* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

* Lesser General Public License for more details.

*

* You should have received a copy of the GNU Lesser General Public License along with this library.

* If not, see .

*/

package net.balusc.webapp;

import java.io.Closeable;

import java.io.File;

import java.io.IOException;

import java.io.OutputStream;

import java.io.RandomAccessFile;

import java.net.URLDecoder;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.List;

import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;

import javax.servlet.ServletOutputStream;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

/**

* A file servlet supporting resume of downloads and client-side caching and GZIP of text content.

* This servlet can also be used for images, client-side caching would become more efficient.

* This servlet can also be used for text files, GZIP would decrease network bandwidth.

*

* @author BalusC

* @link http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html

*/

public class FileServlet extends HttpServlet {

// Constants ----------------------------------------------------------------------------------

private static final int DEFAULT_BUFFER_SIZE = 10240; // ..bytes = 10KB.

private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.

private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

// Properties ---------------------------------------------------------------------------------

private String basePath;

// Actions ------------------------------------------------------------------------------------

/**

* Initialize the servlet.

* @see HttpServlet#init().

*/

public void init() throws ServletException {

// Get base path (path to get all resources from) as init parameter.

this.basePath = getServletContext().getRealPath(getInitParameter("basePath"));

// Validate base path.

if (this.basePath == null) {

throw new ServletException("FileServlet init param 'basePath' is required.");

} else {

File path = new File(this.basePath);

if (!path.exists()) {

throw new ServletException("FileServlet init param 'basePath' value '"

+ this.basePath + "' does actually not exist in file system.");

} else if (!path.isDirectory()) {

throw new ServletException("FileServlet init param 'basePath' value '"

+ this.basePath + "' is actually not a directory in file system.");

} else if (!path.canRead()) {

throw new ServletException("FileServlet init param 'basePath' value '"

+ this.basePath + "' is actually not readable in file system.");

}

}

}

/**

* Process HEAD request. This returns the same headers as GET request, but without content.

* @see HttpServlet#doHead(HttpServletRequest, HttpServletResponse).

*/

protected void doHead(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException

{

// Process request without content.

processRequest(request, response, false);

}

/**

* Process GET request.

* @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse).

*/

protected void doGet(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException

{

// Process request with content.

processRequest(request, response, true);

}

/**

* Process the actual request.

* @param request The request to be processed.

* @param response The response to be created.

* @param content Whether the request body should be written (GET) or not (HEAD).

* @throws IOException If something fails at I/O level.

*/

private void processRequest

(HttpServletRequest request, HttpServletResponse response, boolean content)

throws IOException

{

// Validate the requested file ------------------------------------------------------------

// Get requested file by path info.

String requestedFile = request.getPathInfo();

// Check if file is actually supplied to the request URL.

if (requestedFile == null) {

// Do your thing if the file is not supplied to the request URL.

// Throw an exception, or send 404, or show default/warning page, or just ignore it.

response.sendError(HttpServletResponse.SC_NOT_FOUND);

return;

}

// URL-decode the file name (might contain spaces and on) and prepare file object.

File file = new File(basePath, URLDecoder.decode(requestedFile, "UTF-8"));

// Check if file actually exists in filesystem.

if (!file.exists()) {

// Do your thing if the file appears to be non-existing.

// Throw an exception, or send 404, or show default/warning page, or just ignore it.

response.sendError(HttpServletResponse.SC_NOT_FOUND);

return;

}

// Prepare some variables. The ETag is an unique identifier of the file.

String fileName = file.getName();

long length = file.length();

long lastModified = file.lastModified();

String eTag = fileName + "_" + length + "_" + lastModified;

long expires = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;

// Validate request headers for caching ---------------------------------------------------

// If-None-Match header should contain "*" or ETag. If so, then return 304.

String ifNoneMatch = request.getHeader("If-None-Match");

if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {

response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);

response.setHeader("ETag", eTag); // Required in 304.

response.setDateHeader("Expires", expires); // Postpone cache with 1 week.

return;

}

// If-Modified-Since header should be greater than LastModified. If so, then return 304.

// This header is ignored if any If-None-Match header is specified.

long ifModifiedSince = request.getDateHeader("If-Modified-Since");

if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {

response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);

response.setHeader("ETag", eTag); // Required in 304.

response.setDateHeader("Expires", expires); // Postpone cache with 1 week.

return;

}

// Validate request headers for resume ----------------------------------------------------

// If-Match header should contain "*" or ETag. If not, then return 412.

String ifMatch = request.getHeader("If-Match");

if (ifMatch != null && !matches(ifMatch, eTag)) {

response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);

return;

}

// If-Unmodified-Since header should be greater than LastModified. If not, then return 412.

long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");

if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {

response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);

return;

}

// Validate and process range -------------------------------------------------------------

// Prepare some variables. The full Range represents the complete file.

Range full = new Range(0, length - 1, length);

List ranges = new ArrayList();

// Validate and process Range and If-Range headers.

String range = request.getHeader("Range");

if (range != null) {

// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.

if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {

response.setHeader("Content-Range", "bytes */" + length); // Required in 416.

response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);

return;

}

// If-Range header should either match ETag or be greater then LastModified. If not,

// then return full file.

String ifRange = request.getHeader("If-Range");

if (ifRange != null && !ifRange.equals(eTag)) {

try {

long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.

if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {

ranges.add(full);

}

} catch (IllegalArgumentException ignore) {

ranges.add(full);

}

}

// If any valid If-Range header, then process each part of byte range.

if (ranges.isEmpty()) {

for (String part : range.substring(6).split(",")) {

// Assuming a file with length of 100, the following examples returns bytes at:

// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).

long start = sublong(part, 0, part.indexOf("-"));

long end = sublong(part, part.indexOf("-") + 1, part.length());

if (start == -1) {

start = length - end;

end = length - 1;

} else if (end == -1 || end > length - 1) {

end = length - 1;

}

// Check if Range is syntactically valid. If not, then return 416.

if (start > end) {

response.setHeader("Content-Range", "bytes */" + length); // Required in 416.

response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);

return;

}

// Add range.

ranges.add(new Range(start, end, length));

}

}

}

// Prepare and initialize response --------------------------------------------------------

// Get content type by file name and set default GZIP support and content disposition.

String contentType = getServletContext().getMimeType(fileName);

boolean acceptsGzip = false;

String disposition = "inline";

// If content type is unknown, then set the default value.

// For all content types, see: http://www.w3schools.com/media/media_mimeref.asp

// To add new content types, add new mime-mapping entry in web.xml.

if (contentType == null) {

contentType = "application/octet-stream";

}

// If content type is text, then determine whether GZIP content encoding is supported by

// the browser and expand content type with the one and right character encoding.

if (contentType.startsWith("text")) {

String acceptEncoding = request.getHeader("Accept-Encoding");

acceptsGzip = acceptEncoding != null && accepts(acceptEncoding, "gzip");

contentType += ";charset=UTF-8";

}

// Else, expect for images, determine content disposition. If content type is supported by

// the browser, then set to inline, else attachment which will pop a 'save as' dialogue.

else if (!contentType.startsWith("image")) {

String accept = request.getHeader("Accept");

disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";

}

// Initialize response.

response.reset();

response.setBufferSize(DEFAULT_BUFFER_SIZE);

response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");

response.setHeader("Accept-Ranges", "bytes");

response.setHeader("ETag", eTag);

response.setDateHeader("Last-Modified", lastModified);

response.setDateHeader("Expires", expires);

// Send requested file (part(s)) to client ------------------------------------------------

// Prepare streams.

RandomAccessFile input = null;

OutputStream output = null;

try {

// Open streams.

input = new RandomAccessFile(file, "r");

output = response.getOutputStream();

if (ranges.isEmpty() || ranges.get(0) == full) {

// Return full file.

Range r = full;

response.setContentType(contentType);

response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

if (content) {

if (acceptsGzip) {

// The browser accepts GZIP, so GZIP the content.

response.setHeader("Content-Encoding", "gzip");

output = new GZIPOutputStream(output, DEFAULT_BUFFER_SIZE);

} else {

// Content length is not directly predictable in case of GZIP.

// So only add it if there is no means of GZIP, else browser will hang.

response.setHeader("Content-Length", String.valueOf(r.length));

}

// Copy full range.

copy(input, output, r.start, r.length);

}

} else if (ranges.size() == 1) {

// Return single part of file.

Range r = ranges.get(0);

response.setContentType(contentType);

response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

response.setHeader("Content-Length", String.valueOf(r.length));

response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

if (content) {

// Copy single part range.

copy(input, output, r.start, r.length);

}

} else {

// Return multiple parts of file.

response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);

response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

if (content) {

// Cast back to ServletOutputStream to get the easy println methods.

ServletOutputStream sos = (ServletOutputStream) output;

// Copy multi part range.

for (Range r : ranges) {

// Add multipart boundary and header fields for every range.

sos.println();

sos.println("--" + MULTIPART_BOUNDARY);

sos.println("Content-Type: " + contentType);

sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

// Copy single part range of multi part range.

copy(input, output, r.start, r.length);

}

// End with multipart boundary.

sos.println();

sos.println("--" + MULTIPART_BOUNDARY + "--");

}

}

} finally {

// Gently close streams.

close(output);

close(input);

}

}

// Helpers (can be refactored to public utility class) ----------------------------------------

/**

* Returns true if the given accept header accepts the given value.

* @param acceptHeader The accept header.

* @param toAccept The value to be accepted.

* @return True if the given accept header accepts the given value.

*/

private static boolean accepts(String acceptHeader, String toAccept) {

String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");

Arrays.sort(acceptValues);

return Arrays.binarySearch(acceptValues, toAccept) > -1

|| Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1

|| Arrays.binarySearch(acceptValues, "*/*") > -1;

}

/**

* Returns true if the given match header matches the given value.

* @param matchHeader The match header.

* @param toMatch The value to be matched.

* @return True if the given match header matches the given value.

*/

private static boolean matches(String matchHeader, String toMatch) {

String[] matchValues = matchHeader.split("\\s*,\\s*");

Arrays.sort(matchValues);

return Arrays.binarySearch(matchValues, toMatch) > -1

|| Arrays.binarySearch(matchValues, "*") > -1;

}

/**

* Returns a substring of the given string value from the given begin index to the given end

* index as a long. If the substring is empty, then -1 will be returned

* @param value The string value to return a substring as long for.

* @param beginIndex The begin index of the substring to be returned as long.

* @param endIndex The end index of the substring to be returned as long.

* @return A substring of the given string value as long or -1 if substring is empty.

*/

private static long sublong(String value, int beginIndex, int endIndex) {

String substring = value.substring(beginIndex, endIndex);

return (substring.length() > 0) ? Long.parseLong(substring) : -1;

}

/**

* Copy the given byte range of the given input to the given output.

* @param input The input to copy the given range to the given output for.

* @param output The output to copy the given range from the given input for.

* @param start Start of the byte range.

* @param length Length of the byte range.

* @throws IOException If something fails at I/O level.

*/

private static void copy(RandomAccessFile input, OutputStream output, long start, long length)

throws IOException

{

byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

int read;

if (input.length() == length) {

// Write full range.

while ((read = input.read(buffer)) > 0) {

output.write(buffer, 0, read);

}

} else {

// Write partial range.

input.seek(start);

long toRead = length;

while ((read = input.read(buffer)) > 0) {

if ((toRead -= read) > 0) {

output.write(buffer, 0, read);

} else {

output.write(buffer, 0, (int) toRead + read);

break;

}

}

}

}

/**

* Close the given resource.

* @param resource The resource to be closed.

*/

private static void close(Closeable resource) {

if (resource != null) {

try {

resource.close();

} catch (IOException ignore) {

// Ignore IOException. If you want to handle this anyway, it might be useful to know

// that this will generally only be thrown when the client aborted the request.

}

}

}

// Inner classes ------------------------------------------------------------------------------

/**

* This class represents a byte range.

*/

protected class Range {

long start;

long end;

long length;

long total;

/**

* Construct a byte range.

* @param start Start of the byte range.

* @param end End of the byte range.

* @param total Total length of the byte source.

*/

public Range(long start, long end, long total) {

this.start = start;

this.end = end;

this.length = end - start + 1;

this.total = total;

}

}

}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值