天天看点

C# 断点续传原理与实现

在了解HTTP断点续传的原理之前,让我们先来了解一下HTTP协议,HTTP协议是一种基于tcp的简单协议,分为请求和回复两种。请求协议是由 客户机(浏览器)向服务器(WEB SERVER)提交请求时发送报文的协议。回复协议是由服务器(web server),向客户机(浏览器)回复报文时的协议。请求和回复协议都由头和体组成。头和体之间以一行空行为分隔。

   以下是一个请求报文与相应的回复报文的例子: 

GET /image/index_r4_c1.jpg HTTP/1.1 

Accept: */* 

Accept-Language: zh-cn 

Accept-Encoding: gzip, deflate 

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705) 

Host: 192.168.3.120:8080 

Connection: Keep-Alive

HTTP/1.1 200 OK 

Server: Microsoft-IIS/5.0 

Date: Tue, 24 Jun 2003 05:39:40 GMT 

Content-Type: image/jpeg 

Accept-Ranges: bytes 

Last-Modified: Thu, 23 May 2002 03:05:40 GMT 

ETag: "bec48eb862c21:934" 

Content-Length: 2827

….

  下面我们就来说说"断点续传",顾名思义,断点续传就是在上一次下载时断开的位置开始继续下载。 

在HTTP协议中,可以在请求报文头中加入Range段,来表示客户机希望从何处继续下载。 

  比如说从第1024字节开始下载,请求报文如下:

Range:bytes=1024- 

  .NET中的相关类

   明白了上面的原理,那么,我们来看看.NET FRAMEWORK中为我们提供了哪些类可以来做这些事。

   完成HTTP请求

System.Net.HttpWebRequest

   HttpWebRequest 类对 WebRequest 中定义的属性和方法提供支持,也对使用户能够直接与使用 HTTP 的服务器交互的附加属性和方法提供支持。

   HttpWebRequest 将发送到 Internet 资源的公共 HTTP 标头值公开为属性,由方法或系统设置。下表包含完整列表。可以将 Headers 属性中的其他标头设置为名称/值对。但是注意,某些公共标头被视为受限制的,它们或者直接由 API公开,或者受到系统保护,不能被更改。Range也属于被保护之列,不过,.NET为开发者提供了更方便的操作,就是 AddRange方法,向请求添加从请求数据的开始处或结束处的特定范围的字节范围标头

   完成文件访问

System.IO.FileStream

   FileStream 对象支持使用Seek方法对文件进行随机访问, Seek 允许将读取/写入位置移动到文件中的任意位置。这是通过字节偏移参考点参数完成的。字节偏移量是相对于查找参考点而言的,该参考点可以是基础文件的开始、 当前位置或结尾,分别由SeekOrigin类的三个属性表示。

   代码实现

   了解了.NET提供的相关的类,那么,我们就可以方便的实现了。

   代码如下:

static void Main(string[] args) 

{

string StrFileName="c:\\aa.zip"; //根据实际情况设置 

//打开上次下载的文件或新建文件 

long lStartPos =0; 

System.IO.FileStream fs; 

if (System.IO.File.Exists(StrFileName)) 

fs= System.IO.File.OpenWrite(StrFileName); 

lStartPos=fs.Length; 

fs.Seek(lStartPos,System.IO.SeekOrigin.Current); //移动文件流中的当前指针 

else 

fs = new System.IO.FileStream(StrFileName,System.IO.FileMode.Create); 

lStartPos =0; 

}

//打开网络连接 

try 

System.Net.HttpWebRequest request =(System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(StrUrl); 

if ( lStartPos>0) 

request.AddRange((int)lStartPos); //设置Range值

//向服务器请求,获得服务器回应数据流 

System.IO.Stream ns= request.GetResponse().GetResponseStream();

byte[] nbytes = new byte[512]; 

int nReadSize=0; 

nReadSize=ns.Read(nbytes,0,512); 

while( nReadSize >0) 

fs.Write(nbytes,0,nReadSize); 

fs.Close(); 

ns.Close(); 

Console.WriteLine("下载完成"); 

catch(Exception ex) 

Console.WriteLine("下载过程中出现错误:"+ex.ToString()); 

另一个断点续传的代码:

/* .Net/C#: 实现支持断点续传多线程下载的工具类 

 * Reflector 了一下 System.Net.WebClient ,改写或增加了若干: 

 * DownLoad、Upload 相关方法! 

 * 增加了 DataReceive、ExceptionOccurrs 事件 

 */

