學習應用程式實作續傳的能力
以前做過一個案子,該應用程式使用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端預傳的請求,進行驗證。
類似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:
‧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#)的断点续传(上传)解决方案