學習應用程式實作續傳的能力

學習應用程式實作續傳的能力

以前做過一個案子,該應用程式使用FTP的方式上傳用戶操作的資訊至伺服器端,傳送完畢後,

應用程式透過Web Service啟動伺服器去做同步上傳好的檔案,並產生另一個下載檔案的機制。

 

對於續傳機制,我並沒有非常的清楚,因此,透過該篇文章把續傳的原理加以釐清,並且套用

至ASP.NET裡,實作出可以支援續傳的Web服務。

 

〉概述Http在Request/Response的處理機制

     過去在網路課程裡一定知道Http是一種Three hand-shark的機制,Client端發出Request給Server端,

再由Server端回傳Response給Client完成一次Http的交易。那麼續傳的機制在Http交易的協定裡是怎樣的切分方式呢?

 

A. 對於Server端而言:(引用在ASP.NET中支持断点续传下载大文件(ZT)一文的介紹)

     有幾個重要的Http Header標籤需要注意:「Accept-Range」、「ETag」與「Last-Modified」。

‧Accept-Range

     用於告知Client端這是一個支援續傳的下載。其內容值:存放下載檔案的開始byte位置、檔案的byte大小

‧ETag

     用於告知儲存檔案時的識別標識。通常用於在請求續傳時,做為驗證的一個依據;

‧Last-Modified

     該標題選擇性使用的,它也是做為驗證之用的,通常會放置Server端檔案的最後修改時間,做為驗證之用;

 

到這裡也許會覺得,到底要驗證什麼?其驗證的用途,包括:

     (1) 確認Client端與Server端二邊的檔案,其正確性與一致性;

     (2) 驗證Client端的請求是否為合法,並以比對Client端送出的二個參數驗證檔案是否為一致;

 

B. 對於Client端而言

     在HTTP/1.1之後開始支援續傳的協定,主要透過二個Header中的關鍵key值:「Range」與「Content-Range」。

Range

      在Request的Header加上Range,告訴Server端上一次Client端最後收到的Byte,打算從這個Byte開始繼續接收;

      配合W3C裡的定義,其格式範例如下:

   1: HTTP/1.1 206 Partial content
   2: Date: Wed, 15 Nov 1995 06:25:24 GMT
   3: Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
   4: // 21010-47021:代表要下載這個區間的資料,21010是啟始;47021是結束;
   5: Range: bytes 21010-47021
   6:  
   7: Content-Length: 26012
   8: Content-Type: image/gif

      如果是第一次下載檔案的話,送出請求時,Range為null,這時Server端應在回傳Response的Header中加上:

      「Accept-Range」與「ETag」;告知Client端此段交易是支援續傳的,可將檔案的大小、驗證的識別儲存起來,

      做為下次續傳時使用。

 

‧如果Server端回傳了Accept-Range與ETag,Client端發出續傳請求時,需注意

     當Server端回傳Response裡有上述二個Tag,也代表Client端要進行續傳時,需在Request中加上幾個特定的標籤:

     ‧If-Range:將從Server端Response 中的ETag值,放置其中。(二者是互相對應的);

     ‧Unless-Modified-Since:將從Server端Response 中的Last-Modified值,放置其中。(二者是互相對應的);

     這二個標籤的加上可以協助Server端針對Client端預傳的請求,進行驗證。

 

Content-Range

      類似Range的用途,在Request中告訴Server目前取得檔案的部分範圍,讓Server回傳完整的內容進行下載;

      格式範例如下:

   1: HTTP/1.1 206 Partial content
   2: Date: Wed, 15 Nov 1995 06:25:24 GMT
   3: Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
   4: // bytes是固定字;
   5: // 21010-47021:代表要接收從21010 bytes到結束;
   6: // 總檔案大小為:47022;
   7: Content-Range: bytes 21010-47021/47022
   8:  
   9: Content-Length: 26012
  10: Content-Type: image/gif

上述提到這二個參數,主要是根據<C# 断点续传原理与实现>上所說明的一個原理,所整理出來的,因為透過W3C

的規格,總是會讓人看了更混亂,應該加以整理一下。至於是不是也這麼容易實作呢?來寫個範例試試吧。

 

[範例1]

