近日,随公司的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)来。
使用HttpHandler,单次上传的最大值稍大于4MB,为4128KB,不能超过 HttpRequest.InputStream.Length 的最大限制;
使用WCF,在CustomBinding的情况下,单次上传的最大值比4MB稍小一些;
使用WCF,在BasicHttpBinding的情况下,(估计和使用HttpHandler相同);
使用HttpHandler和使用WCF相同,当上传的文件大于客户端所设定的单次上传最大值时,也是拆分为多次请求HttpHandler,HttpHandler区分处理第一次上传、中间过程的上传、最后一次上传;
对于服务端处理第一次上传之后的上传,有一个细节是如何找到上次上传的文件,从文件的末尾接着写入,我采取的方式是服务端在第一次上传后把最终确定的文件名传回至客户端(在服务中使用ref参数),客户端在续传时均以该文件名作为参数通知服务端找到文件并seek至末尾。
也就是说在速度上,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[],并附加至目的文件的末尾”。 这种方式有几个问题:
在Silverlight端没有生成zip文件,压缩只是对每次上传的数据进行压缩,并非对整个文件进行压缩(不过要对整个文件进行压缩时,Silverlight要求要使用SaveFileDialog来打开FileStream,否则是不能写硬盘的,要不就只能使用独立存储,但是独立存储显然不适合用来存储大文件);
单次上传所设定的Buffer失去了作用,因为每次上传的是压缩后的byte[],大小只有在压缩后才能确定;
要保证压缩后的byte[]的大小不能超过WCF所允许的单次接收的最大值(见前面所述),更不能大于4MB;
在WCF端同样也没有生成zip文件,而且是生成和源文件完全一样的文件。
使用了压缩之后,上传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则不会出错,怪事啊。