Asp.Net 上傳大檔案專題(3)--從請求流中獲取資料並儲存為檔案[下]

iDotNetSpace發表於2009-06-03

     3.4 讀取剩餘的請求
      前面我們已經提到過ReadEntityBody (Byte[] buffer, Int32 size)方法,該方法可以用來讀取客戶端的請求資料。我們想要讀取剩餘部分的請求資料,就是要使用這個方法來從異名管道中迴圈取出請求。 [buffer:將資料讀入的位元組陣列;size:最多讀取的位元組數;如果所被讀取的剩餘請求位元組長度小於size,那麼該方法會將多餘的size大小的位元組陣列用0填充,這樣會損失不必要的效能,因此我們在使用該方法前最好先判斷下剩餘的HTTP請求大小與size的關係。據其他前輩們測試該方法大多數讀取的資料長度都在8192左右,所以size不必定的很大。]
      
讀取剩餘請求
while (iLeave > iReadStepSize && request.IsClientConnected())
{//首先判斷剩餘的請求大小是否大於iReadStepSize

    iRead = request.ReadEntityBody(bReadStepByte, iReadStepSize);
    /**//*讀取最大位元組數為iReadStepSize的使用者請求到bReadStepByte陣列中*/
    
     對檔案內容的處理

    iLeave -= iRead;
}
if (iLeave > 0 && request.IsClientConnected())
{
    iRead = request.ReadEntityBody(bReadStepByte, iLeave);
    /**//*最後還剩一部分,因為小於iReadStepSize,所以寫在迴圈外*/
  
      對檔案內容的處理
}

      3.5 從請求中擷取上傳資料,並將除去檔案資料後的請求寫入快取
      因為檔案上傳的特殊編碼方式採用分隔符來分隔不同的內容,所以我們只要利用分隔符便能精確的獲取檔案資料所在的區間,然後將其中的資料擷取。看起來是不是很簡單呢?
      我們先來看一下檔案上傳的HTTP請求內容的實際存在形式(因為內容比較多,我選取其中一部分,其中的序號是為了方便說明加上去的):

 

 01 -----------------------------7d87d1cc0a88
 02 Content-Disposition: form-data; name="tbVideoName"
 03
 04 vnm
 05 -----------------------------7d87d1cc0a88
 06 Content-Disposition: form-data; name="file"; filename="C:\Documents and Settings\stg609\妗岄潰\浣衝彞.txt"
 07 Content-Type: text/plain
 08
 09 這裡是我上傳的文字內容
 10 -----------------------------7d87d1cc0a88
 11 Content-Disposition: form-data; name="__EVENTVALIDATION"
 12
 13 /wEWBgK0g/7JCQLrqYKOBgKj5pr/CAKBmOPQBQLY14yNBQKmmtpNX1cOVXyqN8xEER3ZXbnXzsUwVVo=
 14 -----------------------------7d87d1cc0a88--
      由上面的內容我們可以發現(1)分隔符為“-----------------------------7d87d1cc0a88”,其中的數字是隨機的,並不固定,但是在同一個請求中都是相同的;(2)02、06、07、11等類似資訊被稱謂實體頭,我們可以發現一個請求流中包含了所有控制元件(包括隱藏域);(3)實體內容與實體頭之前用一個空行分開,也就是說有兩個換行符,如02和04、07和09等;(4)最後一個內容的分隔符較一般的分隔符多出"--"。另外也可以發現,如果是檔案上傳,則實體頭內會有"filename=檔案路徑"的資訊。

      我們要做的便是將上述請求的09行的內容擷取出來,而其它不變。但是要完整的擷取檔案資料並沒有這麼簡單,而且還不止一個檔案。因此我們要考慮很多因素,之前看過其它人的程式碼,感覺比較簡短,但是似乎沒有完全考慮到某些因素。
      我認為考慮因素應該包括如下幾點[可能考慮的並不全面,或者考慮的太多,希望大家多提意見]:
      a、因為我們以"filename="字串為標識查詢檔案開始的位置,所以要判斷該字串是否為於兩個資料流中
      b、"filename="之後開始到這之後的第一個換行符之前是檔名,所以為了正確獲取檔名,要判斷這之後的換行符是否與"filename="在同一個資料流內
      c、這之後是表示檔案型別,在其後又有兩個換行符,然後才是真正的檔案資料首位置。為獲取正確的開始位置,要判斷兩個換行符是否與"filename="在一個資料流內
      d、當開始讀取資料,我們要知道什麼時候檔案結束,所以要查詢分隔符,所以要確保分隔符在一個資料流內
      e、我們還要知道什麼時候已經結束整個請求,所以要查詢結束分隔符,因此要判斷結束分隔符是否在一個資料流內
      至於如何確保識別符號在一個資料流內,參考了一些前輩的做法。一般就是利用臨時陣列對可能分隔於兩個資料流內的識別符號進行儲存。然後拼接在下一次資料流之前與下一次資料流一起做為整體進行處理。

      為了便於理解,我自己搞了個圖,大家看看:

 

       通過上面這圖,大家應該對整體的流程有了一定的認識,我們在編碼中只需要按照上面的流程編碼就基本上可以保證上傳檔案資料的完整性。
       我就不講解全部程式碼了,到時候全系列寫完之後,提供大家下載。要講的主要有如下幾點:
       1)如何獲取分隔符
       通過reflector反編譯System.Web.HttpRequest可以找到GetMultipartBoundary()方法,這便是用來查詢分隔符用的。程式碼如下:
       