a. 實作一個Client端,並且要求指定的檔案位置,並進行續傳的任務;

   1: public class KeepDWHandler
   2: {
   3:     //定義要使用的URL與檔案名稱
   4:     private string gUrl = "http://localhost:2319/download.aspx?FileName=pdf.pdf&Content=application/pdf";
   5:     private string gFileName = "pdf.pdf";
   6:     private string gStoreFilePath = string.Empty;
   7:  
   8:     public void KeepDownloadFile()
   9:     {
  10:         FileStream tFStream = null;
  11:         long tStartPost = 0;
  12:         //組合實際要寫入的檔案位置
  13:         string tFullFilePath = string.Format(@"{0}\Data\{1}", Application.StartupPath, gFileName);
  14:         //檢查要寫入的檔案是否存在
  15:         if (File.Exists(tFullFilePath) == true)
  16:         {
  17:             //存在,先讀取進來
  18:             tFStream = File.OpenWrite(tFullFilePath);
  19:             tStartPost = tFStream.Length;
  20:             //移動FileStream移動到目前的Index;
  21:             tFStream.Seek(tStartPost, SeekOrigin.Current);
  22:         }
  23:         else
  24:         {
  25:             //重新建立一個
  26:             tFStream = new FileStream(tFullFilePath, FileMode.Create);
  27:         }
  28:         try
  29:         {
  30:  
  31:             HttpWebRequest tRequest = HttpWebRequest.Create(gUrl) as HttpWebRequest;
  32:             if (tStartPost > 0)
  33:             {
  34:                 //http header加上range
  35:                 tRequest.AddRange((int)tStartPost);
  36:             }
  37:             //取得Response的資料
  38:             Stream tStream = tRequest.GetResponse().GetResponseStream();
  39:             //以512 bytes為一個單位寫入檔案
  40:             byte[] tBytes = new byte[512];
  41:             int tReadSize = tStream.Read(tBytes, 0, 512);
  42:             while (tReadSize > 0)
  43:             {
  44:                 tFStream.Write(tBytes, 0, tReadSize);
  45:                 tReadSize = tStream.Read(tBytes, 0, 512);
  46:             }
  47:             tFStream.Flush();
  48:             tFStream.Close();
  49:         }
  50:         catch (Exception ex)
  51:         {
  52:             tFStream.Close();
  53:             MessageBox.Show(ex.Message);
  54:         }
  55:     }
  56: }

b. 透過上段的程式,確實只需要加上Range的Key,即可以將一個下載到一半的檔案,在續傳的把它儲存完畢。

 

[Server端實作續傳下載的範例] 可直接參考<在ASP.NET中支持断点续传下载大文件(ZT)>中的範例,其說明相當完整

裡面提供了基本驗證、流量限制、檔案分塊、判斷檔案大小限制、StatusCode的判斷,非常詳細。

 

以上概略的了解了續傳的原理之後,接下來可能有幾個問題就跑出來了:

1. 如果下載的URL是透過Server端根據參數的值,另外產生出來,不是有固定的檔案URL,怎麼處理?

2. Client端在做下載任務的時候,如果用戶不小心關閉了程式,該怎麼保存下載到一半的資料

3. Android中存在一些App可以做系統資源的控制,如果它不小心刪掉我的App,要怎麼通知我的App

 

以下就來一一釐清各自的問題;

1. 如果下載的URL是透過Server端根據參數的值,另外產生出來,不是有固定的檔案URL,怎麼處理

     其實使用URL或是由Server透過Response寫出來的值,概念上是一樣的,只是一個由Web Server自動針對MIME類型,

     轉換成特定的Content-Type告訴Client端怎麼來解釋這個Response;相同地觀念,套到自己寫的Server Code所產生的

     Response,其實不也是為了做到跟Web Server相同的功能嗎。因此,這個問題可以不用擔心;

 

2. Client端在做下載任務的時候,如果用戶不小心關閉了程式,該怎麼保存下載到一半的資料

     透過上述的範例可以看到,透過每512 bytes為一個寫入的單位,如果在寫過入程裡,程式被關閉了,也代表寫入到

     某一個單位了,那接下來重新啟動程式時,一樣可以透過上述的範例將程式重新下載,形成可以讀取的檔案;

 

3. Android中存在一些App可以做系統資源的控制,如果它不小心刪掉我的App,要怎麼通知我的App

     這個問題是我寫Android時候遇到最頭痛的問題,雖然Android是Multi-Thread的執行,但系統本身又提供其他的入口

     來關閉你的程式(Activities);這讓撰寫App時是特別要注意的;

     對於使用第三方原件關閉我們的程式,造成程式強制關閉的問題,其實是有解法的,從Activity的Life Cycle來看:

    程式被關閉,代表當再啟動應用程式時,它會進入OnCreate(),建議在此時就先建立一個機制,去Restore該Activity

     需要用到的所有參數,以確保往下的動作可以正常執行

     用這個道理來反推,其實如果遇到下載過程被關閉,其實又回到第二個問題了,所以並非無解,只是需要先做好Restore

     的動作。

 

 

〉上傳續傳機制

     上傳的機制,主要是由Client端將檔案送至Server端進行儲存,但它與下載續傳有一個比較不一樣的地方,需要先詢問:

「Server端針對Client端要上傳的檔案,目前所擁有的檔案大小」;這是為了先讓Client端知道還有那些byte[]還沒有上傳,

透過這個方法,接著在往下做續傳的任務,因此,實作上傳續傳機制,針對Client與Server端加以說明:

 

a. Client端:   (1) 需要向Server詢問要上傳的檔案目前儲存於Server端的檔案大小;

                              (2) 讀取本機檔案,並移動byte至Server端回傳的檔案大小;

                              (3) 依照流量限制的大小分解(按特定大小)剩下要上傳的部分,一段一段進行上傳;

b. Server端: (1) 提供回傳Client端要求取得特定檔案的目前大小;

                              (2) 接收Client端透過續傳機制上傳的byte[],並將它寫入檔案中;

 

往下便直接透過擷取<C# 断点续传 上传、下载文件处理>的範例程式來加以說明:

1. Server端實作取得檔案大小的方法

   1: public partial class UploadFilePage : System.Web.UI.Page
   2: {
   3:     protected void Page_Load(object sender, EventArgs e)
   4:     {
   5:        //根據QueryString的參數取得指定檔案的大小
   6:        if (Request.QueryString["GetFileLength"] != string.Empty)
   7:        {
   8:            string tFile = Request.QueryString["FileName"].ToString();  
   9:            long tLength = GetSpecFileLength(tFile);
  10:            Response.Write(tLength);
  11:            Response.Flush();
  12:        }
  13:     }
  14:  
  15:     /// <summary>
  16:     /// 根據檔名讀取Server端的檔案大小。0代表沒有值;
  17:     /// </summary>  
  18:     private long GetSpecFileLength(string pFileName)
  19:     {
  20:         string tFile = string.Format("{0}/{1}", @"Uploads/", pFileName);
  21:         FileStream tFstream = new FileStream(Server.MapPath(tFile), FileMode.Open);
  22:         long tFileSize = tFstream.Length;
  23:         tFstream.Close();
  24:         return tFileSize;
  25:     }
  26: }

 

