基本考虑事项
WCF是基于XML的通信基础结构。
编码数据:文本和二进制
最常有的顾虑包括:认为与二进制格式相比XML的开销比较大一些(因为其开始标记和结束标记的重复性),数值的编码可能要大得多(因为他们是以文本值来表示的),并且无法有效的表示二进制数据(因为它们必须进行特殊的编码才能嵌入到文本格式中)。
尽管上述以及其他许多类似的问题是存在的,但是XML Web Services环境中的XML文本编码消息与旧样式的远程过程调用(RPC)环境中的二进制编码消息之间的实际差异,通常远远小于最初的考虑中所预想的程度。
虽然XML文本编码消息是透明的并且是"可读的"(也许难以理解),但比较而言,二进制消息则通常相当晦涩,在无工具的情况下很难解码。
要在文本与二进制之间进行选择,不能简单地认为二进制消息总是小于XML文本消息。
XML文本消息的一个明确而无可争议的优点是它们基于标准的,并且提供最为广泛的互操作性选项和平台支持选择范围。
二进制内容
从最终的消息大小这一角度而言,二进制编码优于基于文本编码的一个方面就在于大型二进制数据项,例如,图片、视频、音效剪辑或者必须在服务与其使用者之间交换的任何其他形式的非透明二进制数据。为了使这些类型的数据也适合XML文本,常用的方法就是使用Base64编码对其进行编码。
在Base64编码字符串中,每个字符都表示原始8位数据的6位,这导致Base64的编码开销比率是4:3,且未计算额外的格式字符(回车符/换行符),而按照惯例这些字符通常是会添加的。虽然XML编码与二进制编码之间的差异显著与否通常取决于具体情况,但是当传送500MB负载时大小增加超过33%通常是不受欢迎的。
为避免这种编码开销,消息传输优化机制(MTOM:Message Transmission Optimization Mechanism)标准允许将消息中包含的大型数据元素外部化,并将其作为无任何特殊编码的二进制数据随消息一起传送。利用MTOM,消息将以一种与带附件或嵌入式内容(图片和其他嵌入式内容)的简单邮件传输协议(SMTP)电子邮件类似的方式交换;MTOM消息会打包成多部分/相关MIME(多用途因特网邮件扩展(Multipurpose Internet Mail Extensions))序列,其中根部分是实际的SOAP消息。
但是,与Base64一样,对于MIME格式,MTOM也有一些必要的开销,这样仅在二进制数据元素的大小超过大约1KB时,才能体现出使用MTOM的好处。由于这一开销,如果二进制负载在该阈值之下,则MTOM编码的消息可能会大于对二进制数据使用Base54编码的消息。
大型数据内容
即使不考虑线路需求量,前面提及的500MB负载也对服务和客户端本身提出了很大的考验。默认情况下,WCF会以缓冲模式处理消息。这意味着消息的整个内容在发送前或接收后都存在于内存中。尽管对于大多数情形来说这是个很好的策略,并且是消息传递功能(如数字签名)和可靠传递所必须的,但是大型消息可能很容易最终耗尽系统资源。
处理大型负载的策略是流。尽管消息(尤其是以XML表示的消息)通常会被认为是相对紧凑的数据包,但消息大小也可能达到GB数量级,从而与连续的数据流很相似,而不是易于处理的数据包。当以流模式而不是缓冲模式传输数据时,发送方会议流的形式将消息正文的内容提供给接收方,并且消息基础结果会不断地将就绪的数据从发送发转发给接收方。
传输此类大型数据内容的最常见情形是传输具有以下特点的二进制数据对象:
1) 无法方便地分成消息序列。
2) 必须以及时方式传递。
3) 当开始传输时,还不是已全部就绪。
对于不具有上述限制条件的数据,通常最好在一个会话的范围内发送消息序列,而不是一次性地发送一个大消息。
编码
编码定义了一组规则,规定消息在线路中的存在形式。把我们的消息如何编码,然后在传输。
编码器实现此类编码,并负责将Message内存中消息转变为可以通过网络发送的字节流或字节缓冲区(对于发送方而言)。在接收方,编码器会将一系列字节转变为内存中的消息。
WCF包括三个编码器:
1) TextMessageEncodingBindingElement:
文本消息编码器是所有基于HTTP的绑定的默认编码器,并且是最关注互操作性的所有自定义绑定的正确选择。此编码器读取和编码标准SOAP 1.1/SOAP 1.2文本消息,而不会对二进制数据进行任何特殊处理。如果消息的MessageVersion设置为None,则SOAP信封包装会从输出中省略,只有消息正文内容会进行序列化。
2) MtomMessageEncodingBindingElement:
MTOM消息编码器是一个文本编码器,实现对二进制数据的特殊处理,默认情况下在任何的标准绑定中都不会使用,因为它是一个严格按具体情况进行优化的实用工具。只有当二进制数据的量不超过某个阀值时,MTOM编码才具有优势,如果消息包含的二进制数据超过了这个阀值,则这些数据会外部化到消息信封之后的MIME部分。
3) BinaryMessageEncodingBindingElement:
二进制消息编码器是Net*(以.NET打头的,例如.NET.TCP,.NET.TCP,NET.Pipe)绑定的默认编码器,当通信双方都基于WCF时,此编码器始终是正确的选择。二进制消息编码器使用.NET 二进制XML格式,此格式是XML信息集(Information Sets,Infosets)的Microsoft特定二进制表示法,与等效的XML 1.0表示法相比产生的需求量通常较小,并将二进制数据编码为字节流。
每个标准绑定都包括一个预配置编码器,因此默认情况下带Net*前缀的绑定使用二进制编码器(通过包括BinaryMessageEncodingBindingElement类),而BasicHttpBinding和WSHttpBinding类则使用文本消息编码器(通过TextMessageEncodingBindingElement类)。
通常,文本消息编码是要求互操作性的任意通信路径的最佳选择,而二进制消息编码则是其他任意通信路径的最佳选择。通常,对于单个消息而言,二进制消息编码生成的消息大小要小于文本编码,并且在通信会话期间消息大小会逐渐变得更小。与文本编码不同的是,二进制编码不需要对二进制数据使用特殊处理(例如,使用Base64),但会将字节表示为字节。
启用MTOM
当要求互操作性,并且必须发送大型二进制数据时,MTOM消息编码是一个备选的编码策略,可以在标准BasicHttpBinding或WSHttpBinding绑定上启用它,方法是:将该绑定的MessageEncoding属性设置为Mtom,或者将MtomMessageEncodingBindingElement编写为CustomBinding。
因为MTOM是在绑定级别启用的,所以启用MTOM会影响给定终结点上的所有操作,通常情况下,只有在终结点交换超过1KB二进制数据的消息时,才应该对它启用MTOM。
编程模型
无论在应用程序中使用三个内置编码器中的哪一个,在传输二进制数据方面的编程体验都是相同的。区别在于WCF如何基于数据类型来处理数据。
当使用MTOM时,将根据以下规则序列化上面的数据协定:
1) 如果binaryBuffer不是null,并且有个别包含足够的数据,使得需要Base64编码所没有的MTOM外部化开销(MIME头等等),则这些数据将外部化并作为二进制MIME部分随消息一起传送。如果未超过阀值,则数据会编码为Base64.
2) 字符串(和其他所有非二进制的类型)无论多大,始终表示为消息正文内的字符
MTOM_DEMO
1) 新建一个WCF Application Service,然后添加“UploadService”WCF服务。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.IO;
namespace Video11.Demo1.Mtom
{
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IUploadService" in both code and config file together.
[ServiceContract(Namespace = "http://Video11.Demo1.Mtom")]
public interface IUploadService
{
[OperationContract]
int Upload(Stream data);
}
}
2) 实现该服务:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace Video11.Demo1.Mtom
{
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "UploadService" in code, svc and config file together.
public class UploadService : IUploadService
{
public int Upload(System.IO.Stream data)
{
int size = 0;
int bytesRead = 0;
byte[] buffer = new byte[1024];
//读取所有流中的数据
do
{
bytesRead = data.Read(buffer, 0, buffer.Length);
size += bytesRead;
}
while (bytesRead > 0);
//返回流的大小
return size;
}
}
}
3) 配置配置信息,注意这里指定了Endpoint的bindingConfiguration属性为Mtom类型的编码。
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IUpload" messageEncoding="Mtom"/>
</wsHttpBinding>
</bindings>
<services>
<service name="Video11.Demo1.Mtom.UploadService" behaviorConfiguration="UploadServiceBehavior">
<endpoint address="" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IUpload" contract="Video11.Demo1.Mtom.IUploadService"/>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="UploadServiceBehavior">
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
4) 部署该服务到IIS上,然后创建一个Client客户端程序。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.ServiceModel.Channels;
namespace Client
{
class Program
{
static void Main(string[] args)
{
byte[] binaryData = new byte[1000];
MemoryStream stream = new MemoryStream(binaryData);
//上传1000字节的stream内存流
ServiceReference1.UploadServiceClient client = new ServiceReference1.UploadServiceClient();
Console.WriteLine(client.Upload(stream)); //返回的也是1000个字节
Console.WriteLine();
stream.Close();
CompareMessageSize(100);
CompareMessageSize(1000);
CompareMessageSize(10000);
CompareMessageSize(100000);
CompareMessageSize(1000000);
Console.ReadKey();
}
private static void CompareMessageSize(int dataSize)
{
// Create and buffer a message with a binary payload
byte[] binaryData = new byte[dataSize];
Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "action", binaryData);
MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);
// Print the size of a text encoded copy
int size = SizeOfTextMessage(buffer.CreateMessage());
Console.WriteLine("编码之前的消息大小{0}byte,使用Text Encoding编码后大小是{1}", binaryData.Length, size);
//Print the size of an MTOM encoded copy
size = SizeOfMtomMessage(buffer.CreateMessage());
Console.WriteLine("编码之前的消息大小{0}byte,使用MTOM Encoding编码后大小是{1}", binaryData.Length, size);
Console.WriteLine();
message.Close();
}
private static int SizeOfMtomMessage(Message message)
{
//创建MTOM编码对象
MessageEncodingBindingElement element = new MtomMessageEncodingBindingElement();
MessageEncoderFactory factory = element.CreateMessageEncoderFactory();
MessageEncoder encoder = factory.Encoder;
//写入message并返回其长度
MemoryStream stream = new MemoryStream();
encoder.WriteMessage(message, stream);
int size = (int)stream.Length;
stream.Close();
message.Close();
return size;
}
private static int SizeOfTextMessage(Message message)
{
//创建一个文本编码
MessageEncodingBindingElement element = new TextMessageEncodingBindingElement();
MessageEncoderFactory factory = element.CreateMessageEncoderFactory();
MessageEncoder encoder = factory.Encoder;
//写入message并返回它的长度
MemoryStream stream = new MemoryStream();
encoder.WriteMessage(message, stream);
int size = (int)stream.Length;
message.Close();
stream.Close();
return size;
}
}
}
5) 运行结果:由此可见MTOM消息编码对比较大的消息的编码上才有优势。
源代码: http://download.youkuaiyun.com/detail/eric_k1m/6437523
数据的流模式
启用流模式
流式传输的编程模型
DEMO
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.IO;
namespace Video11.Demo2.Stream
{
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IStreamingService" in both code and config file together.
[ServiceContract(Namespace = "http://Video11.Demo2.Stream")]
public interface IStreamingService
{
[OperationContract]
System.IO.Stream GetStream(string data);
[OperationContract]
bool UploadStream(System.IO.Stream stream);
[OperationContract]
System.IO.Stream EchoStream(System.IO.Stream stream);
[OperationContract]
System.IO.Stream GetReverseStream();
}
}
2) 实现该服务接口:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.IO;
namespace Video11.Demo2.Stream
{
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "StreamingService" in both code and config file together.
public class StreamingService : IStreamingService
{
public System.IO.Stream GetStream(string data)
{
//图像文件放在服务端的bin文件夹里
//Path.Combine(System.Environment.CurrentDirectory, ".\\image.jpg")获得的地址是当前项目bin文件夹里的image.jpg文件
string filePath = Path.Combine(System.Environment.CurrentDirectory, ".\\image.jpg");
//把指定路径的文件打开并读取到FileStream流文件里,然后返回这个文件流对象
//如果该文件夹里没有这个文件,则捕获异常
try
{
FileStream imageFile = File.OpenRead(filePath);
return imageFile;
}
catch (IOException ex)
{
Console.WriteLine(
String.Format("An exception was thrown while trying to open file {0}", filePath));
Console.WriteLine("Exception is: ");
Console.WriteLine(ex.ToString());
throw ex;
}
}
//从客户端上传一个流文件,然后服务端接收后返回一个bool值
public bool UploadStream(System.IO.Stream stream)
{
//Path.Combine()将多个字符串组合成一个路径。
//Environment.CurrentDirectory 属性:获取或设置当前工作目录的完全限定路径。也就bin\debug文件夹
string filePath = Path.Combine(System.Environment.CurrentDirectory, "uploadedfile.jpg");
try
{
Console.WriteLine("保存到文件 {0}", filePath);
//File.Open 方法 (String要打开的文件路径, FileMode指定文件不存在时候是否创建该文件, FileAccess可以对文件执行的操作):
//在由受信任的应用程序调用时,用指定模式和访问权限打开指定路径上的 FileStream。
FileStream outstream = File.Open(filePath, FileMode.Create, FileAccess.Write);
//限定流大小为4K
const int bufferLen = 4096;
byte[] buffer = new byte[bufferLen];
int count = 0;
//Stream.Read(buffer字节数组此方法返回时该缓冲区包含指定的字符数组该数组的offset和offset+count-1之间的值由当前源中读取的字节替换,
//offset buffer中的从零开始的字节偏移量,从此处开始存储当前流中读取的数据。
//count 要从当前流中最多读取的字节流
//)
//所以这的意思是:读取stream流的数据,从位置0开始一直读取到bufferLen位置也就是4K大小,
//然后再把读取到的流存储到了buffer中。返回值count为读入缓冲区buffer中的总字节数。
while ((count = stream.Read(buffer, 0, bufferLen)) > 0)
{
Console.WriteLine(".");
//把从0到count大小字节流的buffer字节数组复制到outstream流中去。
outstream.Write(buffer, 0, count);
}
//关闭流
outstream.Close();
stream.Close();
Console.WriteLine();
Console.WriteLine("文件 {0} 已经保存", filePath);
return true;
}
catch (IOException ex)
{
Console.WriteLine(
String.Format("An exception was thrown while opening or writing to file {0}", filePath));
Console.WriteLine("Exception is: ");
Console.WriteLine(ex.ToString());
throw ex;
}
}
//客户端发给服务端一个流文件,然后再有服务端返回一个流文件
public System.IO.Stream EchoStream(System.IO.Stream stream)
{
string filePath = Path.Combine(System.Environment.CurrentDirectory, "echofile.jpg");
try
{
FileStream outstream = File.Open(filePath, FileMode.Create, FileAccess.Write);
//把传进来的stream复制一份给outstream
this.CopyStream(stream, outstream);
outstream.Close();
stream.Close();
FileStream echoFile = File.OpenRead(filePath);
return echoFile;
}
catch (IOException ex)
{
Console.WriteLine(
String.Format("An exception was thrown while opening or writing to file {0}", filePath));
Console.WriteLine("Exception is: ");
Console.WriteLine(ex.ToString());
throw ex;
}
}
private void CopyStream(System.IO.Stream instream, FileStream outstream)
{
//固定每次读取的字节流大小为4K
const int bufferLen = 4096;
byte[] buffer = new byte[bufferLen];
int count = 0;
while ((count = instream.Read(buffer, 0, bufferLen)) > 0)
{
outstream.Write(buffer, 0, count);
}
}
public System.IO.Stream GetReverseStream()
{
string filePath = Path.Combine(System.Environment.CurrentDirectory, "\\image.jpg");
ReverseStream stream = new ReverseStream(filePath);
return stream;
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="Video11.Demo2.Stream.StreamingService" behaviorConfiguration="StreamingServiceBehavior">
<host>
<baseAddresses>
<!--这里要打开8000端口才行,否则报错,请参照http://blog.youkuaiyun.com/eric_k1m/article/details/12949169-->
<add baseAddress="http://localhost:8000/Design_Time_Addresses/Video11.Demo2.Stream/StreamingService/"/>
</baseAddresses>
</host>
<endpoint address="ep1" binding="basicHttpBinding"
bindingConfiguration="HttpStreaming"
contract="Video11.Demo2.Stream.IStreamingService"/>
<endpoint address="http://localhost:9000/Design_Time_Addresses/Video11.Demo2.Stream/StreamingService/ep2"
binding="customBinding"
bindingConfiguration="Soap12"
contract="Video11.Demo2.Stream.IStreamingService"/>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<bindings>
<!--使用basicHttpBinding使用流模式-->
<basicHttpBinding>
<binding name="HttpStreaming" maxReceivedMessageSize="67108864" transferMode="Streamed"/>
</basicHttpBinding>
<!--使用customBinding使用HTTP和流模式-->
<customBinding>
<binding name="Soap12">
<textMessageEncoding messageVersion="Soap12WSAddressing10"/>
<httpTransport transferMode="Streamed" maxReceivedMessageSize="67108864"/>
</binding>
</customBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="StreamingServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace Client
{
class Program
{
static void Main(string[] args)
{
//获得要存储到客户端的文件地址也是在bin文件加下的clientfile.jpg文件里
string filePath = Path.Combine(System.Environment.CurrentDirectory, "clientfile.jpg");
//创建客户端代理类,这里的“BasicHttpBinding_IStreamingService”是endpointConfigurationName也就是终结点配置名,可以在客户端的app.config里查看endpoint信息获得,这是由于服务端有多个endpoint导致的,这里需要指定endpoint名字来确定生成哪种客户端代理类实例。这里就是使用basicHttpBinding绑定的服务端endpoint。
StreamingServiceClient client1 = new StreamingServiceClient("BasicHttpBinding_IStreamingService");
Console.WriteLine("———使用HTTP的绑定———");
Console.WriteLine("Calling GetStream()");
Stream stream1 = client1.GetStream("some dummy data");
SaveStreamToFile(stream1, filePath);
Console.WriteLine("调用UploadStream()");
FileStream instream1 = File.OpenRead(filePath);
bool result1 = client1.UploadStream(instream1);
Console.WriteLine("调用EchoStream()");
FileStream filestream1 = File.OpenRead(filePath);
Stream stream2 = client1.EchoStream(filestream1);
string filePathEcho = Path.Combine(System.Environment.CurrentDirectory, "echobackfile.jpg");
SaveStreamToFile(stream2, filePathEcho);
Console.WriteLine("调用GetReveredStream()");
string filePathReverse = Path.Combine(System.Environment.CurrentDirectory, "Reversefile.jpg");
stream1 = client1.GetReverseStream();
SaveStreamToFile(stream1, filePathReverse);
instream1.Close();
Console.ReadKey();
//重复使用TCP
//repeating using TCP
StreamingServiceClient client2 = new StreamingServiceClient("CustomBinding_IStreamingService");
Console.WriteLine("------ Using Custom HTTP ------ ");
Console.WriteLine("Calling GetStream()");
Stream stream2 = client2.GetStream("some dummy data");
SaveStreamToFile(stream2, filePath);
Console.WriteLine("Calling UploadStream()");
FileStream instream2 = File.OpenRead(filePath);
bool result2 = client2.UploadStream(instream2);
instream2.Close();
Console.WriteLine("Calling GetReversedStream()");
stream2 = client2.GetReverseStream();
SaveStreamToFile(stream2, filePath);
client2.Close();
Console.WriteLine();
Console.WriteLine("Press <ENTER> to terminate client.");
Console.ReadLine();
}
private static void SaveStreamToFile(Stream stream, string filePath)
{
Console.WriteLine("Saving to file {0}", filePath);
//创建一个文件流实例,指定该文件流保存到filepath里。
FileStream outstream = File.Open(filePath, FileMode.Create, FileAccess.Write);
//把从服务器端获得的文件流拷贝到上面所创建好的文件流outstream里。
CopyStream(stream, outstream);
outstream.Close();
Console.WriteLine();
Console.WriteLine("File {0} saved", filePath);
}
private static void CopyStream(Stream instream, FileStream outstream)
{
//read from the input stream in 4K chunks
//and save to output stream
const int bufferLen = 4096;
byte[] buffer = new byte[bufferLen];
int count = 0;
int bytecount = 0;
//限制只能读最大的文件流大小为4K
//把从服务器端读取来的文件流instream写入到客户端的outstream里。
while ((count = instream.Read(buffer, 0, bufferLen)) > 0)
{
outstream.Write(buffer, 0, count);
Console.Write(".");
bytecount += count;
}
Console.WriteLine();
Console.WriteLine("Wrote {0} bytes to stream", bytecount);
}
}
}
源代码: http://download.youkuaiyun.com/detail/eric_k1m/6442635