今天遇到了个问题,做一个异步下载器,支持断点续传功能。其中难点在于断点续传。本来想着直接使用BestHttp的,不知道是不是用的版本不对,并不直接做好断点续传,需要手动实现。
本篇文章将逐步更新完成,因为要将本功能接入到框架中,需要一定时间优化,还有要把之前的隐藏bug修复一下,敬请期待!!
下面代码将结合AI完成,AI真的加速了代码开发效率。
原理
下载对于各位开发者来说并不难;但是断点续传,询问AI得知,需要结合着http的请求透实现,下面是AI说的,也带着流程图
其实整个核心就是Range请求,Range是 HTTP 协议中用于请求资源部分内容的头部字段,其核心作用是让客户端告诉服务器 “需要获取文件的哪部分数据”,从而实现断点续传。基于本原理,完全可以实现分块的多线程下载,不过将会更加复杂。
YooAsset资源管理框架,其实现了一个跟好的下载器,并且支持异步下载以及断点续传,并且结合其内部的异步框架,实现更加优秀的逻辑。
因为之前已经各种学习(东拼西凑)了一个框架后续将直接在这个框架上接入。本篇文章仅仅是借助AI进行一个基本实现。
编写代码
首先,写一个任务类,任务类主要记录着当前已经下载完成的数据,和总的数据大小(创建时记录)。其他需要记录的可以自己添加。
using System;
using UnityEngine.Networking;
namespace 断点续传.Script
{
public class DownloadTask
{
public string url;
public string savePath;
/// <summary>
/// Unity的下载器
/// </summary>
public UnityWebRequest request;
/// <summary>
/// 文件总大小
/// </summary>
public long totalBytes;
/// <summary>
/// 已下载大小
/// </summary>
public long downloadedBytes;
/// <summary>
/// 已下载长度
/// </summary>
public long downloadProgress;
public bool isPaused;
public bool isDone;
public bool isError;
/// <summary>
/// 错误日志
/// </summary>
public string errorMessage;
/// <summary>
/// 下载进度率刷新
/// </summary>
public Action<DownloadTask> onProgress;
/// <summary>
/// 完成下载
/// </summary>
public Action<DownloadTask> onCompleted;
/// <summary>
/// 下载文件处理器
/// </summary>
public DownloadHandlerFile downloadHandlerFile;
}
}
关键部分来了,为了便于管理,将创建一个单例管理器,存储所有下载任务,并且返回一个上面的任务类,便于使用者控制下载状态。
不过,在此之前,先基于DownloadHandlerScript基类,实现一个下载写入器,在这里实现一个对已下载数据的处理逻辑,避免一直在内存中的占用。下面的代码就是自定义的下载处理函数,本次将边下边写入,避免内存爆炸。这里的代码要注意,写一个释放文件流的函数,避免在非下载时的文件占用警告。
using System.IO;
using UnityEngine.Networking;
namespace 断点续传.Script
{
public class DownloadHandlerFile : DownloadHandlerScript
{
/// <summary>
/// 存储的文件
/// </summary>
private string filePath;
/// <summary>
/// 文件流
/// </summary>
private FileStream fileStream;
/// <summary>
/// 存储文件是否基于追加形式
/// </summary>
private bool append;
DownloadTask downloadTask;
public DownloadHandlerFile(string path, bool append,DownloadTask task) : base()
{
this.filePath = path;
this.append = append;
downloadTask=task;
// 确保目录存在
string directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 打开文件流
fileStream = new FileStream(
path,
append ? FileMode.Append : FileMode.Create,
FileAccess.Write,
FileShare.Read
);
}
public void Pause()
{
fileStream.Close();
}
/// <summary>
/// 接收下载到的数据
/// </summary>
protected override bool ReceiveData(byte[] data, int dataLength)
{
if (data == null || data.Length == 0)
return false;//终止下载器
fileStream.Write(data, 0, dataLength);
downloadTask.downloadedBytes += dataLength;
return true;//继续下载
}
/// <summary>
/// 当下载完成时被调用,可以在这里进行清理或最终处理。
/// </summary>
protected override void CompleteContent()
{
fileStream.Close();
fileStream.Dispose();
}
}
}
最后的就是整个管理代码。主要的是记录所有下载任务,便于下载管理。而且根据取消时的需求,决定是否删除本地文件。获取长度时,经过测试,使用UnityWebRequest的UnityWebRequest headReq = UnityWebRequest.Head(url)请求时,Content-Length返回的值和实际不符,不知道是啥问题,最终通过在下载过程中来使用request.GetResponseHeader("Content-Length")函数获取目标文件大小,同时要注意,返回的是一个string值,要判断是否为空白,然后尝试转为长度值。
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using DownloadHandlerFile = 断点续传.Script.DownloadHandlerFile;
using File = UnityEngine.Windows.File;
namespace 断点续传.Script
{
public class AsyncDownloader : MonoBehaviour
{
private static AsyncDownloader _instance;
public static AsyncDownloader Instance
{
get
{
if (_instance == null)
{
GameObject obj = new GameObject("AsyncDownloader");
_instance = obj.AddComponent<AsyncDownloader>();
DontDestroyOnLoad(obj);
}
return _instance;
}
}
private List<DownloadTask> activeTasks = new List<DownloadTask>();
/// <summary>
/// 创建下载任务,并且立即启动下载
/// </summary>
/// <param name="url">下载地址</param>
/// <param name="savePath">保存路径</param>
/// <param name="onProgress">进度回调</param>
/// <param name="onCompleted">完成回调</param>
/// <returns></returns>
public DownloadTask DownloadFile(string url, string savePath,
Action<DownloadTask> onProgress = null,
Action<DownloadTask> onCompleted = null)
{
// 检查是否已有相同任务
var existingTask = activeTasks.Find(t => t.url == url && t.savePath == savePath);
if (existingTask != null)
{
if (existingTask.isPaused)
{
ResumeDownload(existingTask);
}
return existingTask;
}
var task = new DownloadTask()
{
url = url,
savePath = savePath,
onProgress = onProgress,
onCompleted = onCompleted,
isPaused = false,
isDone = false
};
activeTasks.Add(task);
StartCoroutine(DownloadCoroutine(task));
return task;
}
/// <summary>
/// 异步执行的下载逻辑函数
/// </summary>
/// <param name="task">任务</param>
/// <returns></returns>
private IEnumerator DownloadCoroutine(DownloadTask task)
{
// 检查已下载文件大小(断点续传),基于已下载文件大小信息
FileInfo fileInfo = new FileInfo(task.savePath);
//判断是否需要徐
bool resumeDownload = fileInfo.Exists && fileInfo.Length > 0;
// 创建下载请求
if (resumeDownload)
{
//如果断点续传,则需要设置Rangeq请求头,设置请求的数据范围
task.request = UnityWebRequest.Get(task.url);
task.request.SetRequestHeader("Range", $"bytes={fileInfo.Length}-");
}
else
{
//直接下载
task.request = UnityWebRequest.Get(task.url);
}
var downlaodFileHandle=new DownloadHandlerFile(task.savePath, resumeDownload,task);
// 设置下载处理器
task.request.downloadHandler = downlaodFileHandle;
task.downloadHandlerFile=downlaodFileHandle;
// 开始下载
task.request.SendWebRequest();
// 更新进度
// 在本循环内,每一帧检查是否请求暂停
// 其实暂停就行进行了取消
while (!task.request.isDone)
{
//如果总大小没有记录,则获取
if (task.totalBytes<=0)
{
//这两种状态时,获取长度信息头
if (task.request.result == UnityWebRequest.Result.InProgress||task.request.result==UnityWebRequest.Result.Success )
{
//获取,保证不为空和能正常把string转为long值
string lengthHeader = task.request.GetResponseHeader("Content-Length");
if (!string.IsNullOrEmpty(lengthHeader) && long.TryParse(lengthHeader, out long size))
{
//存储
task.totalBytes = size;
}
}
}
//Debug.Log($"长度:{task.request.GetResponseHeader("Content-Length")}+{task.request.result}+{task.totalBytes}");
if (task.isPaused)
{
task.request.Abort();
task.downloadHandlerFile.Pause();
yield break;
}
if (task.onProgress != null)
{
task.onProgress.Invoke(task);
}
yield return null;
}
// 处理完成状态
if (task.request.result == UnityWebRequest.Result.Success)
{
task.isDone = true;
}
else
{
task.isError = true;
task.errorMessage = task.request.error;
}
// 触发完成回调
if (task.onCompleted != null)
{
task.onCompleted.Invoke(task);
}
// 清理
activeTasks.Remove(task);
task.request.Dispose();
}
/// <summary>
/// 暂停下载
/// </summary>
/// <param name="task">下载任务</param>
public void PauseDownload(DownloadTask task)
{
if (task != null && !task.isPaused && !task.isDone)
{
task.isPaused = true;
if (task.request != null)
{
task.request.Abort();
}
}
}
/// <summary>
/// 重启下载
/// </summary>
/// <param name="task"></param>
public void ResumeDownload(DownloadTask task)
{
if (task != null && task.isPaused && !task.isDone)
{
task.isPaused = false;
StartCoroutine(DownloadCoroutine(task));
}
}
/// <summary>
/// 取消下砸
/// </summary>
/// <param name="task"></param>
public void CancelDownload(DownloadTask task)
{
if (task != null && !task.isDone)
{
task.isPaused = true;
if (task.request != null)
{
task.request.Abort();
}
activeTasks.Remove(task);
if (File.Exists(task.savePath))
{
File.Delete(task.savePath);
}
}
}
/// <summary>
/// 根据任务获取下载进度
/// </summary>
/// <param name="task"></param>
/// <returns></returns>
public static float GetProgress(DownloadTask task)
{
if (task == null || task.totalBytes <= 0) return 0;
return (float)task.downloadedBytes / task.totalBytes;
}
}
}
上面这三个脚本代码就算是核心内容了,核心要素就是设置请求头Range,值为一个范围bytes= start-end,使用本请头,只要服务器不做限制,可以实现分块下载。
仓库和下载地址
本篇代码已开源到我的一个测试仓库中的UnityAsyncDownload分支下。
同时也打包成了个工具包:下载地址
在把他加到自己的框架里的时候,进行了修改,写了一个支持Unitask异步(用的自己实现的一个异步基类,详细请看我的另一篇文章[# Unity中基于UniTask的异步实现]的版本
评论