在IIS上添加gzip压缩已经不是什么新鲜事情了,但是如何在自host的wcf上对rest响应支持gzip压缩哪?
乍一看这个命题还真的有点难,但是wcf框架本身相当强大,拥有众多的介入点,只要正确的介入binding和behavior就可以很简单的达到目的
准备Binding
首先,因为需要修改输出结果的编码,那么不可避免的需要修改Binding,如果熟悉WCF的Binding模型的话,可以很容易的将传统的wsHttpBinding,webHttpBinding,netTcpBinding等分解,由于目标是rest服务,因此传输层使用http方式,即:HttpTransportBindingElement,而编码层则需要在原来的编码层基础上添加gzip压缩,因此需要嵌套原来的WebMessageEncodingBindingElement,并在原来的基础上添加gzip效果。
然后就是Binding的拼装了,好吧,我不想把事件搞得太负责,直接用CustomBinding来组装:

using System.IO;
using System.IO.Compression;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Web;
using System.Xml;
namespace Zhenway.RestWithGZip
{
public class ZhenwayWebHttpBinding
: CustomBinding, IBindingRuntimePreferences
{
#region Ctors
public ZhenwayWebHttpBinding()
: base(GetBindingElements()) { }
public ZhenwayWebHttpBinding( string configurationName)
: base(GetBindingElements())
{
ApplyConfiguration(configurationName);
}
#endregion
#region Methods
private static BindingElementCollection GetBindingElements()
{
WebHttpBinding webHttpBinding;
webHttpBinding = new WebHttpBinding();
var collection = webHttpBinding.CreateBindingElements();
var encodingBindingElement = collection.Find<MessageEncodingBindingElement>();
var index = collection.IndexOf(encodingBindingElement);
collection.RemoveAt(index);
var wrapperBindingElement = new WrapperEncodingBindingElement(encodingBindingElement);
collection.Insert(index, wrapperBindingElement);
return collection;
}
private void ApplyConfiguration( string configurationName)
{
ZhenwayWebHttpBindingCollectionElement c = ZhenwayWebHttpBindingCollectionElement.GetBindingCollectionElement();
ZhenwayWebHttpBindingElement element = c.Bindings[configurationName];
if (element == null)
throw new InvalidOperationException( " Configuration[ " + configurationName + " ] not found. ");
element.ApplyConfiguration( this);
}
#endregion
#region Properties
private HttpTransportBindingElement httpTransportBindingElement { get { return Elements.Find<HttpTransportBindingElement>(); } }
public bool AllowCookies
{
get { return httpTransportBindingElement.AllowCookies; }
set { httpTransportBindingElement.AllowCookies = value; }
}
public long MaxBufferPoolSize
{
get { return httpTransportBindingElement.MaxBufferPoolSize; }
set { httpTransportBindingElement.MaxBufferPoolSize = value; }
}
public int MaxBufferSize
{
get { return httpTransportBindingElement.MaxBufferSize; }
set { httpTransportBindingElement.MaxBufferSize = value; }
}
public long MaxReceivedMessageSize
{
get { return httpTransportBindingElement.MaxReceivedMessageSize; }
set { httpTransportBindingElement.MaxReceivedMessageSize = value; }
}
public XmlDictionaryReaderQuotas ReaderQuotas
{
get { return ((WrapperEncodingBindingElement)Elements.Find<MessageEncodingBindingElement>()).ReaderQuotas; }
set
{
if (value == null)
throw new ArgumentNullException( " value ");
value.CopyTo(((WrapperEncodingBindingElement)Elements.Find<MessageEncodingBindingElement>()).ReaderQuotas);
}
}
public override string Scheme { get { return " http "; } }
bool IBindingRuntimePreferences.ReceiveSynchronously
{
get { return false; }
}
public TransferMode TransferMode
{
get { return httpTransportBindingElement.TransferMode; }
set { httpTransportBindingElement.TransferMode = value; }
}
#endregion
#region Wrapper Classes
private sealed class WrapperEncodingBindingElement
: MessageEncodingBindingElement
{
private readonly WebMessageEncodingBindingElement m_bindingElement;
public WrapperEncodingBindingElement(MessageEncodingBindingElement bindingElement)
{
this.m_bindingElement = (WebMessageEncodingBindingElement)bindingElement;
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new WrapperMessageEncoderFactory(m_bindingElement.CreateMessageEncoderFactory());
}
public override MessageVersion MessageVersion
{
get { return this.m_bindingElement.MessageVersion; }
set { this.m_bindingElement.MessageVersion = value; }
}
public override BindingElement Clone()
{
MessageEncodingBindingElement bindingElement = (MessageEncodingBindingElement) this.m_bindingElement.Clone();
return new WrapperEncodingBindingElement(bindingElement);
}
public XmlDictionaryReaderQuotas ReaderQuotas
{
get { return m_bindingElement.ReaderQuotas; }
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add( this);
return context.BuildInnerChannelFactory<TChannel>();
}
public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
{
context.BindingParameters.Add( this);
return context.BuildInnerChannelListener<TChannel>();
}
public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add( this);
return context.CanBuildInnerChannelFactory<TChannel>();
}
public override bool CanBuildChannelListener<TChannel>(BindingContext context)
{
context.BindingParameters.Add( this);
return context.CanBuildInnerChannelListener<TChannel>();
}
public override T GetProperty<T>(BindingContext context)
{
if (context == null)
{
throw new ArgumentNullException( " context ");
}
if ( typeof(T) == typeof(XmlDictionaryReaderQuotas))
{
return (T)( object) this.ReaderQuotas;
}
return base.GetProperty<T>(context);
}
}
private sealed class WrapperMessageEncoderFactory
: MessageEncoderFactory
{
private readonly MessageEncoderFactory m_inner;
private readonly MessageEncoder m_encoder;
public WrapperMessageEncoderFactory(MessageEncoderFactory factory)
{
this.m_inner = factory;
this.m_encoder = new WrapperMessageEncoder(factory.Encoder);
}
public override MessageEncoder Encoder
{
get { return this.m_encoder; }
}
public override MessageVersion MessageVersion
{
get { return this.m_encoder.MessageVersion; }
}
}
private sealed class WrapperMessageEncoder
: MessageEncoder
{
private readonly MessageEncoder m_inner;
public WrapperMessageEncoder(MessageEncoder encoder)
{
this.m_inner = encoder;
}
#region Overrides
public override string ContentType
{
get { return this.m_inner.ContentType; }
}
public override string MediaType
{
get { return this.m_inner.MediaType; }
}
public override MessageVersion MessageVersion
{
get { return this.m_inner.MessageVersion; }
}
public override bool IsContentTypeSupported( string contentType)
{
return this.m_inner.IsContentTypeSupported(contentType);
}
public override T GetProperty<T>()
{
return this.m_inner.GetProperty<T>();
}
public override Message ReadMessage(ArraySegment< byte> buffer, BufferManager bufferManager, string contentType)
{
return this.m_inner.ReadMessage(buffer, bufferManager, contentType);
}
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
{
return this.m_inner.ReadMessage(stream, maxSizeOfHeaders, contentType);
}
public override ArraySegment< byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
var c = WebOperationContext.Current;
// 写Buffered消息
ArraySegment< byte> buffer = this.m_inner.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
if (c != null && message.Properties.ContainsKey( " gzip "))
return CompressBuffer(buffer, bufferManager, messageOffset);
return buffer;
}
public override void WriteMessage(Message message, Stream stream)
{
// 写Streaming消息
var c = WebOperationContext.Current;
if (c != null && message.Properties.ContainsKey( " gzip "))
{
using ( var gz = new GZipStream(stream, CompressionMode.Compress, true))
this.m_inner.WriteMessage(message, gz);
stream.Flush();
return;
}
this.m_inner.WriteMessage(message, stream);
}
#endregion
private static ArraySegment< byte> CompressBuffer(ArraySegment< byte> buffer, BufferManager bufferManager, int messageOffset)
{
// 压缩
var ms = new MemoryStream(Math.Max(buffer.Count >> 1, 512));
using ( var gz = new GZipStream(ms, CompressionMode.Compress, true))
gz.Write(buffer.Array, messageOffset, buffer.Count);
// 重新组织消息体
byte[] bufferedBytes = bufferManager.TakeBuffer(messageOffset + ( int)ms.Length);
byte[] compressedBytes = ms.GetBuffer();
Array.Copy(buffer.Array, 0, bufferedBytes, 0, messageOffset);
Array.Copy(compressedBytes, 0, bufferedBytes, messageOffset, ( int)ms.Length);
bufferManager.ReturnBuffer(buffer.Array);
return new ArraySegment< byte>(bufferedBytes, messageOffset, ( int)ms.Length);
}
}
#endregion
}
}
binding就准备好啦,不过这个binding只能用代码形式创建,而不能用配置形式创建,显然与wcf强大的配置功能有点不合拍,再加下binding的配置节支持:

