有個項目(Submition project)是利用Silverlight做個表單將數據提交到SharePoint的List裏,表單除了填寫一些信息外還可以上傳附件,但遇到的問題是上傳附件時文件如果比較大就上傳失敗。
該項目的大致思路是用Silverlight收集數據,然後再通過javascript調用SharePoint的相關WebService來上傳數據。相關代碼如下:
1、Silverlight獲取選擇的待上傳的文件內容:
- OpenFileDialog openFileDialog = new OpenFileDialog();
- if ((bool)openFileDialog.ShowDialog())
- {
- FileInfo fileInfo = openFileDialog.File;
- Stream stream = fileInfo.OpenRead();
- byte[] buffer = new byte[stream.Length];
- stream.Read(buffer, 0, buffer.Length);
- string strFileContent = Convert.ToBase64String(buffer);
- stream.Close();
- }
即在Silverlight中獲取了選擇的文件的內容並轉化成了一個字符串,以便在JavaScript裏調用。
2、利用Javascript上傳文件到SharePoint,即利用SharePoint的WebService Lists.amsx裏的AddAttachment方法:
- function AddAttachment(listName, listItemID, fileName, strFileContent){
- var xmlrequest;
- xmlrequest = SoapPrefix + '<AddAttachment xmlns="http://schemas.microsoft.com/sharepoint/soap/"><listName>' +
- listName + '</listName><listItemID>' + listItemID + '</listItemID><fileName>'
- + fileName + '</fileName><attachment>' + strFileContent + '</attachment></AddAttachment>'
- xmlrequest += SoapPostfix;
- if (!xmlhttpLists)
- xmlhttpLists = GetHTTPObject();
- xmlhttpLists.open("POST", "http://localhost/_vti_bin/Lists.asmx?op=AddAttachment", false);
- xmlhttpLists.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
- xmlhttpLists.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/sharepoint/soap/AddAttachment");
- xmlhttpLists.setRequestHeader("Content-Length", xmlrequest.length);
- xmlhttpLists.send(xmlrequest);
- return xmlhttpLists.readyState + " -- " + xmlhttpLists.status;
- }
注:listItemID是先前調用WebService的UpdateListItems方法添加一條記錄到SharePoint的List後返回的一個ListID。
當文件比較小時上傳能夠成功,但文件超過30M就上傳失敗,並拋出錯誤“遠程服務器不可調用”、“The remote server returned an error: NotFound”等,返回的http狀態碼爲500。爲查找問題根源,我嘗試了以下方法:
1、懷疑Silverlight的C#代碼沒有將strFileContent傳遞給JavaScript
在測試過程中,發現10~30M的文件上傳沒有問題,但當文件超過40M的時候就失敗。跟蹤調試後,上傳40M的文件時在c#裏嘗試查看strFileContent的內容,但顯示“Unable to evaluate the expression. 存儲空間不足,無法完成此操作”。可能是因爲strFileContent的長度太長,內存有限而無法正常顯示。會不會因此而使得strFileContent不能正確傳遞給JavaScript導致上傳失敗?我在JavaScript的AddAttachment函數開始時就加入顯示strFileContent長度的調試語句,結果顯示JavaScript已經獲得了完整的文件內容,所以可以排出這個可能。實際上string是沒有長度限制的,大小取決於內存,至少到幾個G是沒問題的。
2、懷疑JavaScript受內存限制不能上傳大文件,嘗試直接在Silverlight裏用C#的webclient直接調用SharePoint的Web Service上傳文件
那是不是JavaScript因爲受某種限制(例如內存)而導致不能上傳大文件的呢? 爲此,我改變用JavaScript調用webservice的方式,而在c#中直接用webclient調用webservice上傳附件。
- string xmlrequest = "<?xml version=/"1.0/" encoding=/"utf-8/"?><soap:Envelope xmlns:xsi=/"http://www.w3.org/2001/XMLSchema-instance/" xmlns:xsd=/"http://www.w3.org/2001/XMLSchema/" xmlns:soap=/"http://schemas.xmlsoap.org/soap/envelope//"><soap:Body>" + "<AddAttachment xmlns=/"http://schemas.microsoft.com/sharepoint/soap//"><listName>FileList</listName><listItemID>4</listItemID><fileName>testFileName</fileName><attachment>" + strFileContent + "</attachment></AddAttachment>";
- xmlrequest += "</soap:Body></soap:Envelope>";
- WebClient wc = new WebClient();
- wc.Headers["Content-Type"] = "text/xml; charset=utf-8";
- wc.Headers["SOAPAction"] = "http://schemas.microsoft.com/sharepoint/soap/AddAttachment";
- //wc.Headers["Content-Length"] = xmlrequest.Length.ToString();
- wc.UploadStringCompleted += new UploadStringCompletedEventHandler(wc_UploadStringCompleted);
- wc.UploadStringAsync(new Uri("http://localhost/_vti_bin/Lists.asmx?op=AddAttachment", UriKind.Absolute), xmlrequest);
注:用Silver light的webclient調用WebService需要在SharePoint的站點目錄下存放“clientaccesspolicy.xml”文件,否則訪問會被拒絕。如果用JavaScript訪問webservice,則不須配置策略文件,但也要求和sharePoint在同域下。
3、 懷疑是SharePoint的問題,嘗試直接在SharePoint裏手工操作上傳大附件
是不是SharePoint本身的問題呢?我嘗試直接在SharePoint裏手工操作上傳40M的文件,沒有問題,上傳成功了。甚至稍大點文件都沒問題(50M以下)。莫非是訪問Web Service的方式出問題了?
4、 嘗試在Silver light裏用C#直接引用SharePoint的Web Service( Add Service reference)並調用其方法上傳附件
我改變一下調用webservice的方式,不用JavaScript的xmlhttp也不用Silverlight的webclient,而直接在silverlight project裏Add Service reference生成代理,直接用代理來調用webservice:
- SRLists.ListsSoapClient lsc = new UploadFileToSharePointLists.SRLists.ListsSoapClient();
- lsc.AddAttachmentCompleted += new EventHandler<UploadFileToSharePointLists.SRLists.AddAttachmentCompletedEventArgs>(lsc_AddAttachmentCompleted);
- lsc.AddAttachmentAsync("FileList", "4", "testFileName002", buffer);
5、 懷疑不是SharePoint的問題,我自己自定義一個Web Service讓silverlight來調用
至此,可能不是SharePoint的問題了,我們得把SharePoint拋開,估計是WebService的問題。因爲我們只是把webservice請求以某個報文格式發送給服務器而已,一般只要服務器端接收到數據請求,即時處理失敗,也不應該返回“未找到服務器”的錯誤。可能我們的請求服務器都沒有正確接收到。
爲此,我自己寫個WebService取代SharePoint的AddAttachment方法,而這個方法僅僅接收請求並只返回文件的大小,不進行任何實際的文件保存操作。
- [WebMethod]
- public string AddAttachment(byte[] buffer)
- {
- return buffer.Length.ToString();
- }
6、 終於查到與web.config中的maxRequestLenght配置有關,但上傳文件小於配置指定大小時還失敗
這引起了我進一步的思考,WebService的發送請求對數據量有限制麼?如果參數很多,數據量很大,怎麼把這些請求發送到服務器端?我們的問題實質也是調用Web Service時參數數據量過大的問題。
這讓我想起了Asp和.net裏上傳文件時有大小限制的問題,實際上我早應該想起這個,只是以前對這個認識得不是非常清晰而已。從服務器的角度來說,客戶端的請求都是一堆http數據包,有httpheader和http body之類的(對於webservice這麼說不嚴謹),服務器是需要限制請求的http包的大小的,否則容易遭到攻擊。而web.config裏就有這個配置項:
- <configuration>
- <system.web>
- <httpRuntime maxRequestLength="1048576" executionTimeout="3600" />
- </system.web>
- </configuration>
其中的maxRequestLength屬性就限制了請求數據包的最大長度。我嘗試更改我的web.config裏maxRequestLength的值,當這個值很大時,確實能上傳40M的數據。問題有點眉目了,接着查找SharePoint站點下對應的web.config中該節配置屬性,發現maxRequestLength是51200,剛好是50M。但爲什麼我上傳40M的數據時就不行呢?
注:maxRequestLength:指示 ASP.NET 支持的HTTP方式上載的最大字節數。該限制可用於防止因用戶將大量文件傳遞到該服務器而導致的拒絕服務攻擊。指定的大小以 KB 爲單位。默認值爲 4096 KB (4 MB)。
executionTimeout:指示在被 ASP.NET 自動關閉前,允許執行請求的最大秒數。還有其他相關屬性可以參考MSDN。該屬性的默認配置在Machine.config裏。
另外,服務器端對內存使用也是有限制的,可以參考以下配置節:
<configuration>
<system.web>
<processModel memoryLimit="80" />
</system.web>
</configuration>
7、 基本鎖定是由web.config對上傳文件大小限制造成的,所以在Asp.net裏用簡單的FileUpload來測試
在上面已經基本確定了是maxRequestLength的設置問題,但當maxRequestLength設置成50M的時候,實際上傳不了50M,甚至40M都上傳不了。莫非這中間有誤差?誤差也太大了。於是就在Asp.Net裏測試,在asp.Net裏放一個FileUpload控件,後臺僅計算文件的大小:
.Aspx文件內容:
- <asp:FileUpload ID="fu" runat="server" />
- <asp:Button ID="btn" runat="server" Text="Upload File" onclick="btn_Click" />
- protected void btn_Click(object sender, EventArgs e)
- {
- int len = fu.PostedFile.ContentLength;
- }
確實,上傳文件大小超過maxRequestLength時,出現“服務器不可訪問”之類的錯誤,與前面的錯誤基本類似。如果將maxRequestLength配置爲50M,也上傳不了50M,但可以上傳49.99M。誤差明顯減小了好多。
8、 用Fiddler來跟蹤數據包,真相大白
爲了徹底弄清楚上面的問題,我們最好能跟蹤到數據傳遞過程中的Http數據包的發送信息,能看看到底發了多大的數據包。爲此,我安裝Fiddler來進行測試。
1) 在asp.net裏用FileUpload上傳文件時,傳遞的http數據包比選擇的文件稍大。通過fiddler監測可以看到,發送的數據部分與選擇文件大小一樣大,但http header多了viewstate與_evengName等內容(這些是Asp.Net機制所必需的,我寫得不準確),所以加起來比文件大小稍微大一點點,這也是maxRequestLength設置爲50M但實際傳遞的文件只能49.99M的原因了。
2) 在Silverlight裏調用WebService上傳文件時,通過Fiddler監測,發現http請求的數據部分內容遠比實際文件大小大。例如上傳20M的文件,http請求包中body部分數據大小將近27M。我自己自定義一個文本文件,內容爲“abcd123456”,上傳該文件時發現http請求中文件內容變成了“YQBiAGMAZAAxADIAMwA0ADUANgA=”,確實加大了文件大小。在FileUpLoad中上傳時是明文的,調用WebService時怎麼改變了文件內容呢?莫非加密了?後來才發現這不是加密,而是Convert.ToBase64String的結果。將byte[]的內容轉化成Base64String雖然變成字符了,但長度確實加大了很多。儘管有點的時候我們傳遞給webservice的參數直接是byte[],沒有顯式Convert.ToBase64String,但在傳遞過程中還是自動變成Base64String了,大大加大了傳遞的內容。
至此,已經搞清楚爲什麼SharePoint設置的上傳最大允許50M,但實際通過WebService上傳40M都不成功的原因了。
************************************************************************************************************
關於Fiddler2:
Fiddler是IE上監視http數據包的一個很好的調試插件,但在使用過程中發現對localhost的請求它卻不理睬,通過網上查找原來可以用以下方式來讓fiddler也攔截本機的數據包:
http://localhost:81/myweb ----------------à http://localhost.81/myWeb (localhost後面緊接着加一個點號”.”,引用webservice也是如此)。
順便付上相關的一片短文:
============================================================================
IE7 and the .NET Framework are hardcoded not to send requests for Localhost through proxies. Fiddler runs as a proxy. The workaround is to use your machine name as the hostname instead of Localhost or 127.0.0.1.
So, for instance, rather than hitting http://localhost:8081/mytestpage.aspx, instead visit http://machinename:8081/mytestpage.aspx. Alternatively, you can use http://localhost.:8081/mytestpage.aspx (note the trailing dot after localhost). Alternatively, you can customize your Rules file like so:
static function OnBeforeRequest(oSession:Fiddler.Session){
if (oSession.host.ToUpper() == "MYAPP") { oSession.host = "127.0.0.1:8081"; }
}
...and then navigate to http://myapp, which will act as an alias for 127.0.0.1:8081.
我比較喜歡localhost. 方式。
如果你使用IIS7的話,還可以自己定製
<system.net>
<defaultProxy>
<proxy proxyaddress="http://127.0.0.1:8888" />
</defaultProxy>
</system.net>
這樣,就會通過代理來訪問了,從而Fiddler就能捕獲到數據了。
************************************************************************************************************
最後來總結一下:
1、問題的關鍵是maxRequestLength限制了http請求的最大數據大小, maxRequestLength指的是http請求所有數據總和(htt頭已經內容) ,而不是僅僅普通概念上的文件;
2、Byte[]轉換成Base64String加大了數據量。