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

- 在下载文件的时候我们会先创建一个与下载文件对应的以.temp为后缀的临时文件, 下载的文件数据会写入这个临时文件中
- 每次开始下载的时候会检查是否存在需下载文件的临时文件,如果存在,便从该文件数据长度的地方开始下载写入
- 下载完成后便将临时文件移动到目标下载目录
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)
{
}
}


大佬使用遇到问题,第一次运行文件1的下载总长度是正确的,第二次换了文件2的URL和文件名,总长度为什么还是文件1的
功能封装的很好用,必须点个赞。