using System.Configuration;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.Xml;
namespace Zhenway.RestWithGZip
{
public class ZhenwayWebHttpBindingElement
: StandardBindingElement
{
#region Fields
private ConfigurationPropertyCollection m_properties;
#endregion
#region Ctors
public ZhenwayWebHttpBindingElement( string name)
: base(name) { }
public ZhenwayWebHttpBindingElement()
: this( null) { }
#endregion
#region ConfigurationProperties
[ConfigurationProperty( " allowCookies ", DefaultValue = false)]
public bool AllowCookies
{
get { return ( bool) base[ " allowCookies "]; }
set { base[ " allowCookies "] = value; }
}
[LongValidator(MinValue = 0L), ConfigurationProperty( " maxBufferPoolSize ", DefaultValue = 524288L)]
public long MaxBufferPoolSize
{
get { return ( long) base[ " maxBufferPoolSize "]; }
set { base[ " maxBufferPoolSize "] = value; }
}
[ConfigurationProperty( " maxBufferSize ", DefaultValue = 65536), IntegerValidator(MinValue = 1)]
public int MaxBufferSize
{
get { return ( int) base[ " maxBufferSize "]; }
set { base[ " maxBufferSize "] = value; }
}
[ConfigurationProperty( " maxReceivedMessageSize ", DefaultValue = 65536L), LongValidator(MinValue = 1L)]
public long MaxReceivedMessageSize
{
get { return ( long) base[ " maxReceivedMessageSize "]; }
set { base[ " maxReceivedMessageSize "] = value; }
}
[ConfigurationProperty( " readerQuotas ")]
public XmlDictionaryReaderQuotasElement ReaderQuotas
{
get { return (XmlDictionaryReaderQuotasElement) base[ " readerQuotas "]; }
}
[ConfigurationProperty( " transferMode ", DefaultValue = TransferMode.Buffered)]
public TransferMode TransferMode
{
get { return (TransferMode) base[ " transferMode "]; }
set { base[ " transferMode "] = value; }
}
#endregion
#region Methods
protected override Type BindingElementType
{
get { return typeof(ZhenwayWebHttpBinding); }
}
protected override ConfigurationPropertyCollection Properties
{
get
{
if ( this.m_properties == null)
{
ConfigurationPropertyCollection c = base.Properties;
c.Add( new ConfigurationProperty( " allowCookies ", typeof( bool), false, null, null, ConfigurationPropertyOptions.None));
c.Add( new ConfigurationProperty( " maxBufferSize ", typeof( int), 65536, null, new IntegerValidator( 1, 2147483647, false), ConfigurationPropertyOptions.None));
c.Add( new ConfigurationProperty( " maxBufferPoolSize ", typeof( long), 524288L, null, new LongValidator( 0L, 9223372036854775807L, false), ConfigurationPropertyOptions.None));
c.Add( new ConfigurationProperty( " maxReceivedMessageSize ", typeof( long), 65536L, null, new LongValidator( 1L, 9223372036854775807L, false), ConfigurationPropertyOptions.None));
c.Add( new ConfigurationProperty( " readerQuotas ", typeof(XmlDictionaryReaderQuotasElement), null, null, null, ConfigurationPropertyOptions.None));
c.Add( new ConfigurationProperty( " transferMode ", typeof(TransferMode), TransferMode.Buffered, null, null, ConfigurationPropertyOptions.None));
this.m_properties = c;
}
return this.m_properties;
}
}
protected override void InitializeFrom(Binding binding)
{
base.InitializeFrom(binding);
ZhenwayWebHttpBinding zBinding = (ZhenwayWebHttpBinding)binding;
this.MaxBufferSize = zBinding.MaxBufferSize;
this.MaxBufferPoolSize = zBinding.MaxBufferPoolSize;
this.MaxReceivedMessageSize = zBinding.MaxReceivedMessageSize;
this.TransferMode = zBinding.TransferMode;
this.AllowCookies = zBinding.AllowCookies;
this.InitializeReaderQuotas(zBinding.ReaderQuotas);
}
internal void InitializeReaderQuotas(XmlDictionaryReaderQuotas readerQuotas)
{
if (readerQuotas == null)
{
throw new ArgumentNullException( " readerQuotas ");
}
this.ReaderQuotas.MaxDepth = readerQuotas.MaxDepth;
this.ReaderQuotas.MaxStringContentLength = readerQuotas.MaxStringContentLength;
this.ReaderQuotas.MaxArrayLength = readerQuotas.MaxArrayLength;
this.ReaderQuotas.MaxBytesPerRead = readerQuotas.MaxBytesPerRead;
this.ReaderQuotas.MaxNameTableCharCount = readerQuotas.MaxNameTableCharCount;
}
protected override void OnApplyConfiguration(Binding binding)
{
var zBinding = (ZhenwayWebHttpBinding)binding;
zBinding.MaxBufferPoolSize = this.MaxBufferPoolSize;
zBinding.MaxReceivedMessageSize = this.MaxReceivedMessageSize;
zBinding.TransferMode = this.TransferMode;
zBinding.AllowCookies = this.AllowCookies;
PropertyInformationCollection propertyInformationCollection = base.ElementInformation.Properties;
if (propertyInformationCollection[ " maxBufferSize "].ValueOrigin != PropertyValueOrigin.Default)
{
zBinding.MaxBufferSize = this.MaxBufferSize;
}
this.ApplyReaderQuotasConfiguration(zBinding.ReaderQuotas);
}
private void ApplyReaderQuotasConfiguration(XmlDictionaryReaderQuotas readerQuotas)
{
if (readerQuotas == null)
throw new ArgumentNullException( " readerQuotas ");
if ( this.ReaderQuotas.MaxDepth != 0)
{
readerQuotas.MaxDepth = this.ReaderQuotas.MaxDepth;
}
if ( this.ReaderQuotas.MaxStringContentLength != 0)
{
readerQuotas.MaxStringContentLength = this.ReaderQuotas.MaxStringContentLength;
}
if ( this.ReaderQuotas.MaxArrayLength != 0)
{
readerQuotas.MaxArrayLength = this.ReaderQuotas.MaxArrayLength;
}
if ( this.ReaderQuotas.MaxBytesPerRead != 0)
{
readerQuotas.MaxBytesPerRead = this.ReaderQuotas.MaxBytesPerRead;
}
if ( this.ReaderQuotas.MaxNameTableCharCount != 0)
{
readerQuotas.MaxNameTableCharCount = this.ReaderQuotas.MaxNameTableCharCount;
}
}
#endregion
}
}
还有配置节的入口:

