对于一个网络游戏来说,下载网络上面的文件一定是必不可少的一个功能,例如更新资源包。在Unity中,我们可以用系统的WWW或者HttpWebRequest来实现文件的下载。其中有些较大的文件下载需要断点续传的功能(即下载了一部分突然中断下载后,再次下载直接从上次下载的地方继续下载,而不是重新下载),需要使用HttpWebRequest。
这一篇我们主要就讲一讲这两种下载方式的实现(主要都是代码)。
首先先创建一个基类,里面存放下载需要的一些数据,例如文件url,下载存放路径等等。
public abstract class DownloadItem {
///
/// 网络资源url路径
///
protected string m_srcUrl;
///
/// 资源下载存放路径,不包含文件名
///
protected string m_savePath;
///
/// 文件名,不包含后缀
///
protected string m_fileNameWithoutExt;
///
/// 文件后缀
///
protected string m_fileExt;
///
/// 下载文件全路径,路径+文件名+后缀
///
protected string m_saveFilePath;
///
/// 原文件大小
///
protected long m_fileLength;
///
/// 当前下载好了的大小
///
protected long m_currentLength;
///
/// 是否开始下载
///
protected bool m_isStartDownload;
public bool isStartDownload {
get {
return m_isStartDownload;
}
}
public DownloadItem(string url, string path) {
m_srcUrl = url;
m_savePath = path;
m_isStartDownload = false;
m_fileNameWithoutExt = Path.GetFileNameWithoutExtension(m_srcUrl);
m_fileExt = Path.GetExtension(m_srcUrl);
m_saveFilePath = string.Format("{0}/{1}{2}", m_savePath, m_fileNameWithoutExt, m_fileExt);
}
///
/// 开始下载
///
/// 下载完成回调
public virtual void StartDownload(Action callback = null) {
if(string.IsNullOrEmpty(m_srcUrl) || string.IsNullOrEmpty(m_savePath)) {
return;
}
//若存放目录不存在则创建目录
FileTool.CreateDirectory(m_saveFilePath);
}
///
/// 获取下载进度
///
/// 进度,0-1
public abstract float GetProcess();
///
/// 获取当前下载了的文件大小
///
/// 当前文件大小
public abstract long GetCurrentLength();
///
/// 获取要下载的文件大小
///
/// 文件大小
public abstract long GetLength();
public abstract void Destroy();
}
www的方式下载很简单,等www读取好后,直接使用www.bytes去生成文件即可。这种方式文件是一次性生成的,适合小资源的下载
///
/// WWW的方式下载
///
public class WWWDownloadItem : DownloadItem {
WWW m_www;
public WWWDownloadItem(string url, string path) : base(url, path) {
}
public override void StartDownload(Action callback = null) {
base.StartDownload();
UICoroutine.instance.StartCoroutine(Download(callback));
}
IEnumerator Download(Action callback = null) {
m_www = new WWW(m_srcUrl);
m_isStartDownload = true;
yield return m_www;
//WWW读取完成后,才开始往下执行
m_isStartDownload = false;
if(m_www.isDone) {
byte[] bytes = m_www.bytes;
//创建文件
FileTool.CreatFile(m_saveFilePath, bytes);
} else {
Debug.Log("Download Error:" + m_www.error);
}
if(callback != null) {
callback();
}
}
public override float GetProcess() {
if(m_www != null) {
return m_www.progress;
}
return 0;
}
public override long GetCurrentLength() {
if(m_www != null) {
return m_www.bytesDownloaded;
}
return 0;
}
public override long GetLength() {
return 0;
}
public override void Destroy() {
if(m_www != null) {
m_www.Dispose();
m_www = null;
}
}
}
这种方式下载,我们采用的是边读取边生成对应文件,适合较大文件下载。关于断点续传的处理,我们下载的时候,先根据读取的内容生成一个临时文件,当全部下载好后,才将这个临时文件转换成正式的文件名。若中途下载中断,已下载好的那部分文件依据在对应目录下,我们下次下载可以直接在上次下载的临时文件后添加内容。
所以下载逻辑就是,每次下载先判断是否有对应的临时文件,若没有则是第一次下载,从初始位置读取下载,若有该文件,则读取该文件信息,获取文件大小当之后的读取下载的偏移量,从当前位置开始下载,直至下载完成。
///
/// HTTP的方式下载,支持断点续传
///
public class HttpDownloadItem : DownloadItem {
///
/// 临时文件后缀名
///
string m_tempFileExt = ".temp";
///
/// 临时文件全路径
///
string m_tempSaveFilePath;
public HttpDownloadItem(string url, string path) : base(url, path) {
m_tempSaveFilePath = string.Format("{0}/{1}{2}", m_savePath, m_fileNameWithoutExt, m_tempFileExt);
}
public override void StartDownload(Action callback = null) {
base.StartDownload();
UICoroutine.instance.StartCoroutine(Download(callback));
}
IEnumerator Download(Action callback = null) {
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(m_srcUrl);
request.Method = "GET";
FileStream fileStream;
if(File.Exists(m_tempSaveFilePath)) {
//若之前已下载了一部分,继续下载
fileStream = File.OpenWrite(m_tempSaveFilePath);
m_currentLength = fileStream.Length;
fileStream.Seek(m_currentLength, SeekOrigin.Current);
//设置下载的文件读取的起始位置
request.AddRange((int)m_currentLength);
} else {
//第一次下载
fileStream = new FileStream(m_tempSaveFilePath, FileMode.Create, FileAccess.Write);
m_currentLength = 0;
}
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream stream = response.GetResponseStream();
//总的文件大小=当前需要下载的+已下载的
m_fileLength = response.ContentLength + m_currentLength;
m_isStartDownload = true;
int lengthOnce;
int bufferMaxLength = 1024 * 20;
while(m_currentLength < m_fileLength) {
byte[] buffer = new byte[bufferMaxLength];
if(stream.CanRead) {
//读写操作
lengthOnce = stream.Read(buffer, 0, buffer.Length);
m_currentLength += lengthOnce;
fileStream.Write(buffer, 0, lengthOnce);
} else {
break;
}
yield return null;
}
m_isStartDownload = false;
response.Close();
stream.Close();
fileStream.Close();
//临时文件转为最终的下载文件
File.Move(m_tempSaveFilePath, m_saveFilePath);
if(callback != null) {
callback();
}
}
public override float GetProcess() {
if(m_fileLength > 0) {
return Mathf.Clamp((float)m_currentLength / m_fileLength, 0, 1);
}
return 0;
}
public override long GetCurrentLength() {
return m_currentLength;
}
public override long GetLength() {
return m_fileLength;
}
public override void Destroy() {
}
}
上述代码用到了两个自定义的方法FileTool.CreateDirectory()和FileTool.CreatFile(),内容很简单,如下
public class FileTool {
///
/// 创建目录
///
/// 需要创建的目录路径
public static void CreateDirectory(string filePath) {
if(!string.IsNullOrEmpty(filePath)) {
string dirName = Path.GetDirectoryName(filePath);
if(!Directory.Exists(dirName)) {
Directory.CreateDirectory(dirName);
}
}
}
///
/// 创建文件
///
/// 文件路径
/// 文件内容
public static void CreatFile(string filePath, byte[] bytes) {
FileInfo file = new FileInfo(filePath);
Stream stream = file.Create();
stream.Write(bytes, 0, bytes.Length);
stream.Close();
stream.Dispose();
}
}
public class DownloadDemo : MonoBehaviour {
DownloadItem m_item;
string testScrUrl = "http://dlsw.baidu.com/sw-search-sp/soft/ca/13442/Thunder_dl_7.9.42.5050.1449557123.exe";
int count = 0;
void Start() {
Debug.Log(Application.persistentDataPath);
//m_item = new WWWDownloadItem(testScrUrl, Application.persistentDataPath);
//m_item.StartDownload(DownloadFinish);
m_item = new HttpDownloadItem(testScrUrl, Application.persistentDataPath);
m_item.StartDownload(DownloadFinish);
}
void Update() {
count++;
if(count % 20 == 0) {
if(m_item != null && m_item.isStartDownload) {
Debug.Log("下载进度------" + m_item.GetProcess() + "------已下载大小---" + m_item.GetCurrentLength());
}
}
}
void DownloadFinish() {
Debug.Log("DownloadFinish!!!");
}
}
补充:由于个人失误,忘记添加了UICoroutine相关的代码,深表歉意。UICoroutine在这个例子中的作用其实就是场景中新建一个GameObject,专门用来启用协程,因为前面的几个类没有继承于MonoBehaviour,所以无法直接调用StartCoroutine方法
public class UICoroutine : MonoBehaviour {
private static UICoroutine mInstance = null;
public static UICoroutine uiCoroutine {
get {
if(mInstance == null) {
GameObject go = new GameObject();
if(go != null) {
go.name = "_UICoroutine";
go.AddComponent();
} else {
Debug.LogError("Init UICoroutine faild. GameObjet can not be null.");
}
}
return mInstance;
}
}
void Awake() {
DontDestroyOnLoad(gameObject);
mInstance = this;
}
void OnDestroy() {
mInstance = null;
}
}