闲着没事试着写写,本来想应该挺简单的,但一写就折腾大半天。
Http要实现多线程现在需要WebHost对HttpHeader中Range支持,有些资源不支持Range头就必须顺序下载。
协议参考 rfc2616:http://www.ietf.org/rfc/rfc2616.txt
大概步骤:
1.检测Range支持,同时获取长度
2. 通过长度创建一个相当大小的文件
3. 创建线程组
4. 分隔文件
5. 各个线程获取一个文件块任务(比较小),读取后放在内存中,完成块后写入文件,再领取下一个文件块任务
6. 直到全部块都完成
*如果将完成进度实时持久化,启动的时候加载进度,那么就能断的续传了。
线程组用异步IO 代替,为了简便任务用Stack管理块得分配,失败后再次push回,按照大概的顺序关系下载,直到栈空结束下载。实现比较简单,实际网络情况复杂,还有很多功能需要完善,不多说直接贴代码吧:
class ConcurrentDownLoader
{
public Uri ToUri { get ; set ; }
public string SavePath { get ; set ; }
private int _contentLength;
public int ContentLength { get { return _contentLength;} }
private bool _acceptRanges;
public bool AcceptRanges { get { return _acceptRanges; } }
private int _maxThreadCount;
public int MaxThreadCount { get { return _maxThreadCount;} }
public event Action OnDownLoaded;
public event Action < Exception > OnError;
private FileStream saveFileStream;
private object _syncRoot = new object ();
public ConcurrentDownLoader(Uri uri, string savePath)
: this (uri, savePath, 5 )
{
}
public ConcurrentDownLoader(Uri uri, string savePath, int maxThreadCount)
{
ToUri = uri;
SavePath = savePath;
_maxThreadCount = maxThreadCount;
ServicePointManager.DefaultConnectionLimit = 100 ;
}
public void DownLoadAsync()
{
_acceptRanges = CheckAcceptRange();
if ( ! _acceptRanges) // 可以使用顺序下载
throw new Exception( " resource cannot allow seek " );
// create a File the same as ContentLength
if (File.Exists(SavePath))
throw new Exception( string .Format( " SavePath:{0} already exists " ,SavePath));
saveFileStream = File.Create(SavePath);
saveFileStream.SetLength(ContentLength);
saveFileStream.Flush();
PartManager pm = new PartManager( this );
pm.OnDownLoaded += () =>
{
saveFileStream.Close();
if (OnDownLoaded != null ) ;
OnDownLoaded();
};
pm.OnError += (ex) =>
{
saveFileStream.Close();
if (OnError != null )
OnError(ex);
};
pm.Proc();
}
public void WriteFile(DPart part, MemoryStream mm)
{
lock (_syncRoot)
{
try
{
mm.Seek( 0 , SeekOrigin.Begin);
saveFileStream.Seek(part.BeginIndex, SeekOrigin.Begin);
byte [] buffer = new byte [ 4096 ];
int count = 0 ;
while ((count = mm.Read(buffer, 0 , buffer.Length)) > 0 )
saveFileStream.Write(buffer, 0 , count);
saveFileStream.Flush();
Console.WriteLine( " 写入:{0}~{1} 成功 " ,part.BeginIndex,part.EndIndex);
}
catch (Exception ex)
{
Console.WriteLine( " {0},Write Error 这该咋办呢?? fu*K " ,ex.Message);
}
}
}
// 检测资源是否支持断点续传
public bool CheckAcceptRange()
{
bool isRange = false ;
// 同步方式,取到应答头即可
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(ToUri);
WebResponse rsp = req.GetResponse();
if (rsp.Headers[ " Accept-Ranges " ] == " bytes " )
isRange = true ;
_contentLength = ( int )rsp.ContentLength;
rsp.Close();
return isRange;
}
}
2.
class PartManager
{
private int partSize = 128 * 1024 ; // 128K每份
private ConcurrentDownLoader _loader;
private ConcurrentStack < DPart > _dParts;
private AsyncWebRequest[] _reqs;
public event Action OnDownLoaded;
public event Action < Exception > OnError;
private int _aliveThreadCount;
private Thread _checkComplete;
public PartManager(ConcurrentDownLoader loader)
{
_loader = loader;
_dParts = new ConcurrentStack < DPart > ();
_checkComplete = new Thread( new ThreadStart(() =>
{
Thread.Sleep( 3 * 1000 );
while ( true )
{
int count = 0 ;
foreach (var req in _reqs)
{
if (req.IsComplete)
count ++ ;
}
if (_reqs.Length == count)
{
if (OnDownLoaded != null )
{
OnDownLoaded();
_checkComplete.Abort();
}
}
else
Thread.Sleep( 1000 );
}
}));
_checkComplete.IsBackground = true ;
_checkComplete.Start();
// parts Data
if (loader.ContentLength < partSize)
_dParts.Push( new DPart() { BeginIndex = 1 , EndIndex = loader.ContentLength });
else
{
int count = loader.ContentLength % partSize == 0 ? loader.ContentLength / partSize : loader.ContentLength / partSize + 1 ;
for ( int i = 0 ; i < count; i ++ )
{
DPart p = new DPart();
p.BeginIndex = i * partSize;
p.EndIndex = (i + 1 ) * partSize - 1 ;
if (count - 1 == i)
p.EndIndex = loader.ContentLength - 1 ;
_dParts.Push(p);
}
}
// 建立工作线程
_aliveThreadCount = loader.MaxThreadCount;
_reqs = new AsyncWebRequest[loader.MaxThreadCount];
for ( int i = 0 ; i < loader.MaxThreadCount; i ++ )
{
_reqs[i] = new AsyncWebRequest(i, loader);
}
}
public void Proc()
{
foreach (AsyncWebRequest req in _reqs)
{
if (_dParts.Count > 0 )
{
DPart d;
if (_dParts.TryPop( out d))
{
req.IsComplete = false ;
req.BeginGetStream(Callback, d);
}
else
req.IsComplete = true ;
}
else
req.IsComplete = true ;
}
}
public void Callback(AsyncWebRequest req, MemoryStream mm, Exception ex)
{
// 一个线程如果3次都失败,就不再使用了,可能是线程数有限制,
if (ex == null && mm != null )
{
// check mm size
if (( int )mm.Length == req.EndIndex - req.BeginIndex + 1 )
{
_loader.WriteFile(req.Part, mm);
// 重新分配 Part
if (_dParts.Count > 0 )
{
DPart d;
if (_dParts.TryPop( out d))
{
req.BeginGetStream(Callback, d);
}
}
else
{
// 所有Part都已经完成鸟,ok success
req.IsComplete = true ;
}
}
else
{
req.IsComplete = true ;
Console.WriteLine( " mm Length:{0}, Part Length:{1} ,Why not the same ~~~shit " , mm.Length, req.EndIndex - req.BeginIndex);
// 回收分区
_dParts.Push(req.Part);
Interlocked.Decrement( ref _aliveThreadCount);
if (_aliveThreadCount == 0 )
{
if (OnError != null )
OnError( null );
}
}
}
else
{
req.IsComplete = true ;
// 回收分区
_dParts.Push(req.Part);
Interlocked.Decrement( ref _aliveThreadCount);
if (_aliveThreadCount == 0 )
{
_checkComplete.Abort();
if (OnError != null )
OnError( null );
}
}
}
}
3. 文件分片
class DPart
{
public int BeginIndex;
public int EndIndex;
public bool IsComlete;
public int tryTimes;
}
4.异步WebRequst简单封装
class AsyncWebRequest
{
// http://www.ietf.org/rfc/rfc2616.txt
// suffix-byte-range-spec = "-" suffix-length
// suffix-length = 1*DIGIT
// Range = "Range" ":" ranges-specifier
// Range: bytes=100-300
// Content-Range = "Content-Range" ":" content-range-spec
// content-range-spec = byte-content-range-spec
// byte-content-range-spec = bytes-unit SP
// byte-range-resp-spec "/"
// ( instance-length | "*" )
// byte-range-resp-spec = (first-byte-pos "-" last-byte-pos)
// | "*"
// instance-length = 1*DIGIT
// HTTP/1.1 206 Partial content
// Date: Wed, 15 Nov 1995 06:25:24 GMT
// Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
// Content-Range: bytes 21010-47021/47022
// Content-Length: 26012
// Content-Type: image/gif
private int _threadId;
public int ThreadId { get { return _threadId; } }
public bool IsComplete { get ; set ; }
private ConcurrentDownLoader _loader;
public ConcurrentDownLoader Loader { get { return _loader; } }
private DPart _part;
public DPart Part { get { return _part; } }
private int _beginIndex;
public int BeginIndex { get { return _beginIndex; } }
private int _endIndex;
public int EndIndex { get { return _endIndex; } }
private int _tryTimeLeft;
private Action < AsyncWebRequest, MemoryStream, Exception > _onResponse;
private HttpWebRequest _webRequest;
private MemoryStream _mmStream;
private Stream _rspStream;
public AsyncWebRequest( int threadId, ConcurrentDownLoader loader)
{
_threadId = threadId;
_loader = loader;
}
public void BeginGetStream(Action < AsyncWebRequest, MemoryStream, Exception > rep, DPart part)
{
IsComplete = false ;
_beginIndex = part.BeginIndex;
_endIndex = part.EndIndex;
_part = part;
_onResponse = rep;
_tryTimeLeft = 3 ;
DoRequest();
}
private void DoRequest()
{
_webRequest = (HttpWebRequest)WebRequest.Create(Loader.ToUri);
_webRequest.AddRange(BeginIndex, EndIndex);
// _webRequest.Headers.Add(string.Format("Range: bytes={0}-{1}", BeginIndex, EndIndex));
_mmStream = new MemoryStream(EndIndex - BeginIndex);
Console.WriteLine( " 开始获取{0}-{1}段 " , BeginIndex, EndIndex);
_webRequest.BeginGetResponse((result) =>
{
try
{
HttpWebRequest req = result.AsyncState as HttpWebRequest;
WebResponse rsp = req.EndGetResponse(result);
// 验证Content-Range: bytes的正确性
string contentRange = rsp.Headers[ " Content-Range " ];
// (\d+)\-(\d+)\/(\d+) Match Content-Range: bytes 21010-47021/47022
Regex reg = new Regex( @" (\d+)\-(\d+)\/(\d+) " );
Match mc = reg.Match(contentRange);
if (mc.Groups.Count == 4 )
{
int bid = Convert.ToInt32(mc.Groups[ 1 ].Value);
int eid = Convert.ToInt32(mc.Groups[ 2 ].Value);
int len = Convert.ToInt32(mc.Groups[ 3 ].Value);
if (bid == BeginIndex && eid == EndIndex && Loader.ContentLength == len)
Console.WriteLine( " 开始获取{0}-{1}段时返回成功,Content-Range:{2} " , BeginIndex, EndIndex, contentRange);
else
throw new Exception( string .Format( " 开始获取{0}-{1}段时返回失败,Content-Range 验证错误:{2} " , BeginIndex, EndIndex, contentRange));
}
else
{
throw new Exception( " return Content-Range Error : " + contentRange);
}
Console.WriteLine( " 开始获取{0}-{1}段时返回成功,开始读取数据 " , BeginIndex, EndIndex);
_rspStream = rsp.GetResponseStream();
byte [] buffer = new byte [ 4096 ];
_rspStream.BeginRead(buffer, 0 , 4096 , EndReadStream, buffer);
}
catch (Exception ex)
{
if (_tryTimeLeft > 0 )
{
Console.WriteLine( " 获取{0}-{1}失败,ex:{2},重试 " , BeginIndex, EndIndex, ex.Message);
_tryTimeLeft -- ;
_rspStream.Close();
_rspStream = null ;
_webRequest = null ;
DoRequest();
}
else
{
Console.WriteLine( " 获取{0}-{1}失败,ex:{2},已经重试3次放弃~~ " , BeginIndex, EndIndex, ex.Message);
if (_onResponse != null )
_onResponse( this , null , ex);
}
}
}, _webRequest);
}
private void EndReadStream(IAsyncResult result)
{
try
{
byte [] buffer = result.AsyncState as byte [];
int count = _rspStream.EndRead(result);
if (count > 0 )
{
Console.WriteLine( " 读取{0}-{1}段数据中,读取到:{2}字节,continue··· " , BeginIndex, EndIndex, count);
_mmStream.Write(buffer, 0 , count);
_rspStream.BeginRead(buffer, 0 , 4096 , EndReadStream, buffer);
}
else
{
// OK now all is back
if (_onResponse != null )
_onResponse( this , _mmStream, null );
}
}
catch (Exception ex)
{
if (_tryTimeLeft > 0 )
{
Console.WriteLine( " 获取{0}-{1}失败,ex:{2},重试 " , BeginIndex, EndIndex, ex.Message);
_tryTimeLeft -- ;
if (_rspStream != null )
_rspStream.Close();
_rspStream = null ;
_webRequest = null ;
DoRequest();
}
else
{
Console.WriteLine( " 获取{0}-{1}失败,ex:{2},已经重试3次放弃~~ " , BeginIndex, EndIndex, ex.Message);
if (_onResponse != null )
_onResponse( this , _mmStream, ex);
}
}
}
}
自己测试100多M的文件开10个线程可以正常下载,因为仓促写的,没经过大量测试可能还有很多没考虑的地方,仅供参考。