??? ?有个项目(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.UploadStringCompleted?+=?new?UploadStringCompletedEventHandler(wc_UploadStringCompleted);?
-
????????????wc.UploadStringAsync(new?Uri("http://localhost/_vti_bin/Lists.asmx?op=AddAttachment",?UriKind.Absolute),?xmlrequest);
????? 但问题依旧,上传的文件过大时就返回“The remote server returned an error: NotFound”。
??? 注:
用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);
????? ?依然不能上传40M的文件。
5、 怀疑不是SharePoint的问题,我自己自定义一个Web Service让silverlight来调用
??? 至此,可能不是SharePoint的问题了,我们得把SharePoint抛开,估计是WebService的问题。因为我们只是把webservice请求以某个报文格式发送给服务器而已,一般只要服务器端接收到数据请求,即时处理失败,也不应该返回“未找到服务器”的错误。可能我们的请求服务器都没有正确接收到。
??????? 为此,我自己写个WebService取代SharePoint的
AddAttachment方法,而这个方法仅仅接收请求并只返回文件的大小,不进行任何实际的文件保存操作。
- ?? [WebMethod]?
-
????public?string?AddAttachment(byte[]?buffer)?
- ????{?
-
??????????????return?buffer.Length.ToString();????????
- ???}?
??? 同样,上传小文件时没问题,文件大了就失败。但至少证明了这个问题和SharePoint没有实质的关系,顶多是Web Service层面的问题。
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"?/>
??? .cs文件:?
-
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加大了数据量。 ?