Hot Update DownLoader

Hot Update DownLoader
This commit is contained in:
ALEXTANG
2022-06-10 17:40:02 +08:00
parent 4b5aaa3bbc
commit 4d481f0379
10 changed files with 724 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
using System.IO;
using UnityEngine.Networking;
namespace TEngine
{
public class DownloadHandlerFileRange : DownloadHandlerScript, IDownload
{
private readonly string _url;
private string _path;
private string _md5;
private FileStream _fileStream;
private UnityWebRequest _unityWebRequest;
private long _totalFileSize = 0;
private long _curFileSize = 0;
public bool HasError { get; private set; }
protected BackgroundDownloadStatus _status = BackgroundDownloadStatus.NotBegin;
private DownloadImpl _imp;
public void SetImp(DownloadImpl imp)
{
_imp = imp;
}
public DownloadHandlerFileRange(string url, string path, long totalLength, string md5) : base(new byte[1024 * 1024])
{
TLogger.LogInfo($"DownloadHandlerFileRange url:{url},path:{path},totalLength:{totalLength}");
_url = url;
_path = path;
_md5 = md5;
var dirPath = Path.GetDirectoryName(_path);
if (dirPath != null)
{
TLogger.LogInfo($"DownloadHandlerFileRange dirPath{dirPath}");
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
}
_totalFileSize = totalLength;
_status = BackgroundDownloadStatus.NotBegin;
}
/// <summary>
/// 兼容断点续传
/// </summary>
public void StartDownload()
{
if (_status != BackgroundDownloadStatus.NotBegin)
{
return;
}
var index = _path.IndexOf("_md5_");
var fileMd5 = string.Empty;
if (File.Exists(_path))
{
fileMd5 = LoaderUtilities.GetMd5Hash(_path);
}
else
{
if (index >= 0)
{
fileMd5 = LoaderUtilities.GetMd5Hash(_path.Substring(0, index));
}
}
if (fileMd5 == _md5)
{
_status = BackgroundDownloadStatus.Done;
return;
}
try
{
_fileStream = new FileStream(_path, FileMode.OpenOrCreate, FileAccess.Write);
var localFileSize = _fileStream.Length;
_fileStream.Seek(localFileSize, SeekOrigin.Begin);
_curFileSize = localFileSize;
_unityWebRequest = UnityWebRequest.Get(_url);
_unityWebRequest.SetRequestHeader("Range", "bytes=" + localFileSize + "-" + _totalFileSize);
_unityWebRequest.downloadHandler = this;
_unityWebRequest.SendWebRequest();
_status = BackgroundDownloadStatus.Downloading;
}
catch (System.Exception e)
{
TLogger.LogError($"DownloadHandlerFileRange.StartDownload,Exception,{e.StackTrace}");
_status = BackgroundDownloadStatus.Failed;
throw;
}
}
public new void Dispose()
{
if (_status == BackgroundDownloadStatus.Downloading)
{
_status = BackgroundDownloadStatus.Failed;
}
base.Dispose();
if (_fileStream != null)
{
_fileStream.Close();
_fileStream.Dispose();
_fileStream = null;
}
if (_unityWebRequest != null)
{
_unityWebRequest.Abort();
_unityWebRequest.Dispose();
_unityWebRequest = null;
}
}
#region
public float Progress => _totalFileSize == 0 ? 0 : ((float)_curFileSize) / _totalFileSize;
public long TotalSize => _totalFileSize;
public long CurrentSize => _curFileSize;
protected override bool ReceiveData(byte[] data, int dataLength)
{
if (data == null || dataLength <= 0)
{
return false;
}
_fileStream.Write(data, 0, dataLength);
_fileStream.Flush();
_curFileSize += dataLength;
LoadUpdateLogic.Instance.Down_Progress_Action?.Invoke(_curFileSize);
return true;
}
#endregion
#region IEnumerator
public object Current
{
get
{
return null;
}
}
public BackgroundDownloadStatus Status => _status;
public bool MoveNext()
{
if (_status == BackgroundDownloadStatus.Done ||
_status == BackgroundDownloadStatus.Failed ||
_status == BackgroundDownloadStatus.NetworkError)
{
return false;
}
if (_unityWebRequest.isNetworkError || _unityWebRequest.isHttpError)
{
_status = BackgroundDownloadStatus.NetworkError;
}
else if (_unityWebRequest.isDone)
{
_status = BackgroundDownloadStatus.Done;
}
return _status == BackgroundDownloadStatus.Downloading;
}
public void Reset()
{ }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13ec5fbe4e3143e479b624eac7b1431e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,299 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace TEngine
{
/// <summary>
/// 下载状态
/// </summary>
public enum BackgroundDownloadStatus
{
NotBegin = 0, //未开始
Downloading = 1, //下载中
NetworkError = 2, //网络变化
Done = 3, //下载完成
Failed = 4, //下载失败
}
/// <summary>
/// 资源加载过程中的状态
/// </summary>
public enum DownLoadResult
{
StartDownLoad = 0, //开始下载
HeadRequestFail = 1, //请求头失败
DownLoadRequestFail = 2, //现在请求失败
AreadyDownLoaded = 3, //已经下载过而且下载好了
DownLoading = 4, //下载中
NetChanged = 5,
DownLoaded = 6, //下载完成
DownLoadingError = 7,//接收数据的那个过程中出错
HeadRequestError = 8,//获取下载包大小报错
ReceiveNullData = 9,//接受到空数据
DownError = 10,//数据没有接受完但是isDone为true
ReceiveError = 11,//接收数据失败
Md5Wrong = 12,//md5错误
AllDownLoaded = 13//全部下载完成
}
public class DownloadImpl
{
private List<LoadResource> _files;
private string _path;
private Action<int, List<LoadResource>> _callback = null;
private long _totalFileSize = 0;
private long _downLoadedSize = 0;
private long _currentLoadSize = 0;
private float _last_record_time = 0f;
private float _last_record_process = 0f;
private long _speed = 0;
IDownload _downloader = null;
public DownloadImpl(List<LoadResource> files, string path, Action<int, List<LoadResource>> callback)
{
_files = files;
_path = path;
_callback = callback;
_downLoadedSize = 0;
_totalFileSize = 0;
foreach (var item in files)
{
_totalFileSize += item.Size;
}
var dirPath = Path.GetDirectoryName(_path);
if (dirPath != null)
{
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
}
}
public IEnumerator DownLoad()
{
_downLoadedSize = 0;
bool result = false;
foreach (var item in _files)
{
string remoteUrl = item.RemoteUrl + "?" + GameConfig.Instance.GameBundleVersion;
Callback(DownLoadResult.StartDownLoad);
_downloader = GetDownloader(remoteUrl, _path + item.Url, item.Size, item.Md5, this);
_downloader.StartDownload();
yield return _downloader;
if (_downloader != null)
{
_downloader.Dispose();
result = _DealWithDownLoadOk(DownLoadResult.DownLoaded, _downloader.Status, item);
if (result == false)
{
yield break;
}
if (_downloader.Status == BackgroundDownloadStatus.Done)
{
_downLoadedSize += item.Size;
}
}
yield return null;
}
if (result)
{
_DownLoaded(_files);
}
}
void _DownLoaded(List<LoadResource> list)
{
Callback(DownLoadResult.AllDownLoaded, list);
}
bool _DealWithDownLoadOk(DownLoadResult downloadType, BackgroundDownloadStatus status, LoadResource data)
{
string fileLocalPath = _path + data.Url;
if (status == BackgroundDownloadStatus.NetworkError)
{
Callback(DownLoadResult.NetChanged);
return false;
}
if (status == BackgroundDownloadStatus.Failed)
{
LoaderUtilities.DeleteFile(fileLocalPath);
TLogger.LogError("DownloaderImpl._DownLoaded, Load failed");
Callback(DownLoadResult.ReceiveError);
return false;
}
int index = fileLocalPath.IndexOf("_md5_");
var tempMd5 = LoaderUtilities.GetMd5Hash(fileLocalPath);
if (index >= 0)
{
var fileInfo = new FileInfo(fileLocalPath);
string newFilename = fileLocalPath.Substring(0, index);
if (tempMd5 == data.Md5)
{
if (File.Exists(newFilename))
{
File.Delete(newFilename);
}
fileInfo.MoveTo(newFilename);
Callback(downloadType);
}
else
{
if (File.Exists(newFilename))
{
return true;
}
else
{
TLogger.LogError($"DownloaderImpl._DownLoaded, Current md5{tempMd5},Target md5{data.Md5} not match,path:{data.Url}");
LoaderUtilities.DeleteFile(fileLocalPath);
Callback(DownLoadResult.Md5Wrong);
return false;
}
}
}
else
{
if (tempMd5 == data.Md5)
{
Callback(downloadType);
}
else
{
TLogger.LogError($"DownloaderImpl._DownLoaded, Current md5{tempMd5},Target md5{data.Md5} not match,path:{data.Url}");
LoaderUtilities.DeleteFile(fileLocalPath);
Callback(DownLoadResult.Md5Wrong);
return false;
}
}
return true;
}
void Callback(DownLoadResult result, List<LoadResource> files = null)
{
_callback?.Invoke((int)result, files);
}
/// <summary>
/// 文件总大小
/// </summary>
public long FileSize
{
get
{
return _totalFileSize;
}
}
public long DownLoadSize
{
get => _downLoadedSize;
set => _downLoadedSize = value;
}
public long CurrentLoadSize()
{
return _downLoadedSize + _downloader.CurrentSize;
}
/// <summary>
/// 返回下载速度
/// </summary>
public long Speed
{
get
{
if (_downloader == null)
return 0;
if (Time.time - _last_record_time < 0.5)
{
return _speed;
}
float progress = _downloader.Progress;
if (progress == _last_record_process)
{
return _speed;
}
_speed = (long)((progress - _last_record_process) * _downloader.TotalSize / (Time.time - _last_record_time));
_last_record_process = progress;
_last_record_time = Time.time;
return _speed;
}
}
public static IDownload GetDownloader(string url, string path, long totalLength, string md5, DownloadImpl imp)
{
DownloadHandlerFileRange loader = new DownloadHandlerFileRange(url, path, totalLength, md5);
loader.SetImp(imp);
return loader;
}
public bool IsLoading()
{
if (_downloader == null)
return false;
return _downloader.Status == BackgroundDownloadStatus.Downloading;
}
public BackgroundDownloadStatus Statue()
{
if (_downloader == null)
return BackgroundDownloadStatus.NotBegin;
return _downloader.Status;
}
public bool IsNetWorkChanged()
{
if (_downloader == null)
return false;
return _downloader.Status == BackgroundDownloadStatus.NetworkError;
}
public void StopDownLoad()
{
if (_downloader != null)
{
_downloader.Dispose();
}
}
public void Release()
{
_files = null;
_path = "";
_callback = null;
_totalFileSize = 0;
StopDownLoad();
}
}
public interface IDownload : IEnumerator
{
float Progress { get; }
long TotalSize { get; }
long CurrentSize { get; }
BackgroundDownloadStatus Status { get; }
void Dispose();
void StartDownload();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a6103e1339c9a164ba4dc41f33be1cf3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TEngine
{
public enum GameStatus
{
First = 0,
AssetLoad = 1
}
public struct LoadResource
{
public string Url;//资源名称
public string Md5;//资源的md5码
public long Size; //资源大小(字节为单位)
public string RemoteUrl;//服务器地址
}
public class LoadData
{
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c685f02226943e946853424f953dcfee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
using System;
using System.IO;
namespace TEngine
{
public class LoadUpdateLogic
{
private static LoadUpdateLogic _instance;
public Action<int> Download_Complete_Action = null;
public Action<long> Down_Progress_Action = null;
public Action<bool, GameStatus> _Unpacked_Complete_Action = null;
public Action<float, GameStatus> _Unpacked_Progress_Action = null;
public static LoadUpdateLogic Instance
{
get
{
if (_instance == null)
_instance = new LoadUpdateLogic();
return _instance;
}
}
}
public class LoadMgr : TSingleton<LoadMgr>
{
/// <summary>
/// 资源版本号
/// </summary>
public string LatestResId { get; set; }
private Action _startGameEvent;
private int _curTryCount;
private const int MaxTryCount = 3;
private bool _connectBack;
private bool _needUpdate = false;
public LoadMgr()
{
_curTryCount = 0;
_connectBack = false;
_startGameEvent = null;
}
public void StartLoadInit(Action onUpdateComplete)
{
#if RELEASE_BUILD || _DEVELOPMENT_BUILD_
StartLoad(() => { FinishCallBack(onUpdateComplete); });
#else
onUpdateComplete();
#endif
}
/// <summary>
/// 开启热更新逻辑
/// </summary>
/// <param name="action"></param>
public void StartLoad(Action action)
{
_startGameEvent = action;
_connectBack = false;
_curTryCount = 0;
RequestVersion();
}
private void FinishCallBack(Action callBack)
{
GameConfig.Instance.WriteVersion(LatestResId);
if (_needUpdate)
{
callBack();
}
else
{
callBack();
}
}
/// <summary>
/// 请求热更数据
/// </summary>
private void RequestVersion()
{
if (_connectBack)
{
return;
}
_curTryCount++;
if (_curTryCount > MaxTryCount)
{
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56d713df43471c642a913a349900d028
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace TEngine
{
public class LoaderUtilities
{
/// <summary>
/// 删除文件
/// </summary>
/// <param name="filePath">文件路径</param>
public static void DeleteFile(string filePath)
{
try
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
catch (Exception e)
{
TLogger.LogError(e.ToString());
}
}
/// <summary>
/// 获取文件的md5码
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string GetMd5Hash(string fileName)
{
if (!File.Exists(fileName))
{
TLogger.LogWarning($"not exit file,path:{fileName}");
return string.Empty;
}
try
{
using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2"));
}
return sb.ToString();
}
}
catch (Exception ex)
{
TLogger.LogError("GetMD5Hash() fail,error:" + ex.Message);
return string.Empty;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 186c876c2ef7f2f4db981fd403e22ce7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: