接著分析了幾個小時的SunriseUpload.0.9.1的源碼。
終於明白了作者的整體思路。在此就做一個總結。
首先,要想能上傳很大的檔案,我們就必須編寫一個HttpModule來自己處理使用者上傳的資訊。這個模組可以攔截使用者所有的請求,因此有必須選擇性的做一此判斷。如果是mulitypart/form-data請求時,會有這樣的一個ContentHeader在請求資料裡:
multipart/form-data; boundary=---------------------------7d51a51e25012c
我們可以從m_application.Request.ContentType.ToLower()的方法取得這個資料,注意boundary後面的資料是隨機的,它是用來區分上傳變數名與資料的分隔字元,因此為了能分析後面的資料,必須先把它取出來。
然後就是從使用者的請求那裡讀取資料,第一次讀取資料的時候,我們可以用:
m_workRequest.GetPreloadedEntityBody();
其中m_workRequest是HttpWorkerRequest的對象執行個體。如果使用者提交的資料不是很長,那麼一次性就可以讀取完了。而我們最主要的目的就是為了分析這裡的資料。
直接輸出裡面的資料可以得到類似這樣的內容,為了方便說明,我加上了行號:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="__VIEWSTATE"
[03]
[04]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[07]Content-Type: image/x-png
[08]
[09]?PNG
IHDR ] & |??á gAMA ˉè7?é(這裡是上傳的檔案位元據,我刪除了一些)
[10]-----------------------------7d51f321004ec
[11]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[12]Content-Type: image/x-png
[13]
[14]?PNG
IHDR ] & |??á gAMA ˉè7?é tEXtSof(這裡是上傳的檔案位元據,我刪除了一些)
[15]-----------------------------7d51f321004ec
[16]Content-Disposition: form-data; name="Button1"
[17]
[18]Button
[19]-----------------------------7d51f321004ec--
這裡,亂碼是上傳的二進位檔案(刪除了一些,而且假設有兩個檔案上傳)。可以看到,其中有一個__VIEWSTATE(02行)表單變數名,它是ASP.net自己維護的資訊,我們就不說了。還有一個Button1(16行),它的值是Button,其它的是兩個上傳的二進位檔案。注意,這裡的分隔字元比ContentType裡的多兩個位元組,而這兩個位元組就是"--",數一下就知道了,後面的多兩個。不知道為什嗎?我們可以看到,第09行的資料和第14行的資料就是我們上傳的檔案,這裡我刪除了大部分,只留了一點點做例子。看看作者的思想,作者想在這些資料到達頁面以前,我們先把它處理一次,作者是這樣處理的,他想讓用我們的模組處理後的內容變成下面的樣子,為了方便說明,我加上了行號:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="Sunrise_Web_Upload_UploadGUID"
[03]
[04]d922d57d-c1ef-4c02-a3d4-30fae78cb599
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="__VIEWSTATE"
[07]
[08]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[09]-----------------------------7d51f321004ec
[10]Content-Disposition: form-data; name="m_file"
[11]
[12]Content-Type: image/x-png;filename="D:\WuCountry\Pictures\logo.png";filepath="a661ab10-312e-4372-93e6-f37d662ca38f.png"
[13]-----------------------------7d51f321004ec
[14]Content-Disposition: form-data; name="m_file"
[15]
[16]Content-Type: image/x-png;filename="D:\WuCountry\Pictures\logo.png";filepath="sd328d7a-312e-4372-93e6-f37d662ca38f.png"
[17]-----------------------------7d51f321004ec
[18]Content-Disposition: form-data; name="Button1"
[19]
[20]Button
[21]-----------------------------7d51f321004ec--
其中多了一個Upload_GUID,它是用來唯一分一個上傳任務的,我們可以不管它,主要是為了處理上傳進度條。好了,這樣一來,所有的資料都是文本的了,那麼二進位的檔案到什麼地方去了呢?就是filepath="a661ab10-312e-4372-93e6-f37d662ca38f.png"這就記錄了檔案的位置,也是用GUID產生的唯一檔案名,它存放在系統的臨時目錄裡,也就是說,還在我們自己去把它COPY到想存放的目錄,這是很容易的,用一個Move就行了。
好了,思路已經很明確了,那麼就只用來處理資料了,也就是上一篇文章的核心ReqponseStream類的演算法。
這裡請注意,因為資料並不是一次提交上來的,上面的資料在任何一個地方都有可能出現斷點問題,因此我們不防假設第一次資料斷在源請求檔案的第09行的某個位置,那麼我們用同m_workRequest.GetPreloadedEntityBody();取得的資料可能就是這樣的:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="__VIEWSTATE"
[03]
[04]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[07]Content-Type: image/x-png
[08]
[09]?PNG
IHDR ]
而後我們用m_workRequest.ReadEntityBody(m_readBuffer,m_bufferSize);取得的資料可能是這樣的,我們再假設第二個斷點在14行
[09] ] & |??á gAMA ˉè7?é(這裡是上傳的檔案位元據,我刪除了一些)
[10]-----------------------------7d51f321004ec
[11]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[12]Content-Type: image/x-png
[13]
[14]?PNG
IHDR ] &
更多可能的是取得的資料全部是檔案資料,也就是全部是二進位。那麼應該怎樣處理這個問題呢?通過研究作者的演算法,作者是這樣做的:
RequestStream類有一個m_contentTextBody(這是我自己取的名字),它用來處理完讀取的資料後記錄迴文本資訊,也就是說最後這裡面的內容就是我們想產生的所有的常值內容。然後把檔案存在在臨時檔案夾裡,但由於檔案可能沒有傳完,所以在RequestStream類裡還必須記錄上傳檔案的一些資訊。作者想在第二次提交的資料處理的時候,再把前面的m_contentTextBody再傳回RequestStream類來處理,也就是說第二次處理資料時,原本應該全部都是位元據了,但作者把資料改為這樣的:
[01]-----------------------------7d51f321004ec
[02]Content-Disposition: form-data; name="__VIEWSTATE"
[03]
[04]dDwtNTMwNzcxMzI0Ozs+AsSfEXPXvGi5+b7dOBAso7F1wlU=
[05]-----------------------------7d51f321004ec
[06]Content-Disposition: form-data; name="m_file"; filename="D:\WuCountry\Pictures\logo.png"
[07]Content-Type: image/x-png
[08]
[09]?PNG
IHDR ] & (這一行是第二次讀取時真正的資料,前面的8行都是添加上去的)
這樣一來,就感覺又是一個新的請求了,可以當成是第一次請求那樣處理資料。但這次不能重新開啟新的檔案流寫入資料,而應該還用上一次的檔案流來寫入上一次的檔案中。當遇到一個檔案結束的時候,關閉前一個檔案。如果遇到第二個檔案就再來開啟一個檔案流來填寫資料。
我覺得這是很不好的,因為每次把資料當成是第一次的樣子來處理,這樣很浪費資源,可以看的出來,每次都要多處理好多位元組的資料。而在大檔案上傳的時候更是不能忽略這些資料了,而在演算法裡,把位元組數組的複製也搞的很複雜。這是我覺得作者最不可取的地方。因為每次處理讀取的資料,都會NEW一個RequestStream對象,這樣太浪費資源了。(我決定重新改寫這一演算法。)
好了,最後一個任務就是把m_contentTextBody添加到RequestContent裡去,否則我們不能在後來的處理中得到資料,因此還有一點麻煩,作者用到了C#的反射功能,得到IIS的應用程式定義域(我還不知道是不是),然後添加進資料,這是核心代碼:
private byte[] InjectTextParts(HttpWorkerRequest request, byte[] textParts)
{
Type type;
BindingFlags flags = (BindingFlags.NonPublic | BindingFlags.Instance);
//Is there application host IIS6.0?
if (Utils.GetContext().Request.ServerVariables["SERVER_SOFTWARE"].Equals("Microsoft-IIS/6.0"))
{
type = request.GetType().BaseType.BaseType;
}
else
{
type = request.GetType().BaseType;
}
int dataLength = textParts.Length;
//Set values of working request
type.GetField("_contentAvailLength", flags).SetValue(request, dataLength);
type.GetField("_contentTotalLength", flags).SetValue(request, dataLength);
type.GetField("_preloadedContent", flags).SetValue(request, textParts);
type.GetField("_preloadedContentRead", flags).SetValue(request, true);
return textParts;
}
好了,還只剩下最後一個,就是上傳進度條的處理問題了。在後面的文章裡會繼續分析吧,其實上我也開始在寫自己的上傳組件了,當然一些技術上的方法還得用作者的思想,但一些演算法或者一些我覺得自己有突破的地方,我會做一些修改的。
文章來源:http://computer.mblogger.cn/wucountry/posts/48662.aspx