近日,随公司的Silverlight项目进展,需要开发文件上传的功能,大致含 文件批量上传、压缩、界面友好 等要求。回想起我在2009年底使用Silverlight 3.0+WCF在VS2008 SP1的环境下开发过文件上传功能,当时就发现在Silverlight端将byte[]数据传递至WCF端时,一旦byte[]大于16KB,WCF端就拒绝接收返回Not Found错误,而且Silverlight端不管怎么配置都不管用,无奈之下只得将WCF服务分成两类,一类用于一次性写入小于16KB的文件;一类用于先创建16KB的文件头,然后再每次写入16KB的数据直至文件写入完成。于是这次开发首先便要突破这个16KB的限制,遂于CSDN上发贴求助,得到了冷秋寒等大虾的大力帮助,不胜感激,难得又到了周末,便把开发过程中的总结贴出来与众人分享,当然总结仅是一家之言,缺乏理论依据支持,还望诸大虾拍砖指正。
1. 使用 HttpHandler+WebClient 还是使用 WCF?
一开始就考虑过是否要使用Http+WebClient,这还是因为WCF的16KB限制,考虑到如果WCF每次只能接收16KB,则一个160KB的Excel文件便要分成10次才能上传完毕,要请求10次WCF服务,既降低了上传速度,又加大了WCF服务器的负担,想必使用Http的文件上传方案由来已久,早有成熟的方案,便向诸大侠请教。后得冷大侠和猛砍赵云兄指点,深夜在Codeplex下载了Silverlight Multi File Uploader 3.0 (http://slfileupload.codeplex.com/releases/view/30368),解压后不禁称奇,因为这个Silverlight Multi File Uploader 3.0中所使用的WCF服务的思路竟然是和我之前写过的WCF极为类似,只不过他简化为只有一类服务,每次只写入16KB的数据,并且通过SL端传入的参数判断是否为第一次写入、是否整个文件写入完成。运行Silverlight Multi File Uploader 3.0,使用Http方式,单次可以上传高达4MB的数据,而使用WCF,也是每次只能上传16KB,难怪该作者在Http方式的页面写道This page uses the HttpUploadHandler.ashx to upload files (faster but not so fancy)。难道只能选择Http方式了?
2. 如何配置WCF?
带着准备要采用Http方式的心,先行开发了文件上传的进度指示器,刚好冷大侠给出了配置WCF的提示,经过测试确定在使用VS2008或VS2010自动生成“启用了Silverlight支持的WCF服务”时,起作用的是maxArrayLength、maxReceivedMessageSize 这两个参数:
<bindings>
<basicHttpBinding>
<binding name="NewBinding0" maxReceivedMessageSize="2147483647" transferMode="Buffered">
<readerQuotas maxArrayLength="1024576" /> <!--1024576 为 1024KB-->
<security mode="None" />
</binding>
</basicHttpBinding>
<customBinding>
<binding name="customBinding0">
<binaryMessageEncoding>
<readerQuotas maxArrayLength="1073152"/> <!--1024576 为 1024KB-->
</binaryMessageEncoding>
<httpTransport maxReceivedMessageSize="2147483647"/>
</binding>
</customBinding>
</bindings>
如上所示,在使用basicHttpBinding时,设置maxArrayLength="1024576"则可以在SL端每次上传1024KB数据,而使用customBinding则需要多设置24KB,为maxArrayLength="1073152"才可以接收1024KB的数据,并且测试了几组配置值,24KB这个值保持不变。
WCF能配置的参数还有很多个,并且大多数参数VS2008给出的默认值就是最大值2147483647,如下的配置就是一个customBinding的最大配置:
<customBinding>
<binding name="customBinding0">
<binaryMessageEncoding maxReadPoolSize="2147483647" maxSessionSize="2147483647" maxWritePoolSize="2147483647" >
<readerQuotas maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxDepth="2147483647" maxNameTableCharCount="2147483647" maxStringContentLength="2147483647"/>
</binaryMessageEncoding>
<httpTransport maxReceivedMessageSize="2147483647" maxBufferPoolSize="2147483647" maxBufferSize="2147483647" keepAliveEnabled="true" />
</binding>
</customBinding>
3. 回答1.的问题
在解决了WCF的配置问题后,便可以就HttpHandler和WCF作比较了,想必Silverlight Multi File Uploader 3.0的作者也是像我上样不知道如何配置WCF,才会写出This page uses the HttpUploadHandler.ashx to upload files (faster but not so fancy)来。
也就是说在速度上,WCF和HttpHandler是同样的fast,并且WCF不会not so fancy,WCF可以配置的地方多着呢。并且,WCF可以不用选择IIS作为Host,完全可以自己写一个文件上传的Host,灵活性高,安全性也高。
4. 上传速度的问题
解决了单次允许上传的最大值(以下称为Buffer)问题后,速度问题并没有解决,在本机测试,Buffer明显是2MB-4MB为好,SATA II硬盘的速度是3Gb/s,拆合300MB/s,4MB的Buffer显然不成问题。
在局域网测试,结果可能就大一样了,在使用RJ45跳线后直连的两台PC组的局域网、使用HUB连接多台电脑的局域网、使用有线路由器连接的局域网、使用无线路由器连接的局域网下结果不尽相同。在前二者中,由于没有路由器,100Mb或1000Mb的网线连接时,Buffer同样是2MB-4MB为好,基本感觉和本机测试一样;但是在后二者中,特别是无线路由器,由于对数据包的大小能进行控件,并且有时数据包的最大值小得可怜,可能是Buffer为31-32KB时达到最佳速度(不过也就450KB/s-500KB/s,远低于没有路由器时的情况),也有可能是Buffer为512KB或256KB时达到最佳速度,在于路由器的心情了----心情不好时,设置个2MB的Buffer,结果路由器要用个1分钟来拆分数据包,OMG,和用Internet进行测试的效果极为相同。
在Internet进行测试,呃,没有那样的环境,我是在家用ADSL拨号(1Mb的接入带宽),把IP地址和端口告诉朋友让朋友测试的,在只有一个人测试的情况下,Buffer设置为较小的值时进度条很流畅,设置为512KB时取得了较快的传输速度48KB/s-50KB/s,而设置为2MB时,速度和512KB的差不多,但是进度条就极不友好了。
5. 文件压缩
文件压缩当然是要考虑的问题,因为项目是要在Internet上使用的,并且上传的文件主要为Office文件。压缩文件首选是ICSharpCode.SharpZipLib,it is A free C# compression library ,支持byte压缩、文件压缩、目录压缩,之前在项目中使用的是ICSharpCode.SharpZipLib的Silverlight版本,程序集名称为SharpZipLib,只支持byte压缩,由于时间关系,我没有去找Silverlight下的支持文件压缩的ICSharpCode.SharpZipLib。
由于文件上传实际是分成多次上传Buffer大小的byte[],这就为使用byte压缩指明了思路,另外设置一个压缩Buffer,视客户端PC的压缩处理能力,一般设置为1MB至4MB都没有问题,文件上传的过程变为“依次读取数据至压缩Buffer,SharpZipLib对压缩Buffer中的byte[]进行压缩,返回压缩后的byte[],再将该byte[]发送至WCF,WCF接收了byte[]后,通过SharpZipLib解压得出未压缩的byte[],并附加至目的文件的末尾”。 这种方式有几个问题:
使用了压缩之后,上传Office文件速度明显加快了,视乎Office文件的压缩比而定,下图是Internet测试的结果:
除了可以指定压缩Buffer外,还应当能识别哪些文件该压缩,哪些文件由于压缩比过低而不该压缩,像.rar、.exe、.jpg等就不该再进行压缩了(实际上Office 2007文件也不该压缩,因为本身就比Office 2003的文件小得多)。
但是在局域网使用时(不考虑路由器拆分数据包的情况),使用压缩反而影响速度,因为每次上传的数据量是压缩Buffer压缩后的byte[],可能1MB的压缩Buffer每次只上传几十KB的byte[]。
6. Silverligth端文件上传并发管理
这个就简单多了,写一个Silverlight端的管理器,注册各个文件上传类委托管理文件上传类的上传。
Silverlight端的UML类图如下:
7. Silverligth端文件上传指示控件
功能:显示上传进度、估算剩余时间、测算平均速度、显示已用时间、显示是否使用压缩等。
使用模板化控件,设置两种VisualStateGroup,一种用于互斥的“待上传、上传中、已完成(取消)、出现异常”,另一种用于显示“压缩中、压缩完成”。
关于控件的依赖问题,个人认为这个上传指示控件应是只负责显示,不具备文件上传功能,使用过Silverlight+WCF的人都知道,Silverlight中的WCF调用是异步调用,前台界面中显示诸如“请稍等”等提示性UI纯粹是为了提高用户感,所以对于文件上传也是如此,没有进度指示界面文件也同样可以上传。 在结构上,这个控件不依赖于任何上传class,反过来是上传class组合了这个控件。
8. 关于Silverligth端文件上传指示控件的BUG
这是最终在项目中绑定至ListBox或DataGrid中使用时才发现的,而且是很奇怪的BUG,作为ListBox或DataGrid的ItemTemplate.DataTemplate,在同时选择了多个上传文件导致ListBox或DataGrid出现滚动条时,用鼠标上下滚动滚动条后,Silverlight便会出错,用鼠标滚轮也会出错(仅SL4.0),但是用键盘上下方向键滚动则不会出错,在SL 3.0、SL 4.0中情况相同。但是更为奇怪的是不使用ListBox或DataGrid,而是使用ItemsControl则不会出错,怪事啊。