适用于下载较大的文件, 弱网环境, 及 网络抖动 的情况.
http 下载时, 一旦网络遇到断掉 (网络切换) 时, 就会下载失败. 但是 http 中可以设置 请求头 中 Range
值, 开决定下载文件流的起点, 断点续传就是通过这个实现.
原理:
下载文件 a.txt 时, 先将其下载为一个临时文件 a.txt 保存为 a.txt.temp (随便起名), 遇到网络抖动断掉时, 再次下载前, 先读取 a.txt.temp 的文件大小, 然后在 http 请求头 中设置 Range
值的起点值为 a.txt.temp 的文件大小. 往复循环直至最后下载完成, 再讲 a.txt.temp 重命名为 a.txt.
附: 如果下载的文件时自己的文件, 加上一个 md5 值去校验一下下载的文件是否完整.
下面直接丢代码, lua + csharp
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using LuaInterface;
using UnityEngine;
using UnityEngine.Networking;
public class ResumeDownHandler : DownloadHandlerScript {
private FileStream fs;
// 要下载的文件总长度
private int mContentLength = 0;
public int ContentLength {
get { return mContentLength; }
}
private int mDownedLength = 0;
public int DownedLength {
get { return mDownedLength; }
}
private string mFileName;
public string FileName {
get { return mFileName; }
}
public string FileNameTemp {
get { return mFileName + ".temp"; }
}
private string mSavePath = "";
public string FilePath {
get { return mSavePath; }
}
LuaFunction luaProgressFn = null;
LuaFunction luaCompleteFn = null;
public void RegProgress(LuaFunction fn) {
luaProgressFn = fn;
}
public void RegComplete(LuaFunction fn) {
luaCompleteFn = fn;
}
// 初始化下载句柄,定义每次下载的数据上限为 512 kb
public ResumeDownHandler(string filePath) : base(new byte[1024 * 512]) {
mSavePath = filePath.Replace('\\', '/');
mFileName = Path.GetFileName(mSavePath);
string dirPath = System.IO.Path.GetDirectoryName(mSavePath);
if (!Directory.Exists(dirPath)) {
Directory.CreateDirectory(dirPath);
}
this.fs = new FileStream(mSavePath + ".temp", FileMode.Append, FileAccess.Write);
mDownedLength = (int) fs.Length;
}
// 当从网络接收数据时的回调,每帧调用一次, 返回true表示当下载正在进行,返回false表示下载中止
protected override bool ReceiveData(byte[] data, int dataLength) {
if (data == null || data.Length == 0) {
return false;
}
fs.Write(data, 0, dataLength);
mDownedLength += dataLength;
if (luaProgressFn != null) {
luaProgressFn.Call(mDownedLength, mContentLength);
}
return true;
}
public void OnComplete(int code) {
if (luaCompleteFn != null) {
luaCompleteFn.Call(code, mDownedLength, mContentLength);
}
}
// 下载完成
protected override void CompleteContent() {
string TempFilePath = fs.Name; //临时文件路径
OnDispose();
if (File.Exists(TempFilePath)) {
Utils.MoveFile(TempFilePath, mSavePath);
}
}
public void OnDispose() {
if (fs != null) {
fs.Close();
fs.Dispose();
}
}
protected override void ReceiveContentLength(int contentLength) {
mContentLength = contentLength + mDownedLength;
}
}
--======================================================================
-- descrip: 断点续传 下载
--======================================================================
local CResumeDownManager = class()
CResumeDownManager.__name = "CResumeDownManager"
local table = table
local tostring = tostring
local pairs = pairs
function CResumeDownManager.Init(self)
self._requestTbl = {}
end
function CResumeDownManager.StartDownCnt(self, url, savePath, completeFn, progressFn, cnt)
local wrapFn
if completeFn and cnt then
wrapFn = function(isSucc, ...)
if not isSucc and cnt > 1 then
cnt = cnt - 1
local isOk = self:StartDown(url, savePath, wrapFn, progressFn)
if not isOk then -- 防止连续调用
cnt = 0
end
else
completeFn(isSucc, ...)
end
end
end
self:StartDown(url, savePath, wrapFn, progressFn)
end
function CResumeDownManager.StartDown(self, url, savePath, completeFn, progressFn)
if self._requestTbl[url] then
return false
end
if table.count(self._requestTbl) == 0 then
UpdateBeat:Add(self.Update, self)
end
local rdHdl = ResumeDownHandler.New(savePath)
if completeFn then
rdHdl:RegComplete(function(code, downLen, totalLen)
-- 跳几帧在回调回去
gTimeMgr:SetTimeOut(0.2, function ()
local isSucc = code == 206 and totalLen > 0 and downLen == totalLen
completeFn(isSucc, downLen, totalLen, url, savePath)
end)
end)
end
if progressFn then
rdHdl:RegProgress(progressFn)
end
local request = UnityEngine.Networking.UnityWebRequest.Get(url)
self._requestTbl[url] = request
request.chunkedTransfer = true
request.disposeDownloadHandlerOnDispose = true
request:SetRequestHeader("Range", string.formatExt("bytes={0}-", rdHdl.DownedLength))
request.downloadHandler = rdHdl
request.useHttpContinue = true
request:SendWebRequest()
return true
end
function CResumeDownManager.StopDown(self, url)
local request = self._requestTbl[url]
if not request then
return
end
self._requestTbl[url] = nil
request.downloadHandler:OnDispose()
request:Abort()
request:Dispose()
if table.count(self._requestTbl) == 0 then
UpdateBeat:Remove(self.Update, self)
end
end
function CResumeDownManager.StopAll(self)
local keyTbl = table.keys(self._requestTbl)
for _,url in ipairs(keyTbl) do
self:StopDown(url)
end
end
function CResumeDownManager.Update(self)
local rmTbl
for url,request in pairs(self._requestTbl) do
if request.isDone then
request.downloadHandler:OnComplete(Utils.LongToInt(request.responseCode))
if not rmTbl then
rmTbl = {}
end
table.insert(rmTbl, url)
end
end
if rmTbl then
for _,url in ipairs(rmTbl) do
self:StopDown(url)
end
rmTbl = nil
end
end
return CResumeDownManager
local function testUpgrade()
local dlPath = gPlatformMgr:CallLuaFunc("GetApkPath")
local url = "http://192.168.1.200:8080/download/hello_39b001d018b558d96ff15eccf3777ef1.apk";
local function progressFn(downLen, totalLen)
end
local function completeFn(isSucc, downLen, totalLen, url, savePath)
gLog("--- completeFn, isSucc:{0}, downLen:{1}, totalLen:{2}", isSucc, downLen, totalLen)
if isSucc then
else
end
end
gResumeDownMgr:StartDownCnt(url, dlPath, completeFn, progressFn, 5) -- 最多尝试 5 次断点续传
end