namespace Microshaoft.Utils 

 using System; 

 using System.IO; 

 using System.Net; 

 using System.Text; 

 using System.Security; 

 using System.Threading; 

 using System.Collections.Specialized;

 /// <summary> 

 /// 记录下载的字节位置 

 /// </summary> 

 public class DownLoadState 

 { 

  private string _FileName;

  private string _AttachmentName; 

  private int _Position; 

  private string _RequestURL; 

  private string _ResponseURL; 

  private int _Length;

  private byte[] _Data;

  public string FileName 

  { 

   get 

   { 

    return _FileName; 

   } 

  }

  public int Position 

    return _Position; 

  public int Length 

    return _Length; 

  public string AttachmentName 

    return _AttachmentName; 

  public string RequestURL 

    return _RequestURL; 

  public string ResponseURL 

    return _ResponseURL; 

  public byte[] Data 

    return _Data; 

  internal DownLoadState(string RequestURL, string ResponseURL, string FileName, string AttachmentName, int Position, int Length, byte[] Data) 

   this._FileName = FileName; 

   this._RequestURL = RequestURL; 

   this._ResponseURL = ResponseURL; 

   this._AttachmentName = AttachmentName; 

   this._Position = Position; 

   this._Data = Data; 

   this._Length = Length; 

  internal DownLoadState(string RequestURL, string ResponseURL, string FileName, string AttachmentName, int Position, int Length, ThreadCallbackHandler tch) 

   this._ThreadCallback = tch; 

  internal DownLoadState(string RequestURL, string ResponseURL, string FileName, string AttachmentName, int Position, int Length) 

  private ThreadCallbackHandler _ThreadCallback;

  public HttpWebClient httpWebClient 

    return this._hwc; 

   set 

    this._hwc = value; 

  internal Thread thread 

    return _thread; 

    _thread = value; 

  private HttpWebClient _hwc; 

  private Thread _thread;

  // 

  internal void StartDownloadFileChunk() 

   if (this._ThreadCallback != null) 

    this._ThreadCallback(this._RequestURL, this._FileName, this._Position, this._Length); 

    this._hwc.OnThreadProcess(this._thread); 

 }

 //委托代理线程的所执行的方法签名一致 

 public delegate void ThreadCallbackHandler(string S, string s, int I, int i);

 //异常处理动作 

 public enum ExceptionActions 

  Throw, 

  CancelAll, 

  Ignore, 

  Retry 

 /// 包含 Exception 事件数据的类 

 public class ExceptionEventArgs : System.EventArgs 

  private System.Exception _Exception; 

  private ExceptionActions _ExceptionAction;

  private DownLoadState _DownloadState;

  public DownLoadState DownloadState 

    return _DownloadState; 

  public Exception Exception 

    return _Exception; 

  public ExceptionActions ExceptionAction 

    return _ExceptionAction; 

    _ExceptionAction = value; 

  internal ExceptionEventArgs(System.Exception e, DownLoadState DownloadState) 

   this._Exception = e; 

   this._DownloadState = DownloadState; 

  } 

 /// 包含 DownLoad 事件数据的类 

 public class DownLoadEventArgs : System.EventArgs 

  public DownLoadEventArgs(DownLoadState DownloadState) 

 public class ThreadProcessEventArgs : System.EventArgs 

  public Thread thread 

    return this._thread; 

  public ThreadProcessEventArgs(Thread thread) 

   this._thread = thread; 

 /// 支持断点续传多线程下载的类 

 public class HttpWebClient 

  private static object _SyncLockObject = new object();

  public delegate void DataReceiveEventHandler(HttpWebClient Sender, DownLoadEventArgs e);

  public event DataReceiveEventHandler DataReceive; //接收字节数据事件

  public delegate void ExceptionEventHandler(HttpWebClient Sender, ExceptionEventArgs e);

  public event ExceptionEventHandler ExceptionOccurrs; //发生异常事件

  public delegate void ThreadProcessEventHandler(HttpWebClient Sender, ThreadProcessEventArgs e);

  public event ThreadProcessEventHandler ThreadProcessEnd; //发生多线程处理完毕事件

  private int _FileLength; //下载文件的总大小

  public int FileLength 

    return _FileLength; 

  /// <summary> 

  /// 分块下载文件 

  /// </summary> 

  /// <param name="Address">URL 地址</param> 

  /// <param name="FileName">保存到本地的路径文件名</param> 

  /// <param name="ChunksCount">块数,线程数</param> 

  public void DownloadFile(string Address, string FileName, int ChunksCount) 

   int p = 0; // position 

   int s = 0; // chunk size 

   string a = null; 

   HttpWebRequest hwrq; 

   HttpWebResponse hwrp = null; 

   try 

    hwrq = (HttpWebRequest) WebRequest.Create(this.GetUri(Address)); 

    hwrp = (HttpWebResponse) hwrq.GetResponse(); 

    long L = hwrp.ContentLength;

    hwrq.Credentials = this.m_credentials;

    L = ((L == -1) || (L > 0x7fffffff)) ? ((long) 0x7fffffff) : L; //Int32.MaxValue 该常数的值为 2,147,483,647; 即十六进制的 0x7FFFFFFF

    int l = (int) L;

    this._FileLength = l;

    //    在本地预定空间(竟然在多线程下不用先预定空间) 

    //    FileStream sw = new FileStream(FileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); 

    //    sw.Write(new byte[l], 0, l); 

    //    sw.Close(); 

    //    sw = null;

    bool b = (hwrp.Headers["Accept-Ranges"] != null & hwrp.Headers["Accept-Ranges"] == "bytes"); 

    a = hwrp.Headers["Content-Disposition"]; //attachment 

    if (a != null) 

    { 

     a = a.Substring(a.LastIndexOf("filename=") + 9); 

    } 

    else 

     a = FileName; 

    }

    int ss = s; 

    if (b) 

     s = l / ChunksCount; 

     if (s < 2 * 64 * 1024) //块大小至少为 128 K 字节 

     { 

      s = 2 * 64 * 1024; 

     } 

     ss = s; 

     int i = 0; 

     while (l > s) 

      l -= s; 

      if (l < s) 

      { 

       s += l; 

      } 

      if (i++ > 0) 

       DownLoadState x = new DownLoadState(Address, hwrp.ResponseUri.AbsolutePath, FileName, a, p, s, new ThreadCallbackHandler(this.DownloadFileChunk)); 

       //       单线程下载 

       //       x.StartDownloadFileChunk();

       x.httpWebClient = this; 

       //多线程下载 

       Thread t = new Thread(new ThreadStart(x.StartDownloadFileChunk)); 

       //this.OnThreadProcess(t); 

       t.Start();

      p += s; 

     s = ss; 

     byte[] buffer = this.ResponseAsBytes(Address, hwrp, s, FileName); 

     this.OnThreadProcess(Thread.CurrentThread);

     //    lock (_SyncLockObject) 

     //    { 

     //     this._Bytes += buffer.Length; 

     //    } 

   catch (Exception e) 

    ExceptionActions ea = ExceptionActions.Throw; 

    if (this.ExceptionOccurrs != null) 

     DownLoadState x = new DownLoadState(Address, hwrp.ResponseUri.AbsolutePath, FileName, a, p, s); 

     ExceptionEventArgs eea = new ExceptionEventArgs(e, x); 

     ExceptionOccurrs(this, eea); 

     ea = eea.ExceptionAction; 

    if (ea == ExceptionActions.Throw) 

     if (!(e is WebException) && !(e is SecurityException)) 

      throw new WebException("net_webclient", e); 

     throw; 

   }

  internal void OnThreadProcess(Thread t) 

   if (ThreadProcessEnd != null) 

    ThreadProcessEventArgs tpea = new ThreadProcessEventArgs(t); 

    ThreadProcessEnd(this, tpea); 

  /// 下载一个文件块,利用该方法可自行实现多线程断点续传 

  /// <param name="Length">块大小</param> 

  public void DownloadFileChunk(string Address, string FileName, int FromPosition, int Length) 

    //this._FileName = FileName; 

    HttpWebRequest hwrq = (HttpWebRequest) WebRequest.Create(this.GetUri(Address)); 

    //hwrq.Credentials = this.m_credentials; 

    hwrq.AddRange(FromPosition); 

    byte[] buffer = this.ResponseAsBytes(Address, hwrp, Length, FileName); 

    //   lock (_SyncLockObject) 

    //   { 

    //    this._Bytes += buffer.Length; 

    //   } 

     DownLoadState x = new DownLoadState(Address, hwrp.ResponseUri.AbsolutePath, FileName, a, FromPosition, Length); 

  internal byte[] ResponseAsBytes(string RequestURL, WebResponse Response, long Length, string FileName) 

   string a = null; //AttachmentName 

   int P = 0; //整个文件的位置指针 

   int num2 = 0; 

    a = Response.Headers["Content-Disposition"]; //attachment 

    long num1 = Length; //Response.ContentLength; 

    bool flag1 = false; 

    if (num1 == -1) 

     flag1 = true; 

     num1 = 0x10000; //64k 

    byte[] buffer1 = new byte[(int) num1];

    int p = 0; //本块的位置指针

    string s = Response.Headers["Content-Range"]; 

    if (s != null) 

     s = s.Replace("bytes ", ""); 

     s = s.Substring(0, s.IndexOf("-")); 

     P = Convert.ToInt32(s); 

    int num3 = 0;

    Stream S = Response.GetResponseStream(); 

    do 

     num2 = S.Read(buffer1, num3, ((int) num1) - num3);

     num3 += num2; 

     if (flag1 && (num3 == num1)) 

      num1 += 0x10000; 

      byte[] buffer2 = new byte[(int) num1]; 

      Buffer.BlockCopy(buffer1, 0, buffer2, 0, num3); 

      buffer1 = buffer2; 

     }

     //     this._bytes += num2; 

     if (num2 > 0) 

      if (this.DataReceive != null) 

       byte[] buffer = new byte[num2]; 

       Buffer.BlockCopy(buffer1, p, buffer, 0, buffer.Length); 

       DownLoadState dls = new DownLoadState(RequestURL, Response.ResponseUri.AbsolutePath, FileName, a, P, num2, buffer); 

       DownLoadEventArgs dlea = new DownLoadEventArgs(dls); 

       //触发事件 

       this.OnDataReceive(dlea); 

       //System.Threading.Thread.Sleep(100);

      p += num2; //本块的位置指针 

      P += num2; //整个文件的位置指针 

     else 

      break; 

    while (num2 != 0);

    S.Close(); 

    S = null; 

    if (flag1) 

     byte[] buffer3 = new byte[num3]; 

     Buffer.BlockCopy(buffer1, 0, buffer3, 0, num3); 

     buffer1 = buffer3; 

    return buffer1; 

     DownLoadState x = new DownLoadState(RequestURL, Response.ResponseUri.AbsolutePath, FileName, a, P, num2); 

    return null; 

  private void OnDataReceive(DownLoadEventArgs e) 

   //触发数据到达事件 

   DataReceive(this, e); 

  public byte[] UploadFile(string address, string fileName) 

   return this.UploadFile(address, "POST", fileName, "file"); 

  public string UploadFileEx(string address, string method, string fileName, string fieldName) 

   return Encoding.ASCII.GetString(UploadFile(address, method, fileName, fieldName)); 

  public byte[] UploadFile(string address, string method, string fileName, string fieldName) 

   byte[] buffer4; 

   FileStream stream1 = null; 

    fileName = Path.GetFullPath(fileName); 

    string text1 = "---------------------" + DateTime.Now.Ticks.ToString("x");

    string text2 = "application/octet-stream";

    stream1 = new FileStream(fileName, FileMode.Open, FileAccess.Read); 

    WebRequest request1 = WebRequest.Create(this.GetUri(address)); 

    request1.Credentials = this.m_credentials; 

    request1.ContentType = "multipart/form-data; boundary=" + text1;

    request1.Method = method; 

    string[] textArray1 = new string[7] {"--", text1, "\r\nContent-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"", Path.GetFileName(fileName), "\"\r\nContent-Type: ", text2, "\r\n\r\n"}; 

    string text3 = string.Concat(textArray1); 

    byte[] buffer1 = Encoding.UTF8.GetBytes(text3); 

    byte[] buffer2 = Encoding.ASCII.GetBytes("\r\n--" + text1 + "\r\n"); 

    long num1 = 0x7fffffffffffffff; 

    try 

     num1 = stream1.Length; 

     request1.ContentLength = (num1 + buffer1.Length) + buffer2.Length; 

    catch 

    byte[] buffer3 = new byte[Math.Min(0x2000, (int) num1)]; 

    using (Stream stream2 = request1.GetRequestStream()) 

     int num2; 

     stream2.Write(buffer1, 0, buffer1.Length); 

     do 

      num2 = stream1.Read(buffer3, 0, buffer3.Length); 

      if (num2 != 0) 

       stream2.Write(buffer3, 0, num2); 

     while (num2 != 0); 

     stream2.Write(buffer2, 0, buffer2.Length); 

    stream1.Close(); 

    stream1 = null; 

    WebResponse response1 = request1.GetResponse();

    buffer4 = this.ResponseAsBytes(response1); 

   catch (Exception exception1) 

    if (stream1 != null) 

     stream1.Close(); 

     stream1 = null; 

    if (!(exception1 is WebException) && !(exception1 is SecurityException)) 

     //throw new WebException(SR.GetString("net_webclient"), exception1); 

     throw new WebException("net_webclient", exception1); 

    throw; 

   return buffer4; 

  private byte[] ResponseAsBytes(WebResponse response) 

   int num2; 

   long num1 = response.ContentLength; 

   bool flag1 = false; 

   if (num1 == -1) 

    flag1 = true; 

    num1 = 0x10000; 

   byte[] buffer1 = new byte[(int) num1]; 

   Stream stream1 = response.GetResponseStream(); 

   int num3 = 0; 

   do 

    num2 = stream1.Read(buffer1, num3, ((int) num1) - num3); 

    num3 += num2; 

    if (flag1 && (num3 == num1)) 

     num1 += 0x10000; 

     byte[] buffer2 = new byte[(int) num1]; 

     Buffer.BlockCopy(buffer1, 0, buffer2, 0, num3); 

     buffer1 = buffer2; 

   while (num2 != 0); 

   stream1.Close(); 

   if (flag1) 

    byte[] buffer3 = new byte[num3]; 

    Buffer.BlockCopy(buffer1, 0, buffer3, 0, num3); 

    buffer1 = buffer3; 

   return buffer1; 

  private NameValueCollection m_requestParameters; 

  private Uri m_baseAddress; 

  private ICredentials m_credentials = CredentialCache.DefaultCredentials;

  public ICredentials Credentials 

    return this.m_credentials; 

    this.m_credentials = value; 

  public NameValueCollection QueryString 

    if (this.m_requestParameters == null) 

     this.m_requestParameters = new NameValueCollection(); 

    return this.m_requestParameters; 

    this.m_requestParameters = value; 

  public string BaseAddress 

    if (this.m_baseAddress != null) 

     return this.m_baseAddress.ToString(); 

    return string.Empty; 

    if ((value == null) || (value.Length == 0)) 

     this.m_baseAddress = null; 

     try 

      this.m_baseAddress = new Uri(value); 

     catch (Exception exception1) 

      throw new ArgumentException("value", exception1); 

  private Uri GetUri(string path) 

   Uri uri1; 

     uri1 = new Uri(this.m_baseAddress, path); 

     uri1 = new Uri(path); 

     return uri1; 

    StringBuilder builder1 = new StringBuilder(); 

    string text1 = string.Empty; 

    for (int num1 = 0; num1 < this.m_requestParameters.Count; num1++) 

     builder1.Append(text1 + this.m_requestParameters.AllKeys[num1] + "=" + this.m_requestParameters[num1]); 

     text1 = "&"; 

    UriBuilder builder2 = new UriBuilder(uri1); 

    builder2.Query = builder1.ToString(); 

    uri1 = builder2.Uri; 

   catch (UriFormatException) 

    uri1 = new Uri(Path.GetFullPath(path)); 

   return uri1; 

