wcf rest服务启用gzip压缩

    在IIS上添加gzip压缩已经不是什么新鲜事情了,但是如何在自host的wcf上对rest响应支持gzip压缩哪?

    乍一看这个命题还真的有点难,但是wcf框架本身相当强大,拥有众多的介入点,只要正确的介入binding和behavior就可以很简单的达到目的

准备Binding

     首先,因为需要修改输出结果的编码,那么不可避免的需要修改Binding,如果熟悉WCF的Binding模型的话,可以很容易的将传统的wsHttpBinding,webHttpBinding,netTcpBinding等分解,由于目标是rest服务,因此传输层使用http方式,即:HttpTransportBindingElement,而编码层则需要在原来的编码层基础上添加gzip压缩,因此需要嵌套原来的WebMessageEncodingBindingElement,并在原来的基础上添加gzip效果。

     然后就是Binding的拼装了,好吧,我不想把事件搞得太负责,直接用CustomBinding来组装:

ExpandedBlockStart.gif ZhenwayWebHttpBinding
using System;
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 >>  1512));
                 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的配置节支持:

ExpandedBlockStart.gif ZhenwayWebHttpBindingElement
using System;
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),  falsenullnull, ConfigurationPropertyOptions.None));
                    c.Add( new ConfigurationProperty( " maxBufferSize "typeof( int),  65536nullnew IntegerValidator( 12147483647false), ConfigurationPropertyOptions.None));
                    c.Add( new ConfigurationProperty( " maxBufferPoolSize "typeof( long),  524288Lnullnew LongValidator( 0L9223372036854775807Lfalse), ConfigurationPropertyOptions.None));
                    c.Add( new ConfigurationProperty( " maxReceivedMessageSize "typeof( long),  65536Lnullnew LongValidator( 1L9223372036854775807Lfalse), ConfigurationPropertyOptions.None));
                    c.Add( new ConfigurationProperty( " readerQuotas "typeof(XmlDictionaryReaderQuotasElement),  nullnullnull, ConfigurationPropertyOptions.None));
                    c.Add( new ConfigurationProperty( " transferMode "typeof(TransferMode), TransferMode.Buffered,  nullnull, 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

    }
}

 

    还有配置节的入口:

ExpandedBlockStart.gif ZhenwayWebHttpBindingCollectionElement
using System;
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,没标记就不压,这样就能基本顾及常规的使用


ExpandedBlockStart.gif GZipAttribute
using System;
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

    }
}

 

    这样就能很方便的使用啦。

来个示例

    契约:

ExpandedBlockStart.gif 契约
using System;
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);
    }
}

 

    实现:

ExpandedBlockStart.gif 实现
using System;
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;
        }
    }
}

 

    配置:

ExpandedBlockStart.gif 配置
<? xml version="1.0" ?>
< 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方式):

    实际效果(Streaming方式): 


 

源代码下载:

/Files/vwxyzh/201202/RestWithGZip.zip 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值