using System.Configuration;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
namespace Zhenway.RestWithGZip
{
public class ZhenwayWebHttpBindingCollectionElement
: StandardBindingCollectionElement<ZhenwayWebHttpBinding, ZhenwayWebHttpBindingElement>
{
protected override Binding GetDefault()
{
return new ZhenwayWebHttpBinding();
}
internal static ZhenwayWebHttpBindingCollectionElement GetBindingCollectionElement()
{
BindingsSection bindingsSection = null;
string text = " system.serviceModel/bindings ";
bindingsSection = (BindingsSection)ConfigurationManager.GetSection(text);
BindingCollectionElement bindingCollectionElement = bindingsSection[ " zhenwayWebHttpBinding "];
return (ZhenwayWebHttpBindingCollectionElement)bindingCollectionElement;
}
}
}
这样binding相关的内容就全部准备好了。
准备Behavior
binding虽然准备好了,不过,哪些接口的结果需要进行gzip压缩哪?毕竟gzip对于部分内容的压缩效果好,对于某些内容着完全没有效果,甚至还会变大。
最好能有个标记,标了就用gzip,没标记就不压,这样就能基本顾及常规的使用

using System.IO;
using System.Net;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Web;
namespace Zhenway.RestWithGZip
{
[AttributeUsage(AttributeTargets.Method)]
public class GZipAttribute
: Attribute, IOperationBehavior
{
#region IOperationBehavior 成员
public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
{
dispatchOperation.Formatter = new GZipFormatter(dispatchOperation.Formatter);
}
void IOperationBehavior.AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { }
void IOperationBehavior.ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { }
void IOperationBehavior.Validate(OperationDescription operationDescription) { }
#endregion
#region Subclass: OperationInvoker
private sealed class GZipFormatter
: IDispatchMessageFormatter
{
private readonly IDispatchMessageFormatter m_inner;
public GZipFormatter(IDispatchMessageFormatter inner)
{
m_inner = inner;
}
#region IDispatchMessageFormatter 成员
public void DeserializeRequest(Message message, object[] parameters)
{
m_inner.DeserializeRequest(message, parameters);
}
public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
{
var msg = m_inner.SerializeReply(messageVersion, parameters, result);
var c = WebOperationContext.Current;
if (c != null)
{
var ae = c.IncomingRequest.Headers[HttpRequestHeader.AcceptEncoding];
if (ae != null || ae.IndexOf( " gzip ", StringComparison.OrdinalIgnoreCase) >= 0 &&
" gzip ".Equals(c.OutgoingResponse.Headers[HttpResponseHeader.ContentEncoding], StringComparison.OrdinalIgnoreCase) &&
result != null)
{
msg.Properties[ " gzip "] = " 1 ";
c.OutgoingResponse.Headers[HttpResponseHeader.ContentEncoding] = " gzip ";
}
}
return msg;
}
#endregion
}
#endregion
}
}
这样就能很方便的使用啦。
来个示例
契约:

