Blink

纸上得来终觉浅,绝知此事要躬行

Unity实现断点续传下载功能

什么是断点续传

就是下载文件时,不必重头开始下载,而是从指定的位置继续下载,这样的功能就叫做断点续传

为什么需要断点续传

在下载文件的过程中,打断文件下载的原因有很多,比如网络不稳定,导致下载中断,如果没有断点续传功能的话,中断之后需要重新开始下载。例如一个文件有100M大小,我下载了99M,马上就要下载完成了,这是突然网络中断导致下载失败了,我重新开始下载的时候发现又需要重新开始下载,这时候是不是会感觉心态崩了呀,既浪费时间也浪费金钱(毕竟流量也是要钱的)。如果有了断点续传功能的话,我下载了99M,即使网络中断,重连之后我的下载依旧是从99M的位置开始下载,这样给用户的体验就很棒了

如何实现断点续传

《Unity实现断点续传下载功能》
  1. 在下载文件的时候我们会先创建一个与下载文件对应的以.temp为后缀的临时文件, 下载的文件数据会写入这个临时文件中
  2. 每次开始下载的时候会检查是否存在需下载文件的临时文件,如果存在,便从该文件数据长度的地方开始下载写入
  3. 下载完成后便将临时文件移动到目标下载目录

Unity中的代码实现

(一)定义一个错误码枚举,用来标识下载出错的情况

public enum ErrorCode
{
    DownloadContentEmpty,    // 需要下载的文件内容为空
    TempFileMissing,            // 临时文件丢失
}

(二) 定义委托,用来外部注册相关状态的处理

/// <summary>
/// 下载出错
/// </summary>
/// <param name="errorCode">错误码
/// <param name="message">错误信息
public delegate void ErrorEventHandler(ErrorCode errorCode, string message);
/// 
<summary>
/// 下载完成
/// </summary>
/// <param name="message">完成信息
public delegate void CompletedEventHandler(string message);
/// 
<summary>
/// 下载进度
/// </summary>
/// <param name="prg">当前进度
/// <param name="currLength">当前下载完成的长度
/// <param name="totalLength">文件总长度
public delegate void ProgressEventHandler(float prg, long currLength, long totalLength);

(三)实现下载处理脚本(DownloadHandler)

这个脚本是需要继承 DownloadHandlerScript

public class DownloadHandler : DownloadHandlerScript
{
    private string savePath = null; // 保存到的路径
    private string tempPath = null; // 下载临时文件路径
    private long currLength = 0; // 当前已经下载的数据长度
    private long totalLength = 0; // 文件总数据长度
    private long contentLength = 0; // 本次需要下载的数据长度
    private FileStream fileStream = null; // 文件流,用来将接收到的数据写入文件
    private ErrorEventHandler onError = null; // 出错回调
    private CompletedEventHandler onCompleted = null; // 完成回调
    private ProgressEventHandler onProgress = null; // 进度回调

    public DownloadHandler(string savePath, CompletedEventHandler onCompleted, ProgressEventHandler onProgress,
        ErrorEventHandler onError)
    {
        this.savePath = savePath.Replace("\\", "/");
        this.onCompleted = onCompleted;
        this.onProgress = onProgress;
        this.onError = onError;
        this.tempPath = savePath + ".temp";
        fileStream = new FileStream(tempPath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
        currLength = fileStream.Length;
        fileStream.Position = currLength;
    }

    public long CurrLength
    {
        get { return currLength; }
    }

    public long TotalLength
    {
        get { return totalLength; }
    }

    /// 
<summary>
    /// 在收到 Content-Length 标头调用的回调。
    /// </summary>
    /// <param name="contentLength">
    protected override void ReceiveContentLengthHeader(ulong contentLength)
    {
        this.contentLength = (long) contentLength;
        totalLength = this.contentLength + currLength;
    }

    /// 
<summary>
    /// 从远程服务器收到数据时调用的回调。
    /// </summary>
    /// <param name="data">
    /// <param name="dataLength">
    /// 
<returns></returns>
    protected override bool ReceiveData(byte[] data, int dataLength)
    {
        // 如果下载的数据长度小于等于0,就结束下载
        if (contentLength <= 0 || data == null || data.Length <= 0)
        {
            return false;
        }

        fileStream.Write(data, 0, dataLength);
        currLength += dataLength;
        onProgress?.Invoke(currLength * 1.0f / totalLength, currLength, totalLength);
        return true;
    }

    /// 
<summary>
    /// 在从远程服务器接收所有数据后调用的回调
    /// </summary>
    protected override void CompleteContent()
    {
        // 接收完成所有数据后,首先关闭文件流
        Close();
        // 如果服务器上不存在该文件,请求下载的内容长度会为0
        // 所以需要特殊处理这种情况
        if (contentLength <= 0)
        {
            onError(ErrorCode.DownloadContentEmpty, "下载内容长度为0");
            return;
        }
        // 如果下载完成后,临时文件如果被意外删除了,也抛出错误提示
        if (!File.Exists(tempPath))
        {
            onError(ErrorCode.TempFileMissing, "下载临时缓存文件丢失");
            return;
        }
        // 如果下载的文件已经存在,就删除原文件
        if (File.Exists(savePath))
        {
            File.Delete(savePath);
        }
        // 通过了以上的校验后,就将临时文件移动到目标路径,下载成功
        File.Move(tempPath, savePath);
        onCompleted("下载文件完成");
    }

    // 关闭
    public void Close()
    {
        if (fileStream == null) return;
        fileStream.Close();
        fileStream.Dispose();
        fileStream = null;
    }
}

(四)实现下载器脚本 (Downloader)

public class Downloader
{
    private string url = null; // 需要下载的文件的地址
    private string savePath = null; // 保存的路径
    private UnityWebRequest request = null; // Unity中用来与Web服务器进行通信的类
    private DownloadHandler downloadHandler = null; // 我们自己实现的下载处理类
    private ErrorEventHandler onError = null; // 出错回调
    private CompletedEventHandler onCompleted = null; // 完成回调
    private ProgressEventHandler onProgress = null; // 进度回调

    public Downloader(string url, string savePath, CompletedEventHandler onCompleted, ProgressEventHandler onProgress,
        ErrorEventHandler onError)
    {
        this.url = url;
        this.savePath = savePath;
        this.onCompleted = onCompleted;
        this.onProgress = onProgress;
        this.onError = onError;
    }

    /// 
<summary>
    /// 开始下载
    /// </summary>
    /// <param name="timeout">超时时间(秒)
    public void Start(int timeout = 10)
    {
        request = UnityWebRequest.Get(url);
        if (!string.IsNullOrEmpty(savePath))
        {
            request.timeout = timeout;
            request.disposeDownloadHandlerOnDispose = true;
            downloadHandler = new DownloadHandler(savePath, onCompleted, onProgress, onError);
            // 这里是设置http的请求头
            // range表示请求资源的部分内容(不包括响应头的大小),单位是byte
            request.SetRequestHeader("range", $"bytes={downloadHandler.CurrLength}-");
            request.downloadHandler = downloadHandler;
        }
        request.SendWebRequest();
    }

    /// 
<summary>
    /// 清理
    /// </summary>
    public void Dispose()
    {
        onError = null;
        onCompleted = null;
        onProgress = null;
        if (request!=null)
        {
            // 如果下载没有完成,就中止
            if (!request.isDone)
                request.Abort();
            request.Dispose();
            request = null;
        }
    }
}

(五)示例

简单的测试一下,按A键开始下载,按D键结束下载,然后看看是否能够断点续传

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Example : MonoBehaviour
{
    private Downloader downloader;
    private string url = "http://vfx.mtime.cn/Video/2019/03/21/mp4/190321153853126488.mp4";
    private string path;

    private void Start()
    {
        path = Application.dataPath + "/../190321153853126488.mp4";
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            if (downloader != null)
            {
                return;
            }
            else
            {
                downloader = new Downloader(url, path, OnCompleted, OnProgress, OnError);
                downloader.Start();
            }
        }

        if (Input.GetKeyDown(KeyCode.D))
        {
            if (downloader != null)
            {
                downloader.Dispose();
                downloader = null;
            }
        }
    }

    private void OnApplicationQuit()
    {
        if (downloader != null)
        {
            downloader.Dispose();
            downloader = null;
        }
    }

    private void OnProgress(float prg, long currLength, long totalLength)
    {
        Debug.LogFormat("下载进度{0:0.00}%,{1}M/{2}M", (prg * 100), currLength * 1.0f / 1024 / 1024,
            totalLength * 1.0f / 1024 / 1024);
    }

    private void OnCompleted(string msg)
    {
        Debug.Log(msg);
    }

    private void OnError(ErrorCode code, string msg)
    {
    }
}

《Unity实现断点续传下载功能》
《Unity实现断点续传下载功能》

点赞
  1. 能够留住时间说道:

    大佬使用遇到问题,第一次运行文件1的下载总长度是正确的,第二次换了文件2的URL和文件名,总长度为什么还是文件1的

  2. 火鸡味锅巴说道:

    功能封装的很好用,必须点个赞。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注