GetMultipartBoundary()
private byte[] GetMultipartBoundary()
{
    string attributeFromHeader = GetAttributeFromHeader(this.ContentType, "boundary");
    if (attributeFromHeader == null)
    {
        return null;
    }
    attributeFromHeader = "--" + attributeFromHeader;
    return Encoding.ASCII.GetBytes(attributeFromHeader.ToCharArray());
}

      2)如何知道檔案資料開始
      由HTTP請求內容中可以看到,檔案開始處資料與實體頭之前差距兩個換行符,所以我們首先找到“filename=”,然後找到“Content-Type: ”,最後找到兩個換行符的末尾,這便是檔案開始處位置
 
      3)如何將資料寫入檔案
      首先根據檔名,我們可以在檔案資料開始前建立一個檔案。然後通過FileStream這個I/O流的Write方法將位元組陣列寫入之前建立的檔案。 [注意使用完FileStream必須將它關閉,否則所寫入的檔案一直處於被佔用的情況,那其它程式將無法使用]


FileStream fs = File.Create(HttpContext.Current.Server.MapPath("TempUpload/" +  strFileName));
//以上是建立一個檔案
fs.Write(FileByte, FileStartPos, FileReadedLength);
//以上是將讀取到的部分檔案資料寫入該檔案


      4)如何保留除去檔案資料的請求
      這個其實和我們讀取檔案資料正好相反,因為檔案資料的讀取實際上就是將檔案資料從起始處開始儲存一直到檔案資料結束。而除去檔案資料的請求則是從請求的第一個資料開始儲存,當檔案資料開始後,則不儲存,直到檔案資料結束後,又繼續儲存。因為除去檔案資料後的請求比較小,所以我們可以直接用一個位元組陣列進行儲存。


將除去檔案資料的其餘HTTP請求寫入快取
    /**////


    /// 將除去檔案資料的其餘HTTP請求寫入快取
    ///

    /// 要被寫入快取的陣列
    /// 被寫入陣列的起始位置
    /// 被寫入陣列的長度
    /// HTTP快取
    private void WriteHttpRequestWithoutFileData(byte[] bWriteByte,int iStartIndex, int iLength,ref byte[] bHttpRequestByte)
    {
        if (bHttpRequestByte == null)
        {
            bHttpRequestByte = new byte[iLength];
            Array.Copy(bWriteByte, iStartIndex, bHttpRequestByte, 0, iLength);//將檔案資料之前的所有內容放入快取用於稍後封裝HTTP請求
        }
        else
        {
            byte[] newbyte = new byte[iLength];
            Array.Copy(bWriteByte, iStartIndex, newbyte, 0, iLength);
            bHttpRequestByte = UnionByte(newbyte, bHttpRequestByte);
        }
    }

    private byte[] UnionByte(byte[] bnewbyte, byte[] boldbyte)
    {
        int iLen = 0;
        
        if (bnewbyte != null)
        {//要新增到原陣列中的位元組如果不為空
            if (boldbyte != null)
            {
                iLen = boldbyte.Length;
            }
            byte[] unionbyte = new byte[bnewbyte.Length + iLen];
            if (boldbyte != null)
            {
                boldbyte.CopyTo(unionbyte, 0);
            }
            bnewbyte.CopyTo(unionbyte, iLen);
            return unionbyte;
        }
        else
        {
            return boldbyte;
        }
    }

      3.6 重新封裝HTTP請求
      這部分要利用反射實現,由於本人對反射這塊還不是很熟悉,所以程式碼借鑑高山來客在Asp.NET大檔案上傳元件開發總結(四)---封送資料給Asp.NET頁面中的程式碼:
      InjectTextParts
    private void InjectTextParts(HttpRequest request, byte[] textParts)
    {
        BindingFlags flags1 = BindingFlags.NonPublic | BindingFlags.Instance;
        Type type1 = request.GetType();
        FieldInfo info1 = type1.GetField("_rawContent", flags1);
        FieldInfo info2 = type1.GetField("_contentLength", flags1);

        if ((info1 != null) && (info2 != null))
        {
            Assembly web = Assembly.GetAssembly(typeof(HttpRequest));
            Type hraw = web.GetType("System.Web.HttpRawUploadedContent");
            object[] argList = new object[2];
            argList[0] = textParts.Length + 1024;
            argList[1] = textParts.Length;

            CultureInfo currCulture = CultureInfo.CurrentCulture;
            object httpRawUploadedContent = Activator.CreateInstance(hraw,
                                                                     BindingFlags.NonPublic | BindingFlags.Instance,
                                                                     null,
                                                                     argList,
                                                                     currCulture,
                                                                     null);

            Type contentType = httpRawUploadedContent.GetType();
            contentType.GetField("_completed", flags1).SetValue(httpRawUploadedContent, true);//不加上這句就會有問題

              FieldInfo dataField = contentType.GetField("_data", flags1);
            dataField.SetValue(httpRawUploadedContent, textParts);
         
            FieldInfo lengthField = contentType.GetField("_length", flags1);
            lengthField.SetValue(httpRawUploadedContent, textParts.Length);
            FieldInfo fileThresholdField = contentType.GetField("_fileThreshold", flags1);
            fileThresholdField.SetValue(httpRawUploadedContent, textParts.Length + 1024);
            info1.SetValue(request, httpRawUploadedContent);
            info2.SetValue(request, textParts.Length);

        }
    }
      4 在aspx頁面中加入一個,並修改Form的enctype = " multipart/form-data " , 測試一下吧

      好了,到這差不多就基本完成了,但是這樣只是實現了檔案的上傳,並沒有發揮HTTP模組真正的魅力。接下來一篇將向大家介紹如何實現進度條的顯示。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-605017/,如需轉載,請註明出處,否則將追究法律責任。

相關文章