using System.IO;
using System.ServiceModel;
using System.ServiceModel.Web;
namespace Zhenway.RestWithGZip
{
[ServiceContract]
public interface IService1
{
[GZip]
[ContentType( " text/plain ")]
[CacheControl(CacheControl.NoCache)]
[WebGet(UriTemplate = " buffered/{value}/ ")]
[OperationContract]
string TestBufferedMessage( string value);
[GZip]
[ContentType( " text/html ")]
[CacheControl( 10)]
[WebGet(UriTemplate = " streaming/{value}/ ")]
[OperationContract]
Stream TestStreamingMessage( string value);
}
}
实现:

using System.IO;
namespace Zhenway.RestWithGZip
{
public class Service1 : IService1
{
public string TestBufferedMessage( string value)
{
return string.Format( " You entered: {0} ", value);
}
public Stream TestStreamingMessage( string value)
{
var s = string.Format( " <p>You entered: {0}<p> ", value);
var ms = new MemoryStream();
var sw = new StreamWriter(ms);
sw.Write( " <html><body> ");
for ( int i = 0; i < 1000; i++)
{
sw.WriteLine(s);
}
sw.Write( " </body></html> ");
sw.Flush();
ms.Seek( 0, SeekOrigin.Begin);
return ms;
}
}
}
配置:

