C#下载文件通用方法,支持大文件、续传、速度限制。
支持续传的响应头Accept-Ranges、ETag,请求头Range 。
Accept-Ranges:响应头,向客户端指明,此进程支持可恢复下载.实现后台智能传输服务(BITS),值为:bytes;
ETag:响应头,用于对客户端的初始(200)响应,以及来自客户端的恢复请求,
必须为每个文件提供一个唯一的ETag值(可由文件名和文件最后被修改的日期组成),这使客户端软件能够验证它们已经下载的字节块是否仍然是最新的。
Range:续传的起始位置,即已经下载到客户端的字节数,值如:bytes=1474560- 。
另外:UrlEncode编码后会把文件名中的空格转换中 ( 转换为%2b),但是浏览器是不能理解加号为空格的,所以在浏览器下载得到的文件,空格就变成了加号;
解决办法:UrlEncode 之后, 将 " " 替换成 "%20",因为浏览器将%20转换为空格
001 | using System; |
002 | using System.Collections.Generic; |
003 | using System.Linq; |
004 | using System.Web; |
005 | using System.IO; |
006 | using System.Text; |
007 | using System.Threading; |
008 |
009 | namespace AutoHome.CommonHelper |
010 | { |
011 | public class FileDownHelper |
012 | { |
013 | //C#下载文件帮助类 支持大文件、续传、速度限制 |
014 | /// <summary> |
015 | /// 下载文件,支持大文件、续传、速度限制。支持续传的响应头Accept-Ranges、ETag,请求头Range 。 |
016 | /// Accept-Ranges:响应头,向客户端指明,此进程支持可恢复下载.实现后台智能传输服务(BITS),值为:bytes; |
017 | /// ETag:响应头,用于对客户端的初始(200)响应,以及来自客户端的恢复请求, |
018 | /// 必须为每个文件提供一个唯一的ETag值(可由文件名和文件最后被修改的日期组成),这使客户端软件能够验证它们已经下载的字节块是否仍然是最新的。 |
019 | /// Range:续传的起始位置,即已经下载到客户端的字节数,值如:bytes=1474560- 。 |
020 | /// 另外:UrlEncode编码后会把文件名中的空格转换中 ( 转换为%2b),但是浏览器是不能理解加号为空格的,所以在浏览器下载得到的文件,空格就变成了加号; |
021 | /// 解决办法:UrlEncode 之后, 将 " " 替换成 "%20",因为浏览器将%20转换为空格 |
022 | /// </summary> |
023 | /// <param name="httpContext">当前请求的HttpContext</param> |
024 | /// <param name="filePath">下载文件的物理路径,含路径、文件名</param> |
025 | /// <param name="speed">下载速度:每秒允许下载的字节数</param> |
026 | /// <returns>true下载成功,false下载失败</returns> |
027 | public static bool DownloadFile(HttpContext httpContext, string filePath, long speed) |
028 | { |
029 | httpContext.Response.Clear(); |
030 | bool ret = true ; |
031 | try |
032 | { |
033 | switch (httpContext.Request.HttpMethod.ToUpper()) |
034 | { //目前只支持GET和HEAD方法 |
035 | case "GET" : |
036 | case "HEAD" : |
037 | break ; |
038 | default : |
039 | httpContext.Response.StatusCode = 501; |
040 | return false ; |
041 | } |
042 | if (!File.Exists(filePath)) |
043 | { |
044 | httpContext.Response.StatusCode = 404; |
045 | return false ; |
046 | } |
047 | //定义局部变量#region 定义局部变量#region 定义局部变量#region 定义局部变量 |
048 | long startBytes = 0; |
049 | long stopBytes = 0; |
050 | int packSize = 1024 * 10; //分块读取,每块10K bytes |
051 | string fileName = Path.GetFileName(filePath); |
052 | FileStream myFile = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); |
053 | BinaryReader br = new BinaryReader(myFile); |
054 | long fileLength = myFile.Length; |
055 | int sleep = ( int )Math.Ceiling(1000.0 * packSize / speed); //毫秒数:读取下一数据块的时间间隔 |
056 | string lastUpdateTiemStr = File.GetLastWriteTimeUtc(filePath).ToString( "r" ); |
057 | string eTag = HttpUtility.UrlEncode(fileName, Encoding.UTF8) lastUpdateTiemStr; //便于恢复下载时提取请求头; |
058 |
059 | // --验证:文件是否太大,是否是续传,且在上次被请求的日期之后是否被修改过#region --验证:文件是否太大,是否是续传,且在上次被请求的日期之后是否被修改过 |
060 | if (myFile.Length > long .MaxValue) |
061 | { //-------文件太大了------- |
062 | httpContext.Response.StatusCode = 413; //请求实体太大 |
063 | return false ; |
064 | } |
065 | // |
066 | if (httpContext.Request.Headers[ "If-Range" ] != null ) //对应响应头ETag:文件名 文件最后修改时间 |
067 | { |
068 | //----------上次被请求的日期之后被修改过-------------- |
069 | if (httpContext.Request.Headers[ "If-Range" ].Replace( "\"" , "" ) != eTag) |
070 | { //文件修改过 |
071 | httpContext.Response.StatusCode = 412; //预处理失败 |
072 | return false ; |
073 | } |
074 | } |
075 | try |
076 | { |
077 | //-------添加重要响应头、解析请求头、相关验证#region -------添加重要响应头、解析请求头、相关验证 |
078 | httpContext.Response.Clear(); |
079 | if (httpContext.Request.Headers[ "Range" ] != null ) |
080 | { //------如果是续传请求,则获取续传的起始位置,即已经下载到客户端的字节数------ |
081 | httpContext.Response.StatusCode = 206; //重要:续传必须,表示局部范围响应。初始下载时默认为200 |
082 | string [] range = httpContext.Request.Headers[ "Range" ].Split( new char [] { '=' , '-' }); //"bytes=1474560-" |
083 | startBytes = Convert.ToInt64(range[1]); //已经下载的字节数,即本次下载的开始位置 |
084 | if (startBytes < 0 || startBytes >= fileLength) |
085 | { //无效的起始位置 |
086 | return false ; |
087 | } |
088 | if (range.Length == 3) |
089 | { |
090 | stopBytes = Convert.ToInt64(range[2]); //结束下载的字节数,即本次下载的结束位置 |
091 | if (startBytes < 0 || startBytes >= fileLength) |
092 | { |
093 | return false ; |
094 | } |
095 | } |
096 | } |
097 | httpContext.Response.Buffer = false ; |
098 | httpContext.Response.AddHeader( "Content-MD5" , GetMD5HashFromFile(filePath)); //用于验证文件 |
099 | httpContext.Response.AddHeader( "Accept-Ranges" , "bytes" ); //重要:续传必须 |
100 | httpContext.Response.AppendHeader( "ETag" , "\"" eTag "\"" ); //重要:续传必须 |
101 | httpContext.Response.AppendHeader( "Last-Modified" , lastUpdateTiemStr); //把最后修改日期写入响应 |
102 | httpContext.Response.ContentType = "application/octet-stream" ; //MIME类型:匹配任意文件类型 |
103 | httpContext.Response.AddHeader( "Content-Disposition" , "attachment;filename=" HttpUtility.UrlEncode(fileName, Encoding.UTF8).Replace( " " , "%20" )); |
104 | httpContext.Response.AddHeader( "Content-Length" , (fileLength - startBytes).ToString()); |
105 | httpContext.Response.AddHeader( "Connection" , "Keep-Alive" ); |
106 | httpContext.Response.ContentEncoding = Encoding.UTF8; |
107 | if (startBytes > 0) |
108 | { //------如果是续传请求,告诉客户端本次的开始字节数,总长度,以便客户端将续传数据追加到startBytes位置后---------- |
109 | httpContext.Response.AddHeader( "Content-Range" , string .Format( "bytes {0}-{1}/{2}" , startBytes, fileLength - 1, fileLength)); |
110 | } |
111 | // -------向客户端发送数据块-------------------#region -------向客户端发送数据块------------------- |
112 | br.BaseStream.Seek(startBytes, SeekOrigin.Begin); |
113 | int maxCount = ( int )Math.Ceiling((fileLength - startBytes 0.0) / packSize); //分块下载,剩余部分可分成的块数 |
114 | for ( int i = 0; i < maxCount && httpContext.Response.IsClientConnected; i ) |
115 | { //客户端中断连接,则暂停 |
116 | httpContext.Response.BinaryWrite(br.ReadBytes(packSize)); |
117 | httpContext.Response.Flush(); |
118 | if (sleep > 1) |
119 | Thread.Sleep(sleep); |
120 | } |
121 | } |
122 | catch |
123 | { |
124 | ret = false ; |
125 | } |
126 | finally |
127 | { |
128 | br.Close(); |
129 | myFile.Close(); |
130 | } |
131 | } |
132 | catch |
133 | { |
134 | ret = false ; |
135 | } |
136 | return ret; |
137 | } |
138 | public static string GetMD5HashFromFile( string fileName) |
139 | { |
140 | try |
141 | { |
142 | FileStream file = new FileStream(fileName, FileMode.Open); |
143 | System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider(); |
144 | byte [] retVal = md5.ComputeHash(file); |
145 | file.Close(); |
146 |
147 | StringBuilder sb = new StringBuilder(); |
148 | for ( int i = 0; i < retVal.Length; i ) |
149 | { |
150 | sb.Append(retVal[i].ToString( "x2" )); |
151 | } |
152 | return sb.ToString(); |
153 | } |
154 | catch (Exception ex) |
155 | { |
156 | throw new Exception( "GetMD5HashFromFile() fail,error:" ex.Message); |
157 | } |
158 | } |
159 | } |
160 | } |