2. Server端實作接收Client端上傳續傳的Request,並寫入檔案

   1: public partial class UploadFilePage : System.Web.UI.Page
   2: {
   3:     protected void Page_Load(object sender, EventArgs e)
   4:     {
   5:         //根據QueryString的參數來處理上傳
   6:         if (Request.QueryString["UploadFile"] != string.Empty)
   7:         {
   8:             //Server端處理儲存上傳續傳的檔案
   9:             string tFileName = string.Format("{0}/{1}", @"Uploads/", Request.QueryString["FileName"]);
  10:             long tSOwnSize = Convert.ToInt64(Request.QueryString["SOwnSize"]);
  11:             //指定檔案名稱、目前Server擁有檔案的大小、Request上傳的Stream
  12:             SaveUpLoadFileRange(tFileName, tSOwnSize, Request.InputStream);
  13:         }
  14:     }
  15:  
  16:     /// <summary>
  17:     /// 處理上傳續傳的檔案。
  18:     /// </summary>
  19:     public void SaveUpLoadFileRange(string pFileName, long pSOwnSize, Stream pRStream)
  20:     {
  21:         //取得Request這次的Stream大小
  22:         int tUpLoadLength = Convert.ToInt32(pRStream.Length);
  23:  
  24:         //記得FileMode使用OpenOrCreate;FileAccess.ReadWrite才能往下動作
  25:         FileStream tFStream = new FileStream(pFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
  26:  
  27:         //移動檔案讀取的指標,如果pSOwnSize = 0,代表沒有儲存過檔案;
  28:         tFStream.Seek(pSOwnSize, SeekOrigin.Begin);
  29:  
  30:         //將Request中的Stream放成BinaryReader準備寫入檔案
  31:         BinaryReader tBReader = new BinaryReader(pRStream);
  32:         try
  33:         {
  34:             byte[] data = new byte[tUpLoadLength];
  35:             //把Stream讀到byte[]裡
  36:             tBReader.Read(data, 0, tUpLoadLength);
  37:             //把Byte[]寫入檔案
  38:             tFStream.Write(data, 0, tUpLoadLength);
  39:         }
  40:         catch (Exception)
  41:         {
  42:             throw;
  43:         }
  44:         finally
  45:         {
  46:             //寫入檔案後,記得釋放,以免檔案被占住無法使用;
  47:             tFStream.Close();
  48:             tBReader.Close();
  49:         }
  50:     }
  51: }

 

3. Client端實作上傳續傳的功能,先取得目前Server端有的檔案大小,再分段上傳未上傳的部分

   1: /// <summary>
   2: /// 處理上傳續傳檔案。
   3: /// </summary>
   4: /// <param name="fileName">檔案的完整路徑</param>
   5: /// <param name="byteCount">上傳檔案的流量限制,當檔案大時指定的值時,切割檔案分段上傳(單位Byte)</param>
   6: /// <returns>true/false</returns>
   7: public static bool UpLoadFile2(string pFileName, int pUByteLimit)
   8: {
   9:     bool tResult = true;
  10:     //取得目前在Server端檔案的大小
  11:     long tSOwnSize = GetServerFileLength(pFileName);
  12:     //讀取要上傳的檔案
  13:     FileStream tFStream = new FileStream(pFileName, FileMode.Open, FileAccess.Read);
  14:     BinaryReader tReader = new BinaryReader(tFStream);
  15:     long tFileSize = tFStream.Length;
  16:     //取得檔案名稱,準備上傳時用的檔名
  17:     pFileName = pFileName.Substring(pFileName.LastIndexOf('\\') + 1);
  18:  
  19:     //開始上傳檔案
  20:     try
  21:     {
  22:         //識別是否需要移動至指定的byte指標
  23:         byte[] tUploadData;
  24:         if (tSOwnSize > 0)
  25:         {
  26:             tFStream.Seek(tSOwnSize, SeekOrigin.Current);
  27:         }
  28:  
  29:         #region //識別檔案是否需要被分成多個檔案進刪上傳
  30:         //使用for迴傳把檔案送完;
  31:         for (; tSOwnSize <= tFileSize; tSOwnSize = tSOwnSize + pUByteLimit)
  32:         {
  33:             //識別Server端有的檔案大小加上流量限制的大小是否會超過實際檔案大小
  34:             if (tSOwnSize + pUByteLimit > tFileSize)
  35:             {
  36:                 //大於,代表可以一次上傳,設定要讀取的byte大小
  37:                 tUploadData = new byte[Convert.ToInt64((tFileSize - tSOwnSize))];
  38:                 tReader.Read(tUploadData, 0, Convert.ToInt32((tFileSize - tSOwnSize)));
  39:             }
  40:             else
  41:             {
  42:                 //小於,代表要分段上傳,依照流量限制大小進行分段讀取
  43:                 tUploadData = new byte[pUByteLimit];
  44:                 tReader.Read(tUploadData, 0, pUByteLimit);
  45:             }
  46:  
  47:             try
  48:             {
  49:                 Hashtable parms = new Hashtable();
  50:                 parms.Add("FileName", pFileName);
  51:                 parms.Add("SOwnSize", tSOwnSize.ToString());
  52:                 //上傳檔案,透過WebClient使用UploadData方法
  53:                 byte[] byRemoteInfo = PostData(serverPath + "UpLoadFilePage.aspx", tUploadData, parms);
  54:             }
  55:             catch (Exception)
  56:             {
  57:                 tResult = false;
  58:                 break;
  59:             }
  60:         }
  61:         #endregion
  62:     }
  63:     catch (Exception ex)
  64:     {
  65:         throw ex;
  66:     }
  67:     finally
  68:     {
  69:         tReader.Close();
  70:         tFStream.Close();
  71:     }
  72:     return tResult;
  73: }

該段程式,我僅擷錄處理上傳與分段的部分,其他詳細的可以到參考資料中進行下載,有更完整可以執行的範例;

======

隨著用戶手邊的設備愈來愈多元,並且可以與網路溝通進行檔案的上傳與下載,對於續傳的需求,將是開發時要考量的。

因為行動網路不是非常穩或有線網路這麼快速,那麼傳輸過程失敗的可能性就增加了,續傳將會是一個不錯的解決方案。

 

以上是分享學習撰寫一個續傳應用程式的概念,希望有所幫助。

 

References:

在ASP.NET中支持断点续传下载大文件(ZT) (转)

C# 断点续传原理与实现 & C# asp.net 文件下载代码例子,支持缓存,断点续传

http断点续传原理:http头 Range、Content-Range

HTTP协议header头域 & 14.35.2 Range Retrieval Requests & Range Units

C# 断点续传 上传、下载文件处理 (重要)

转  asp.net断点上传 & C#断点续传实例(包含客户端、服务器端代码) & FLASH+ASP.NET(C#)的断点续传(上传)解决方案

 

Dotblogs Tags: