下载器断点下载 — 方便游戏强更
Unity 版本的游戏内置下载器,设计目标是嵌入游戏客户端,支持强制更新时的安装包下载。
支持断点续传、HTTP 重定向、MD5 完整性校验、自动重试,仅需 Android 平台(iOS 直接跳转 App Store)。
设计概览
断点续传
HTTP Range
利用 AddRange 从已下载位置继续,服务器返回 206 Partial Content
完整性校验
MD5
下载前后均校验 MD5,失败则删除重下
容错机制
10 次重试
异常/超时/校验失败自动递归重试,超限报 Error
状态机
None
→
Checking
→
Verifying
→
Downloading
→
Verifying
→
OK
| 状态 | 含义 | 触发时机 |
|---|---|---|
None | 未启动 / 已终止 | 初始状态 或 Terminal() 调用后 |
Checking | 预处理中 | 创建目录、准备文件、HTTP 请求前 |
Verifying | MD5 校验中 | 下载前检查已有文件 / 下载后校验 |
Downloading | 数据接收中 | 进入 Read 循环后 |
OK | 下载完成,可安装 | 文件大小匹配 + MD5 校验通过 |
Error | 失败 | 重试超过 10 次 |
核心流程
入口 — StartDownload()
主线程调用
终止旧线程
→
构造 DownloadParam
→
try_times = 0
→
new Thread(ThreadFunc).Start()
ThreadFunc() — 下载线程主逻辑
完整下载流程
try_times++ — 超过 10 次则 Status = Error, return
↓
文件已存在?→ 校验 MD5 → 通过则直接 OK
↓
MD5 不匹配或文件不存在
↓
enable_break_point == false? → 删除旧文件
↓
打开/创建文件,Seek 到末尾(current_size)
↓
创建 HttpWebRequest,设置 AddRange(current_size)
↓
发送请求,获取 HttpWebResponse
↓
分支处理 HTTP 状态码
HTTP 状态码分支
302 Redirect
读取 Location header
关闭当前连接和文件
param.url = 新地址
param.IsRedirect = true
递归调用 ThreadFunc(param)
206 Partial Content
服务器支持断点续传
继续从 current_size 写入
进入 Read 循环
200 OK(非 206)
服务器不支持 Range
删除已有文件,从头开始
重新创建文件
进入 Read 循环
408 / 504 Timeout
关闭连接和文件
直接 return(保留文件,下次续传)
其他错误码
关闭连接和文件
直接 return
数据接收循环
Read Loop — 10KB 缓冲区
Status = Downloading
↓
while (read_size = stream.Read(buf, 0, 10240)) > 0
↓
file.Write(buf, 0, read_size) + file.Flush()
↓
CurrentSize += read_size(供 UI 进度显示)
↓
循环结束
↓
比较 current_size + down_size vs TotalSize
下载后校验
downloaded < TotalSize
文件不完整
保留文件(下次断点续传)
递归重试 ThreadFunc()
downloaded > TotalSize
文件损坏(超过预期大小)
删除整个目录
递归重试 ThreadFunc()
downloaded == TotalSize
大小匹配
MD5 校验文件
通过 → OK,失败 → 删除重试
断点续传原理
Resume 机制
检查本地文件是否存在 → 获取 file.Length
↓
file.Seek(current_size, SeekOrigin.Begin)
↓
request.AddRange((int)current_size)
↓
服务器返回 206 → 仅传输剩余部分
↓
TotalSize = ContentLength + current_size
↓
从断点位置继续写入文件
降级处理:如果服务器返回 200 而非 206,说明不支持 Range 请求。此时删除已有文件,从头开始完整下载。
容错与重试
| 异常场景 | 处理策略 | 是否保留文件 |
|---|---|---|
| 网络超时 | return,等待外部重新触发 | 是(支持续传) |
| 下载不完整(size < total) | 递归 ThreadFunc() 重试 | 是(续传) |
| 文件过大(size > total) | 删除后递归重试 | 否 |
| MD5 校验失败 | 删除后递归重试 | 否 |
| HTTP 重定向 | 更新 URL 递归(不计入重试次数) | 是 |
| Exception 异常 | 关闭文件后递归重试 | 是(续传) |
| 重试超过 10 次 | Status = Error,停止 | 是 |
API 接口
| 类/方法 | 说明 |
|---|---|
MAX_VERSION | 版本描述:URL、MD5、FileSize、RollBackUrl |
StartDownload(url, path, name, md5, breakpoint) | 启动下载线程(主线程调用) |
Terminal() | 中止下载,Abort 线程,重置状态 |
GetLeftSize(path, name, total) | 计算剩余需下载的字节数(UI 显示用) |
StartInstallApk(path) | 调用 Native API 安装 APK(主线程) |
_Status | 静态变量,UI 轮询读取当前状态 |
CurrentSize / TotalSize | 静态变量,UI 计算下载进度百分比 |
完整实现
InstallerDownloader.cs
C# / Unity
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Net;
using UnityEngine;
using System.IO;
using System;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Collections.Specialized;
using System.Reflection;
namespace Patches2
{
//just android suport IOS直接跳转app-store 即可
//用于游戏内置的下载器 下载
public class MAX_VERSION
{
public string Version = "";
public string MD5 = "";
public int FileSize = 5000;//这个file size 只是给玩家看的 实际下载 不用到该变量
public string Url = "";
public string RollBackUrl = "";//下载回滚页面 比如下载fatal error 会直接opel url
//this data is legal or illegal
public bool IsLegal()
{
if (Version != null && MD5 != null && FileSize > 0 && Url != null)
{
if (Version.Length >= 5 && MD5.Length > 0 && Url.Length > 5)
{
return true;
}
}
return false;
}
}
//安装包下载器 支持断点续传 支持游戏内置 下载 下载完成后 调用native api进行安装
//其实只需要android 平台即可 IOS直接跳转 app-store
public class InstallerDownloader
{
//下载的 apk 存放名字
public const string ApkName = "hcr.apk";
//调用该函数 开始安装 只能在 unity主线程调用
public static void StartInstallApk(string full_file_path)
{
#if UNITY_ANDROID
NativeApi.InstallApk(full_file_path);
#endif
}
public enum Status
{
None = 0,//未知
Error, // 下载错误 或者超时
Downloading, // 下载中 外部可以读取静态变量 来显示下载进度
Checking,// 处理 下载前逻辑中
Verifying, // 校验文件中 可能是下载前 可能是下载后
OK,//下载完成 可以开始执行安装操作了
}
public class DownloadParam
{
public string url;
public string path_to_save;
public string file_name;
public bool enable_break_point;//是否开启断点续传功能
public string md5;//md5 用于校验安装包的完整性
public bool IsRedirect = false;//是否是重定向 是重定向的话 会忽略 失败次数统计
}
Thread t_download = null;
public void Terminal()
{
if (t_download != null)
{
try
{
t_download.Abort();
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
t_download = null;
}
CurrentSize = 0;
TotalSize = 0;
_Status = Status.None;
}
// 需要下载的大小 内部减去了 断点续传的 部分
public static int GetLeftSize(string path_to_save, string file_name, int total_size)
{
string full_path = path_to_save + "/" + file_name;
if (File.Exists(full_path))
{
var info = new FileInfo(full_path);
if (info != null)
{
if (total_size < info.Length)
{
try
{
File.Delete(full_path);
}
catch (Exception e)
{
}
return total_size;
}
else
{
return total_size - (int)info.Length;
}
}
}
return total_size;
}
//改变量只能在 unity主线程调用
public static string InstallRootDir
{
get
{
#if !UNITY_EDITOR
return Application.persistentDataPath + "/Installer";
#else
return "Installer";
#endif
}
}
public void StartDownload(string url, string path_to_save, string file_name, string md5, bool enable_break_point = true)
{
if (t_download != null)
{
try
{
t_download.Abort();
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
}
t_download = new Thread(new ParameterizedThreadStart(ThreadFunc));
IsThreadRunning = true;
DownloadParam _param = new DownloadParam
{
url = url,
path_to_save = path_to_save,
file_name = file_name,
enable_break_point = enable_break_point,
md5 = md5
};
try_times = 0;
_Status = Status.Checking;
t_download.Start(_param);
}
bool IsThreadRunning = false;
public static Status _Status = Status.None;
public int HttpRetCode = 0;
public static int TotalSize = 0; // 总大小
public static int CurrentSize = 0; // 当前大小 可用于进度显示
int try_times = 0;
private void DownloadOK()
{
_Status = Status.OK;
//开始安装流程 调用native api 开始安装
Debug.LogWarning("download ok");
}
private void ThreadFunc(object _param_call)
{
CurrentSize = 0;
TotalSize = 0;
Debug.LogWarning("start to download");
DownloadParam _param = _param_call as DownloadParam;
if (_param == null)
{
IsThreadRunning = false;
_Status = Status.Error;
return;
}
if (_param.IsRedirect)
{
//重定向的话 不处理 失败 重试次数
_param.IsRedirect = false;
}
else
{
//重新下载 或者 分批下载 都会重试计次
++try_times;
}
if (try_times > 10)
{
//fatal error or net error
_Status = Status.Error;
return;
}
try
{
_Status = Status.Verifying;
if (File.Exists(_param.path_to_save + "/" + _param.file_name))
{
//文件存在的话 检查一下 是否成功 不成功的话 才开始下载
_Status = Status.Verifying;
if (string.IsNullOrEmpty(_param.md5) == false && _param.md5 == Patches.MD5Code.GetMD5HashFromFile(_param.path_to_save + "/" + _param.file_name))
{
//ok
_Status = Status.OK;
this.DownloadOK();
return;
}
else
{
//md5 verify error 需要处理下载 (or断点下载) 下载完成后 才 再次校验
}
}
}
catch (Exception e)
{
}
_Status = Status.Checking;
if (_param.enable_break_point == false)
{
//无需断点下载 尝试暴力删除文件
try
{
Directory.Delete(_param.path_to_save, true);
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
}
if (Directory.Exists(_param.path_to_save) == false)
{
try
{
Directory.CreateDirectory(_param.path_to_save);
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
}
//先打开文件
Stream file = null;
using (file = (File.Exists(_param.path_to_save + "/" + _param.file_name)) ? File.OpenWrite(_param.path_to_save + "/" + _param.file_name) : file = File.Create(_param.path_to_save + "/" + _param.file_name))
{
/* try
{
if (File.Exists(_param.path_to_save + "/" + _param.file_name))
{
file = File.OpenWrite(_param.path_to_save + "/" + _param.file_name);
}
else
{
file = File.Create(_param.path_to_save + "/" + _param.file_name);
}
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}*/
try
{
long current_size = file.Length;
if (current_size > 0)
{
file.Seek(current_size, SeekOrigin.Begin);
}
HttpWebRequest request = null;
//如果是发送HTTPS请求
if (_param.url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
{
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult);
request = (HttpWebRequest)WebRequest.Create(_param.url);
}
else
{
request = (HttpWebRequest)WebRequest.Create(_param.url);
}
request.ProtocolVersion = new System.Version(1, 1);
if (current_size > 0)
{
request.AddRange((int)current_size);
CurrentSize = (int)current_size;
}
HttpWebResponse response = null;
request.Timeout = 5000;
request.ReadWriteTimeout = 5000;
request.Method = "GET";
request.KeepAlive = false;
response = (HttpWebResponse)request.GetResponse();
var HttpRetCode = response.StatusCode;
Debug.Log("InstallDownloader http " + HttpRetCode);
if (HttpRetCode == HttpStatusCode.Redirect)
{
//重定向
_param.url = response.Headers["Location"].Trim();
response.Close();
response = null;
request.Abort();
request = null;
try
{
file.Close();
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
Debug.Log("Redirect " + _param.url);
_param.IsRedirect = true;
ThreadFunc(_param);
return;
}
else if (HttpRetCode == HttpStatusCode.GatewayTimeout || HttpRetCode == HttpStatusCode.RequestTimeout)
{
//net error
response.Close();
response = null;
request.Abort();
request = null;
try
{
file.Close();
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
Debug.Log("timeout");
return;
}
else if (HttpRetCode == HttpStatusCode.OK || HttpRetCode == HttpStatusCode.Created || HttpRetCode == HttpStatusCode.Accepted || HttpRetCode == HttpStatusCode.NonAuthoritativeInformation || HttpRetCode == HttpStatusCode.NoContent || HttpRetCode == HttpStatusCode.ResetContent || HttpRetCode == HttpStatusCode.PartialContent)
{
if (HttpRetCode != HttpStatusCode.PartialContent)
{
//如果不是断点下载 或者服务器不支持 那么需要 重新下载完整文件
try
{
file.Close();
file = null;
}
catch (Exception e)
{
}
try
{
Directory.Delete(_param.path_to_save, true);
}
catch (Exception e)
{
}
try
{
Directory.CreateDirectory(_param.path_to_save);
}
catch (Exception e)
{
}
file = File.Create(_param.path_to_save + "/" + _param.file_name);
}
}
else
{
//req error
response.Close();
response = null;
request.Abort();
request = null;
try
{
file.Close();
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
Debug.LogWarning("error");
return;
}
//web 请求处理完成了 开始处理 接受数据了
long total_len = response.ContentLength;
TotalSize = (int)total_len + (int)current_size;
if (current_size < TotalSize)
{
if (current_size > 0)
{
// request.AddRange((int)current_size);
CurrentSize = (int)current_size;
}
Stream web = request.GetResponse().GetResponseStream();
byte[] _cache = new byte[10240]; // 10kb
int down_size = 0;
int read_size = web.Read(_cache, 0, 10240);
int total_read_size = 0;
_Status = Status.Downloading;
while (read_size > 0)
{
_Status = Status.Downloading;
file.Write(_cache, 0, read_size);
total_read_size += read_size;
down_size += read_size;
CurrentSize += read_size;
// Debug.LogError("download ing " + CurrentSize + " " + TotalSize);
file.Flush();
read_size = web.Read(_cache, 0, 10240);
}
file.Close();
file = null;
web.Close();
web = null;
response.Close();
response = null;
request.Abort();
request = null;
if (current_size + down_size < TotalSize)
{
//下载文件 长度不够 需要重新下载
Debug.LogWarning("file is smaller will re-try");
ThreadFunc(_param);
return;
}
else if (current_size + down_size > TotalSize)
{
//下载的长度 超过了 实际长度 文件已经损坏 重新下载把
try
{
Directory.Delete(_param.path_to_save, true);
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
Debug.LogWarning("file is bigger will delete and re-download");
ThreadFunc(_param);
return;
}
else
{
//下载文件成功 开始校验MD5
_Status = Status.Verifying;
if (string.IsNullOrEmpty(_param.md5) == false && _param.md5 == Patches.MD5Code.GetMD5HashFromFile(_param.path_to_save + "/" + _param.file_name))
{
//ok
}
else
{
//md5 verify error 尝试重新下载
try
{
file.Close();
file = null;
response.Close();
response = null;
request.Abort();
request = null;
}
catch (Exception e)
{
}
try
{
Directory.Delete(_param.path_to_save, true);
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
ThreadFunc(_param);
return;
}
_Status = Status.OK;
}
}
else if (current_size == total_len)
{//当前文件和 服务器文件大小一样 默认为 下载完成 需要校验MD5
try
{
file.Close();
file = null;
response.Close();
response = null;
request.Abort();
request = null;
}
catch (Exception e)
{
}
Debug.LogWarning("file is req just done");
_Status = Status.Verifying;
if (string.IsNullOrEmpty(_param.md5) == false && _param.md5 == Patches.MD5Code.GetMD5HashFromFile(_param.path_to_save + "/" + _param.file_name))
{
//ok
}
else
{
//md5 verify error 尝试重新下载
try
{
file.Close();
file = null;
response.Close();
response = null;
request.Abort();
request = null;
}
catch (Exception e)
{
}
try
{
Directory.Delete(_param.path_to_save, true);
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
ThreadFunc(_param);
return;
}
_Status = Status.OK;
}
else
{
//当前文件超过了 大小 需要重新下载
try
{
Directory.Delete(_param.path_to_save, true);
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
Debug.LogWarning("file is bigger will delete and re-download 2");
try
{
file.Close();
file = null;
response.Close();
response = null;
request.Abort();
request = null;
}
catch (Exception e)
{
}
ThreadFunc(_param);
return;
}
//走到了这里 都当作文件下载成功 并且校验成功 可以开始安装了
_Status = Status.OK;
this.DownloadOK();
}
catch (Exception ee)
{
//整个下载流程出了异常错误
Debug.LogWarning(ee.Message);
_Status = Status.Checking;
try
{
if (file != null)
{
file.Close();
file = null;
}
}
catch (Exception e)
{
Debug.LogWarning(e.Message);
}
ThreadFunc(_param);
return;
}
}
}
private static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
return true;
}
}
}