闲着没事试着写写,本来想应该挺简单的,但一写就折腾大半天。
Http要实现多线程现在需要WebHost对HttpHeader中Range支持,有些资源不支持Range头就必须顺序下载。
协议参考 rfc2616:http://www.ietf.org/rfc/rfc2616.txt
大概步骤:
1.检测Range支持,同时获取长度
2. 通过长度创建一个相当大小的文件
3. 创建线程组
4. 分隔文件
5. 各个线程获取一个文件块任务(比较小),读取后放在内存中,完成块后写入文件,再领取下一个文件块任务
6. 直到全部块都完成
*如果将完成进度实时持久化,启动的时候加载进度,那么就能断的续传了。
线程组用异步IO 代替,为了简便任务用Stack管理块得分配,失败后再次push回,按照大概的顺序关系下载,直到栈空结束下载。实现比较简单,实际网络情况复杂,还有很多功能需要完善,不多说直接贴代码吧:
1.
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个线程可以正常下载,因为仓促写的,没经过大量测试可能还有很多没考虑的地方,仅供参考。