1. 介绍

HTTP断点续传是一种通过多次请求,将文件分块传输的技术。这样,如果在传输过程中发生了中断,可以从中断处继续传输,而不需要重新开始。这对于大文件的下载是一种有效的优化手段,提高了下载的稳定性和效率。

2. 原理

HTTP断点续传的原理基于HTTP协议的能力和服务器的支持。当客户端需要下载一个文件时,通常会发送一个HTTP请求到服务器来获取文件内容。在断点续传的情况下,这个过程有所不同。

  1. Range头部字段: HTTP协议中的Range头部字段允许客户端请求服务器发送文件的指定范围,而不是整个文件。这个范围可以通过字节偏移量来指定,比如从哪个字节到哪个字节的范围。
  2. 服务器支持: 为了实现断点续传,服务器需要支持处理这样的部分请求。服务器接收到这个带有Range头部字段的请求后,会识别并返回请求的部分内容,而不是整个文件。
  3. 客户端处理: 客户端收到部分内容后,可以将这些部分数据拼接成完整的文件。如果下载中断,客户端可以发送新的请求,请求中包含之前下载过的字节范围以及后续需要的字节范围,从而继续下载。

3. 简单实现

import android.util.Log;

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

public class FileDownloader {

    private static final String TAG = "FileDownloader";
    private static final int BUFFER_SIZE = 4096;

    private String downloadUrl;
    private String destinationPath;
    private DownloadListener downloadListener;

    public FileDownloader(String downloadUrl, String destinationPath, DownloadListener downloadListener) {
        this.downloadUrl = downloadUrl;
        this.destinationPath = destinationPath;
        this.downloadListener = downloadListener;
    }

    public void startDownload() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                downloadFile();
            }
        }).start();
    }

    private void downloadFile() {
        HttpURLConnection connection = null;
        InputStream inputStream = null;
        FileOutputStream outputStream = null;

        try {
            URL url = new URL(downloadUrl);
            connection = (HttpURLConnection) url.openConnection();

            // 设置部分下载的起始点
            File destinationFile = new File(destinationPath);
            //获取了已下载文件的大小
            long downloadedSize = destinationFile.length();
            //告诉服务器只发送文件中指定范围的字节,即从 downloadedSize 字节位置开始继续下载。这是 HTTP 断点续传的关键。
            connection.setRequestProperty("Range", "bytes=" + downloadedSize + "-");

            connection.connect();

            // 检查服务器是否支持部分内容
            int responseCode = connection.getResponseCode();
            if (responseCode / 100 != 2 && responseCode != 206) {
                // 服务器不支持部分内容,从头开始
                downloadedSize = 0;
            }

            int fileLength = connection.getContentLength() + (int) downloadedSize;

            inputStream = connection.getInputStream();
            //创建了一个输出流,将数据写入到 destinationFile 文件中。
            // 第二个参数 true 表示以追加模式打开文件,即如果文件已经存在,将在文件末尾追加新的数据。
            outputStream = new FileOutputStream(destinationFile, true);

            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                downloadedSize += bytesRead;

                // 更新进度
                if (downloadListener != null) {
                    int progress = (int) (downloadedSize * 100 / fileLength);
                    downloadListener.onProgressUpdate(progress);
                }
            }

            // 下载完成
            if (downloadListener != null) {
                downloadListener.onDownloadComplete();
            }

        } catch (IOException e) {
            Log.e(TAG, "Error downloading file: " + e.getMessage());
        } finally {
            try {
                if (inputStream != null) inputStream.close();
                if (outputStream != null) outputStream.close();
                if (connection != null) connection.disconnect();
            } catch (IOException e) {
                Log.e(TAG, "Error closing resources: " + e.getMessage());
            }
        }
    }

    public interface DownloadListener {
        void onProgressUpdate(int progress);

        void onDownloadComplete();
    }
}

==需要注意== 关于断线续传的HTTP状态码不是200,而是206,如果服务器返回的是200,则不支持续传,此时进行全覆盖即从头开始下载!