WCF大文件传输解决之道(2)
按此帖实战:http://www.cnblogs.com/zhijianliutang/archive/2011/11/28/2265989.html
此篇文章主要是: 基于http协议应用于大文件传输中的应用,现在我们先解析下wcf中编码器的定义,编码器实现了类的编码,并负责将Message内存中消息转变为网络发送的字节流或者字节缓冲区(对于发送方而言)。在接收方,编码会将一系列字节转变为内存中的消息
在wcf中有三个编码器
1、TextMessageEncodingBindingElement
文本消息编码器是所有的基于Http的绑定的默认编码器,并且是最关注互操作性的所有的自定义绑定的正确选择。即为请求/应答模式,此编码器读取和编码标准为SOAP1.1/SOAP1.2的文本消息,而不会对二进制数据进行任何特殊处理,如果消息的MessageVersion设置为None,则SOAP信封包装从输出中省略,只有正文内容进行序列化。
2、MtomMessageEncodingBindingElement
MTOM消息编码器是也是一个文本编码器,实现对二进制数据的特殊处理,默认情况下在任何标准绑定中都不会使用,也就是说需要我们自己定义(一般定义在wsHttpBinding中),因为它是一个严格按具体情况进行优化的实用工具,只有当二进制数据的量不超过某个阀值时,MTOM编码才具有优势,如果信息包含的二进制数据超过某个阀值,则这些数据会外部化消息信封之后的MIME部分
3、BinaryMessageEncodingBindingElement
二进制消息编码器是Net*绑定的默认编码器,当通信双方都基于WCF时,此编码器始终是正确的选择。二进制消息编码使用.NET二进制XML格式,该格式是XML信息集(Information Sets,Infosets)的Microsoft特定二进制表示法,与等效的xml1.0表示法相比产生的需求量通常较小,并将二进制数据编码为字节流
每个标准绑定都包括一个预配置编码器,因此默认情况下Net*前缀的绑定使用二进制编码(通过包括BinaryMessageEncodingBindingElement类),而BasicHttpBinding和WSHttpBing类则使用文本信息编码器(通过TextMessageEncodingBindingElement类)
通常:
(1)文本信息编码是要求互操作性的任意通信路径的最佳选择,也就是通用性比较高
(2)二进制消息编码则是其他任意通信路径的最佳选择。通常,对于当个消息而言,二进制编码生成的消息要小于文本编码,并且在通信会话期间消息大小会逐渐变得更小。于文本编码不同的是,二进制编码不需要对数据进行特殊处理(例如,使用Base64),当会字节表示为字节如果数据无法分段,消息必须及时的方式传递或者当传输启动时数据为完全就绪,则应考虑启用流模式,而且只能对大型消息(带文本或者二进制)启用流模式无法执行信息正文的数字签名,因为他们需要整个消息内容进行哈希算法。采用流模式的情况下,当构造和发送消息头时,内容尚未完全就绪,因此无法计算数字签名。加密依赖于数字签名验证是否已经正确的重新构造数据
如果消息在传输过程中丢失,可靠的会话必须在客户端上缓冲已发送的消息以便可以重新传递,并且在将消息传递给服务实现之前必须在服务上保留信息以保证信息顺序。以备在未按顺序接受消息时可以按照正确的顺寻重新排列消息
下面我们通过一个上传文件的简单程序实现流文件的上传:
有几点我们需要注意:
1、在我们流文件上传的时候,需要定义文件的一些属性,这样我们就需要用消息契约代替数据契约方式
2、流文件上传的时候我们定义方法的时候只能保持一个参数,即消息契约。
第一步、新建文件消息契约
using System; using System.Collections.Generic; using System.ServiceModel; using System.IO; namespace streamFileUp { /// <summary> /// 消息契约(定义与SOAP消息相对应的强类型) /// 因为我们用流传输,所以用消息契约代替传统的数据契约 /// /// </summary> [MessageContract] public class FileWrapper { /// <summary> ///SOAP的消息头这里即为标记文件的路径 /// </summary> [MessageHeader] public string FilePath; /// <summary> /// SOAP消息的内容,指定成员序列化正文中的元素 /// </summary> [MessageBodyMember] public Stream FileData; } /// <summary> /// 返回结果 /// </summary> [MessageContract] public class result { [MessageBodyMember] public bool returnresult; } }
using System;
第二步,定义服务契约
using System; using System.ServiceModel; namespace streamFileUp { [ServiceContract] public interface IStreamed { /// <summary> /// 上传文件 /// </summary> /// 1、支持数据流传输的绑定有:BasicHttpBding、NetTcpBinding和NetNamedPipeBinding /// 2、数据流类型必须是可序列化的sream或MemorySream /// 3、传递时消息体(Message Body)中不可能包含其他数据,即参数只能有一个streamFileUp.FileWrapper /// <param name="fileWrapper">信息载体</param> [OperationContract] result UploadFile(FileWrapper fileWrapper); } }
第三步、实现类,注释很全,就不解释了
using System; using System.Collections.Generic; using System.IO; using System.ServiceModel; namespace streamFileUp { public class Streamed : IStreamed { /// <summary> /// 上传文件 /// </summary> /// <param name="fileWrapper">streamFileUp.FileWrapper</param> public result UploadFile(FileWrapper fileWrapper) { try { var sourceSream = fileWrapper.FileData; var targetSream = new FileStream(fileWrapper.FilePath, FileMode.Create, FileAccess.Write, FileShare.None); var buffer = new byte[4096]; var count = 0; while ((count = sourceSream.Read(buffer, 0, buffer.Length)) > 0) { targetSream.Write(buffer, 0, count); } targetSream.Close(); sourceSream.Close(); } catch (Exception) { return new result { returnresult = false }; } return new result { returnresult = true }; } } }
第四步、我们采取自托管,新建服务Nothing playing
using System; using System.ServiceModel; namespace streamFileUp { class Program { static void Main(string[] args) { using (ServiceHost host = new ServiceHost(typeof(Streamed))) { host.Opened += delegate { Console.WriteLine("服务已经启动"); }; host.Open(); foreach (var endpoint in host.Description.Endpoints) { Console.WriteLine(endpoint.Address.ToString()); } Console.ReadLine(); } } } }
第五步,这里是我们的配置文件,晒一下,注释很详细,记住是配置流传输
<?xml version="1.0"?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup> <system.serviceModel> <services> <!--name 提供服务的类名--> <!--behaviorConfiguraron 指定相关的行为配置 --> <service name="streamFileUp.Streamed" behaviorConfiguration="MessageBehavior"> <!--address - 服务地址--> <!--binding - 通信方式--> <!--contract - 服务契约--> <!--bindingConfiguration - 指定相关的绑定配置--> <endpoint address="Message/Streamed" binding="netTcpBinding" contract="streamFileUp.IStreamed" bindingConfiguration="StreamedBindingConfiguration" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://localhost:12345/Message/Streamed/"/> <add baseAddress="net.tcp://localhost:54321/"/> </baseAddresses> </host> </service> </services> <behaviors> <serviceBehaviors> <behavior name="MessageBehavior"> <!--httpGetEnabled - 使用get方式提供服务--> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="true"/> </behavior> </serviceBehaviors> </behaviors> <bindings> <netTcpBinding> <!--transferMode - 指示通道是使用流处理模式还是缓冲模式来传输请求和响应消息--> <!--maxReceivedMessageSize - 在采用此绑定配置的通道上可接收的最大消息大小(单位:字节)--> <!--receiveTimeout - 在传输引发异常之前可用于完成读取操作的时间间隔--> <binding name="StreamedBindingConfiguration" transferMode="Streamed" maxReceivedMessageSize="1073741824" receiveTimeout="00:10:00" /> </netTcpBinding> </bindings> </system.serviceModel> </configuration>
到此我们的服务端已经建立成功了,跑一下试试...
可以看到我们公开了两个服务地址,一个是net.tcp、一个是http...其实这里面http地址是引用服务的基地址,其传递方式还是采用
tcp方式的,一会我们通过客户端验证来下我们的推测。
下面我们开始新建客户端,来连接该服务:
第一步、新建类库,引用该服务,这里面有几点注意,在流的传输下我们客户端生成代码的时候服务地址是不能用上面的net.tcp...
我们需要引用http:......基地址生成:
第三步、实现客户端上传文件:
using System; using System.Collections.Generic; using System.ServiceModel; namespace client { class Program { static void Main(string[] args) { ///自定义绑定 string strAddress = "net.tcp://localhost:54321/Message/Streamed"; ChannelFactory<ServiceFileUp.IStreamed> factory = new ChannelFactory<ServiceFileUp.IStreamed>("NetTcpBinding_IStreamed", new EndpointAddress(strAddress)); ServiceFileUp.IStreamed service = factory.CreateChannel(); string filePath = @"G:\wcf学习测试案例\wcf大型数据传输\1.jpg"; string newFilePath = @"G:\wcf学习测试案例\wcf大型数据传输\2.jpg"; System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); //定义观察者 watch.Start(); ServiceFileUp.result returnResult = service.UploadFile(getSreamFromFile(filePath, newFilePath)); watch.Stop(); if (returnResult.returnresult) { Console.WriteLine("上传成功,上传时间为:" + watch.ElapsedMilliseconds); Console.ReadLine(); } else { Console.WriteLine("上传失败"); Console.ReadLine(); } } /// <summary> /// 流数据上传文件 /// </summary> /// <param name="file">源文件地址</param> public static ServiceFileUp.FileWrapper getSreamFromFile(string file, string newFilePath) { ServiceFileUp.FileWrapper myFileFw = null; try { var sr = new System.IO.FileStream( file, System.IO.FileMode.Open); ServiceFileUp.FileWrapper oneFW = new ServiceFileUp.FileWrapper() { FilePath = newFilePath, FileData = sr }; myFileFw = oneFW; Console.WriteLine("文件大小为:"+sr.Length.ToString()); //sr.Close(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } return myFileFw; } } }
我们在G盘下面放了一个jpg文件,然后通过上传同样的在该目录下存入我们刚才上传的文件,这里我们顺便监听了一下上传该文件需要的时间
下面晒一下结果:
说明文件上传是成功的,下面我们变换一种方式,以上方式我们是自定义了net.tcp地址的方式实现了文件的上传,我们改变一下直接用
http方式看看文件上传时候会变慢:
看运行结果:
呵呵...同样能实现上传的功能,但是性能的落后于基地址赋值地址,当然本身client方式就是通过ChannelFactory工厂创建,性能有所耗损是必然的,这里同样告诫我们:
在大文件传输的时候我们最好是通过自定义地址实现客户端配置,当然我们现在只是传递了一个简单的图片,对于大文件的上传我们需要更多的配置和性能优化。