/// <summary> 

/// 测试类 

/// </summary> 

class AppTest 

 int _k = 0; 

 int _K = 0;

 static void Main() 

  AppTest a = new AppTest(); 

  Microshaoft.Utils.HttpWebClient x = new Microshaoft.Utils.HttpWebClient();

  a._K = 10;

  //订阅 DataReceive 事件 

  x.DataReceive += new Microshaoft.Utils.HttpWebClient.DataReceiveEventHandler(a.x_DataReceive); 

  //订阅 ExceptionOccurrs 事件 

  x.ExceptionOccurrs += new Microshaoft.Utils.HttpWebClient.ExceptionEventHandler(a.x_ExceptionOccurrs);

  x.ThreadProcessEnd += new Microshaoft.Utils.HttpWebClient.ThreadProcessEventHandler(a.x_ThreadProcessEnd); 

  a._F = F; 

  string f = F.Substring(F.LastIndexOf("/") + 1);

  //(new System.Threading.Thread(new System.Threading.ThreadStart(new ThreadProcessState(F, @"E:\temp\" + f, 10, x).StartThreadProcess))).Start();

  x.DownloadFile(F, @"E:\temp\temp\" + f, a._K); 

  //  x.DownloadFileChunk(F, @"E:\temp\" + f,15,34556);

  System.Console.ReadLine(); 

  //  string uploadfile = "e:\\test_local.rar"; 

  //  System.Console.WriteLine(str); 

  //  System.Console.ReadLine(); 

 string bs = ""; //用于记录上次的位数 

 bool b = false; 

 private int i = 0; 

 private static object _SyncLockObject = new object(); 

 string _F; 

 string _f;

 private void x_DataReceive(Microshaoft.Utils.HttpWebClient Sender, Microshaoft.Utils.DownLoadEventArgs e) 

  if (!this.b) 

   lock (_SyncLockObject) 

    if (!this.b) 

     System.Console.Write(System.DateTime.Now.ToString() + " 已接收数据:           "); 

     //System.Console.Write( System.DateTime.Now.ToString() + " 已接收数据:           "); 

     this.b = true; 

  string f = e.DownloadState.FileName; 

  if (e.DownloadState.AttachmentName != null) 

   f = System.IO.Path.GetDirectoryName(f) + @"\" + e.DownloadState.AttachmentName;

  this._f = f;

  using (System.IO.FileStream sw = new System.IO.FileStream(f, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite, System.IO.FileShare.ReadWrite)) 

   sw.Position = e.DownloadState.Position; 

   sw.Write(e.DownloadState.Data, 0, e.DownloadState.Data.Length); 

   sw.Close(); 

  string s = System.DateTime.Now.ToString(); 

  lock (_SyncLockObject) 

   this.i += e.DownloadState.Data.Length; 

   System.Console.Write(bs + "\b\b\b\b\b\b\b\b\b\b" + i + " / " + Sender.FileLength + " 字节数据 " + s); 

   //System.Console.Write(bs + i + " 字节数据 " + s); 

   this.bs = new string('\b', Digits(i) + 3 + Digits(Sender.FileLength) + s.Length); 

 int Digits(int n) //数字所占位数 

  n = System.Math.Abs(n); 

  n = n / 10; 

  int i = 1; 

  while (n > 0) 

   n = n / 10; 

   i++; 

  return i; 

 private void x_ExceptionOccurrs(Microshaoft.Utils.HttpWebClient Sender, Microshaoft.Utils.ExceptionEventArgs e) 

  System.Console.WriteLine(e.Exception.Message); 

  //发生异常重新下载相当于断点续传,你可以自己自行选择处理方式 

  Microshaoft.Utils.HttpWebClient x = new Microshaoft.Utils.HttpWebClient(); 

  x.DownloadFileChunk(this._F, this._f, e.DownloadState.Position, e.DownloadState.Length); 

  e.ExceptionAction = Microshaoft.Utils.ExceptionActions.Ignore; 

 private void x_ThreadProcessEnd(Microshaoft.Utils.HttpWebClient Sender, Microshaoft.Utils.ThreadProcessEventArgs e) 

  //if (e.thread.ThreadState == System.Threading.ThreadState.Stopped) 

  if (this._k ++ == this._K - 1) 

   System.Console.WriteLine("\nend"); 

 }