< configuration >
< system.serviceModel >
< extensions >
< bindingExtensions >
< add name ="zhenwayWebHttpBinding" type ="Zhenway.RestWithGZip.ZhenwayWebHttpBindingCollectionElement, Zhenway.RestWithGZip" />
</ bindingExtensions >
</ extensions >
< bindings >
< zhenwayWebHttpBinding >
< binding name ="AllowCookies" allowCookies ="true" />
</ zhenwayWebHttpBinding >
</ bindings >
< services >
< service name ="Zhenway.RestWithGZip.Service1" >
< host >
< baseAddresses >
< add baseAddress ="http://localhost:8888/" />
</ baseAddresses >
</ host >
< endpoint address ="" binding ="zhenwayWebHttpBinding" bindingConfiguration ="AllowCookies" contract ="Zhenway.RestWithGZip.IService1" behaviorConfiguration ="RestBehavior" >
< identity >
< dns value ="localhost" />
</ identity >
</ endpoint >
</ service >
</ services >
< behaviors >
< serviceBehaviors >
< behavior >
< serviceMetadata httpGetEnabled ="True" />
< serviceDebug includeExceptionDetailInFaults ="False" />
</ behavior >
</ serviceBehaviors >
< endpointBehaviors >
< behavior name ="RestBehavior" >
< webHttp />
</ behavior >
</ endpointBehaviors >
</ behaviors >
</ system.serviceModel >
< startup >
< supportedRuntime version ="v4.0" sku =".NETFramework,Version=v4.0" />
</ startup >
</ configuration >
实际效果(Buffered方式):
源代码下载: