|
|
using System;
|
using System.Collections;
|
using System.Collections.Generic;
|
using System.IO;
|
using System.Linq;
|
using System.Threading;
|
using UnityEngine;
|
using UnityEngine.Networking;
|
|
|
namespace StartAot
|
{
|
public class DownloadMgr : SingletonMonobehaviour<DownloadMgr>
|
{
|
#region 配置
|
public static int MaxRetryTimes = 5;//最大重试的次数
|
public static int MaxDownLoadTask = 10;//最大同时下载数量
|
public static int TimeOut = 20;//下载超时时间
|
#endregion
|
|
#region 任务列表
|
private Dictionary<string, DownloadTask> allTask = new Dictionary<string, DownloadTask>(); //所有任务组
|
private Queue<DownloadTask> waitTask = new Queue<DownloadTask>(); //当前等待下载的任务
|
private Dictionary<string, DownloadTask> downloadTask = new Dictionary<string, DownloadTask>(); //当前正在下载的任务
|
#endregion 任务列表
|
|
private bool isBegin;
|
private float startTickcount = 0;
|
private Func<bool, DownloadTask, bool> onTaskCompleted;//文件下载完成回调,<是否全部完成,任务对象,return true:立即重试,false:到队尾重试>
|
|
//总速度
|
private float speed;
|
public float Speed { get { UpdateValue(); return speed; } }
|
public string SpeedFormat { get { return StringUtility.FormatSpeed(Speed); } }
|
|
//总已下载大小
|
private long finishedBytes;//已完成任务的大小
|
private long downloadedBytes;
|
private long downloadedBytesRef;
|
public long DownloadedBytes { get { UpdateValue(); return downloadedBytes; } }
|
|
public bool IsBegin { get { return isBegin; } }
|
public bool IsFinished { get { return allTask.Count == 0; } }
|
|
|
//更新数值
|
public void UpdateValue()
|
{
|
if (startTickcount == 0)//任务还未开始
|
return;
|
float second = Mathf.Abs(Time.time - startTickcount);
|
//DebugEx.LogFormat("更新数值:{0} {1} {2}", Time.time, startTickcount, second);
|
if (second < 1f)//刷新间隔1秒
|
return;
|
long bytes = 0;
|
long length = 0;
|
foreach (var pair in downloadTask)
|
{
|
var task = pair.Value;
|
bytes += task.downloadedBytes;
|
length += task.remoteLength;
|
}
|
downloadedBytes = finishedBytes + bytes;
|
// totalLength = length;
|
|
var delta = downloadedBytes < downloadedBytesRef || downloadedBytesRef == 0 ? 0 : downloadedBytes - downloadedBytesRef;
|
downloadedBytesRef = downloadedBytes;
|
speed = delta / second;
|
startTickcount = Time.time;
|
}
|
|
//添加任务
|
public DownloadMgr AddTask(DownloadTask task)
|
{
|
if (allTask.ContainsKey(task.Key))
|
{
|
DebugEx.LogFormat("DownloadMgr-已存在相同的下载任务:{0}", task.Key);
|
return this;
|
}
|
allTask.Add(task.Key, task);
|
waitTask.Enqueue(task);
|
return this;
|
}
|
|
//准备下载,重置状态
|
public DownloadMgr Prepare()
|
{
|
Stop();
|
this.onTaskCompleted = null;
|
//清除所有任务
|
allTask.Clear();
|
waitTask.Clear();
|
downloadTask.Clear();
|
finishedBytes = 0;
|
return this;
|
}
|
|
/// <summary>
|
/// 开始下载
|
/// </summary>
|
/// <param name="onTaskCompleted">任务结束回调</param>
|
/// <returns></returns>
|
public DownloadMgr Begin(Func<bool, DownloadTask, bool> onTaskCompleted = null)
|
{
|
this.onTaskCompleted = onTaskCompleted;
|
startTickcount = Time.time;
|
downloadedBytesRef = 0;
|
isBegin = true;
|
foreach (var pair in downloadTask)
|
{
|
//重新开始下载任务
|
if (pair.Value.BeginDownload(OnTaskCompleted))
|
DebugEx.LogFormat("重新开始下载:{0}", pair.Key);
|
}
|
return this;
|
}
|
|
//停止下载
|
public void Stop()
|
{
|
startTickcount = 0;
|
isBegin = false;
|
foreach (var pair in downloadTask)
|
{
|
pair.Value.Stop();//停止所有下载任务
|
}
|
}
|
|
private void Update()
|
{
|
if (!isBegin)
|
return;
|
if (waitTask.Count == 0)
|
return;
|
if (downloadTask.Count >= MaxDownLoadTask)
|
return;
|
var task = waitTask.Dequeue();
|
if (task.IsDone)
|
return;
|
//开始下载
|
if (!downloadTask.ContainsKey(task.Key))
|
downloadTask.Add(task.Key, task);
|
task.BeginDownload(OnTaskCompleted);
|
}
|
|
// 任务下载完成
|
private void OnTaskCompleted(DownloadTask task)
|
{
|
downloadTask.Remove(task.Key);
|
if (task.IsDone)//下载成功
|
{
|
finishedBytes += task.downloadedBytes;
|
allTask.Remove(task.Key);
|
this.onTaskCompleted?.Invoke(IsFinished, task);
|
}
|
else//下载失败
|
{
|
var prior = this.onTaskCompleted == null ? false : this.onTaskCompleted(IsFinished, task);//返回值为是否立即重试
|
//所有任务中不存在,说明已经被清除了,就不重新排队
|
if (allTask.ContainsKey(task.Key))
|
{
|
if (prior) //不排队,直接重新开始
|
{
|
downloadTask.Add(task.Key, task);
|
if (isBegin)
|
task.BeginDownload(OnTaskCompleted);
|
}
|
else
|
waitTask.Enqueue(task); //重新排队
|
}
|
}
|
//TODO:这里暂时用无限重复下载,不做上限处理
|
// if (task.failedTimes >= MaxRetryTimes)//重试次数已达到上限,任务失败
|
// task.team.OnFailed(task);
|
// else//重试次数未达到上限,重新加入等待队列
|
// waitTask.Enqueue(task);
|
}
|
}
|
|
|
|
public class DownloadTask
|
{
|
//下载任务状态
|
public enum TaskState
|
{
|
Wait,
|
Working,
|
Succeed,
|
Failed
|
}
|
public readonly string remoteUrl; //远程连接地址
|
public readonly string localFile; //本地地址
|
public readonly string tempFile;//临时文件地址
|
private readonly bool clearCache;//是否清除旧的下载缓存,(断点续传)
|
public readonly object obj; //自定义对象
|
private Action<DownloadTask> onCompleted;//下载完成回调
|
private TaskState state;//下载状态
|
|
public bool isStop;//下载被停止
|
public int failedTimes = 0;//失败的次数
|
private bool requestHeaderOk = false;//是否已获取头信息
|
private System.DateTime remoteLastModified; //远端文件最后修改时间
|
public long remoteLength;//远端文件大小
|
private bool isAcceptRange = false;//服务端是否支持断点续传
|
private string errorStr;//错误信息
|
public long downloadedBytes;//当前已下载数据
|
public float downloadedSpeed;//当前下载速度
|
private float beginTickCount;
|
|
private UnityWebRequest fileRequest;
|
|
public string Key { get { return remoteUrl; } }
|
|
public string SpeedFormat { get { return StringUtility.FormatSpeed(downloadedSpeed); } }
|
|
//本地文件大小
|
public long LocalLength { get { return (File.Exists(tempFile)) ? (new FileInfo(tempFile)).Length : 0L; } }
|
|
//本地文件最后修改时间
|
private System.DateTime LocalLastModified { get { return File.Exists(tempFile) ? File.GetLastWriteTime(tempFile) : DateTime.MinValue; } }
|
public bool IsWorking { get { return state == TaskState.Working; } }
|
public bool IsDone { get { return state == TaskState.Succeed; } }
|
|
/// <summary>
|
///
|
/// </summary>
|
/// <param name="remoteUrl">下载地址</param>
|
/// <param name="localUrl">本地保存地址</param>
|
/// <param name="obj">自定义对象</param>
|
/// <param name="clearCache">是否清除上次下载的缓存</param>
|
public DownloadTask(string remoteUrl, string localUrl, object obj = null, bool clearCache = false)
|
{
|
this.remoteUrl = remoteUrl;
|
this.localFile = localUrl;
|
this.tempFile = localFile + ".temp";
|
this.obj = obj;
|
this.clearCache = clearCache;
|
this.state = TaskState.Wait;
|
}
|
|
private bool IsOutdated //本地文件是否已过时
|
{
|
get
|
{
|
if (File.Exists(tempFile))
|
return remoteLastModified > LocalLastModified;
|
return false;
|
}
|
}
|
|
private void SetSucceed()
|
{
|
state = TaskState.Succeed;
|
}
|
|
private void SetFailed(string error)
|
{
|
this.state = TaskState.Failed;
|
this.errorStr = error;
|
this.failedTimes++;
|
}
|
|
public void BeginGetHeader()
|
{
|
if (IsWorking)
|
return;
|
DownloadMgr.Instance.StartCoroutine(Co_GetHeader());
|
}
|
|
public bool BeginDownload(Action<DownloadTask> onCompleted)
|
{
|
if ((IsWorking && requestHeaderOk) || IsDone)//已经在下载任务中或已经下载完成了
|
return false;
|
if (state == TaskState.Wait)//第一次尝试
|
failedTimes = 0;
|
this.onCompleted = onCompleted;
|
this.isStop = false;
|
DownloadMgr.Instance.StartCoroutine(Co_DownloadFile());
|
return true;
|
}
|
|
//停止下载
|
public void Stop()
|
{
|
isStop = true;
|
if (fileRequest != null && !fileRequest.isDone)
|
fileRequest.Abort();
|
DebugEx.LogFormat("暂停任务:{0}", remoteUrl);
|
}
|
|
//获取文件头信息
|
private IEnumerator Co_GetHeader()
|
{
|
this.state = TaskState.Working;
|
FileExtersion.MakeSureDirectory(tempFile);
|
DebugEx.LogFormat("开始获取头信息:{0}", remoteUrl);
|
using (var www = UnityWebRequest.Head(remoteUrl))
|
{
|
www.timeout = DownloadMgr.TimeOut;
|
yield return www.SendWebRequest();
|
if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError)
|
{
|
DebugEx.LogErrorFormat("头信息获取失败:{0};error:{1}", remoteUrl, www.error);
|
requestHeaderOk = false;
|
SetFailed(www.error);
|
www.Dispose();
|
yield break;
|
}
|
long.TryParse(www.GetResponseHeader(HttpHeader.ContentLength), out this.remoteLength);
|
this.remoteLastModified = DateTime.Parse(www.GetResponseHeader(HttpHeader.LastModified));
|
var acceptRange = www.GetResponseHeader(HttpHeader.AcceptRanges);
|
this.isAcceptRange = acceptRange != "none";
|
DebugEx.LogFormat("头信息获取成功:{0};大小:{1};修改时间:{2};是否支持续传:{3}", remoteUrl, remoteLength, remoteLastModified, acceptRange);
|
requestHeaderOk = true;
|
state = TaskState.Wait;
|
www.Dispose();
|
}
|
}
|
private IEnumerator Co_DownloadFile()
|
{
|
state = TaskState.Working;
|
while (Time.time < beginTickCount + 5)//限制重新下载的时间间隔
|
{
|
if (isStop)
|
yield break;
|
else
|
yield return null;
|
}
|
beginTickCount = Time.time;
|
if (!requestHeaderOk)
|
yield return Co_GetHeader();
|
if (!requestHeaderOk)//头文件获取失败
|
{
|
onCompleted?.Invoke(this);
|
yield break;
|
}
|
try
|
{
|
if (clearCache && File.Exists(tempFile))
|
{
|
File.Delete(localFile);//清除上次下载的临时文件
|
DebugEx.LogFormat("已清除缓存文件:{0}", tempFile);
|
}
|
}
|
catch (Exception e)
|
{
|
DebugEx.LogFormat("清除缓存文件失败:{0};error:{1}", tempFile, e);
|
}
|
if (File.Exists(tempFile))//临时文件存在
|
{
|
var _localLength = LocalLength;
|
var _outdated = IsOutdated;
|
//文件完整且未过期,无须再下载了
|
if (_localLength == remoteLength && !_outdated)
|
{
|
yield return Move(tempFile, localFile);//把临时文件改名为正式文件
|
onCompleted?.Invoke(this);
|
yield break;
|
}
|
else if (_localLength > remoteLength || _outdated)//文件不对或过期了
|
{
|
if (!_outdated) DebugEx.LogFormat("本地文件大于服务器端文件:{0}", tempFile);
|
if (_outdated) DebugEx.LogFormat("文件已过期:{0}", tempFile);
|
try
|
{
|
if (File.Exists(tempFile))
|
File.Delete(tempFile);
|
}
|
catch (Exception e)
|
{
|
DebugEx.LogErrorFormat("删除临时文件出错:{0};error:{1}", tempFile, e);
|
isAcceptRange = false;//如果删除临时文件出错,取消续传,覆写文件
|
}
|
}
|
}
|
if (isStop)//任务被暂停
|
{
|
state = TaskState.Wait;
|
yield break;
|
}
|
DebugEx.LogFormat("下载任务开始:{0}", remoteUrl);
|
fileRequest = UnityWebRequest.Get(remoteUrl);
|
|
fileRequest.downloadHandler = new DownloadHandlerFile(tempFile, this.isAcceptRange);
|
if (LocalLength != 0L && isAcceptRange)
|
fileRequest.SetRequestHeader(HttpHeader.Range, "bytes=" + LocalLength + "-");
|
DebugEx.LogFormat("下载请求范围:{0} ;bytes={1}", remoteUrl, LocalLength);
|
var localLength = LocalLength;
|
this.downloadedBytes = localLength;
|
fileRequest.SendWebRequest();
|
while (!fileRequest.isDone)
|
{
|
var length = localLength + (long)fileRequest.downloadedBytes;//本地文件大小+本次下载的数据大小
|
this.downloadedSpeed = Math.Max(0, (length - this.downloadedBytes) / Time.deltaTime);
|
this.downloadedBytes = length;
|
yield return null;
|
}
|
if (fileRequest.result == UnityWebRequest.Result.ConnectionError || fileRequest.result == UnityWebRequest.Result.ProtocolError)
|
{
|
if (isStop)//任务被暂停
|
{
|
state = TaskState.Wait;
|
}
|
else //任务失败
|
{
|
SetFailed(fileRequest.error);
|
onCompleted?.Invoke(this);
|
DebugEx.LogErrorFormat("文件下载失败:{0};error:{1}", remoteUrl, fileRequest.error);
|
}
|
fileRequest.Dispose();
|
fileRequest = null;
|
yield break;
|
}
|
DebugEx.LogFormat("文件下载成功:{0};", tempFile);
|
fileRequest.Dispose();
|
fileRequest = null;
|
|
FileInfo tempFileInfo = new FileInfo(tempFile);
|
if (tempFileInfo.Exists)
|
{
|
//判断临时文件和远程文件大小是否一致
|
if (tempFileInfo.Length != remoteLength && remoteLength != 0L)
|
{
|
DebugEx.LogErrorFormat("下载完成后但是大小不一致:{0}; {1} || {2}", remoteUrl, tempFileInfo.Length, remoteLastModified);
|
SetFailed("文件下载完成后但是大小不一致");
|
}
|
else
|
{ //大小一致
|
yield return Move(tempFile, localFile);
|
}
|
}
|
else
|
{ //临时文件不存在
|
DebugEx.LogErrorFormat("下载完成但是临时文件不存在:{0}", tempFile);
|
SetFailed("下载完成但是文件不存在");
|
}
|
onCompleted?.Invoke(this);
|
yield return null;
|
}
|
|
//拷贝文件,把临时文件改成正式文件
|
private IEnumerator Move(string sourceFile, string destFile)
|
{
|
var copyState = 0;//复制文件状态,0等待,1成功,2失败
|
try
|
{
|
// if (speedLimit)
|
ThreadPool.QueueUserWorkItem((object _obj) =>
|
{
|
if (File.Exists(destFile))
|
File.Delete(destFile);
|
File.Move(sourceFile, destFile);
|
System.Threading.Interlocked.Exchange(ref copyState, 1);
|
});
|
// else
|
// {
|
// if (File.Exists(destFile))
|
// File.Delete(destFile);
|
// File.Move(sourceFile, destFile);
|
// copyState = 1;
|
// }
|
}
|
catch (Exception ex)
|
{
|
DebugEx.LogErrorFormat("临时文件改名出错:{0}", ex);
|
copyState = 1;
|
}
|
while (copyState == 0)
|
yield return null;
|
if (copyState == 1)
|
{
|
DebugEx.LogFormat("文件复制成功:{0}", localFile);
|
SetSucceed();
|
}
|
else
|
SetFailed("临时文件改名出错");
|
}
|
|
}
|
|
}
|