65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 中使用 SOAP 扩展进行高效跟踪

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (25投票s)

2009年3月22日

CPOL

6分钟阅读

viewsIcon

166102

downloadIcon

4054

介绍如何在实现 SOAP 扩展时避免性能损失。

引言

前一段时间,我不得不为我们的 Web 服务添加对传出 SOAP 消息的跟踪功能。跟踪仅应用于某些 Web 调用,具体取决于配置。当我读到 SOAP 扩展并建议我的同事使用它们时,他坚决反对。经过深入研究,我明白了原因。如果按照 MSDN 和其他一些资源中的描述使用 SOAP 扩展,它们确实会影响应用程序的性能。

常见实现中的问题

  • 您需要将原始流复制到一个内存流中,以便多次读取它。
  • 这会同时影响传入和传出的消息。
  • 在链接流时,您没有任何关于当前调用的是哪个类的哪个方法的信息,也无法访问代理对象的属性。
  • 您无法在运行时选择要使用的扩展,它们是声明式选择的。

有几种初始化 SOAP 扩展的方法

  • 类构造函数 - 每次实例化 SOAP 扩展时都会调用类构造函数,通常用于初始化成员变量。
  • GetInitializer - GetInitializer 然而,它只在第一次向 XML Web Service 方法发出 SOAP 请求时调用一次。它有两个重载版本。如果自定义属性应用于 XML Web Service 方法,则会调用第一个 GetInitializer 方法。这允许 SOAP 扩展查询 XML Web Service 方法的 LogicalMethodInfo 以获取原型信息,或访问从 SoapExtensionAttribute 派生的类传递的扩展特定数据。不幸的是,这不适用于我的情况。第二个版本在 web.config 中添加 SOAP 扩展时被调用。它只有一个参数 - Web 服务类型。
  • Initialize - 每次向 XML Web Service 方法发出 SOAP 请求时都会调用 Initialize,但它比类构造函数有优势,因为 GetInitializer 中初始化的对象被传递给它。

这些功能对我都没有帮助,因为我需要根据运行时数据来控制执行过程。

SOAP 扩展最糟糕的地方在于,您唯一可以替换网络线程为内存线程的方法 - ChainStream - 除了线程之外没有任何参数,并且在更合理的 ProcessMessage 方法之前被调用。您甚至不知道它是服务器消息还是客户端消息。ProcessMessage 接收所有必要的信息来做出决定,但当它被调用时,已经太晚了,无法更改流。而且,一旦您在 ChainStream 方法中替换了流,您总是必须将其复制到真实流中,这会影响性能并需要更多内存。

如何高效实现 SOAP 扩展

经过几天的对此问题的调查,我设法说服了我的同事使用 SOAP 扩展,并在实现中进行了一些改进,其中包括使用特殊的、可切换的流。

特殊流

此流继承自抽象的 Stream 类,并将所有标准方法调用委托给两个内部流之一。第一个是原始的“旧”流,第二个是仅按需实例化的 MemoryStream

下面是该类的实现

#region TraceExtensionStream

/// <summary>
/// Special switchable stream
/// </summary>
internal class TraceExtensionStream : Stream
{
    #region Fields

    private Stream innerStream;
    private readonly Stream originalStream;

    #endregion

    #region .ctor

    /// <summary>
    /// Constructs an instance of the stream
    /// wrapping the original stream into it
    /// </summary>
    internal TraceExtensionStream(Stream originalStream)
    {
        innerStream = this.originalStream = originalStream;
    }

    #endregion

    #region New public members

    /// <summary>
    /// Creates a new memory stream and makes it active
    /// </summary>
    public void SwitchToNewStream()
    {
        innerStream = new MemoryStream();
    }

    /// <summary>
    /// Copies data from the old stream to the new in-memory stream
    /// </summary>
    public void CopyOldToNew()
    {
        //innerStream = new MemoryStream((int)originalStream.Length);
        Copy(originalStream, innerStream);
        innerStream.Position = 0;
    }

    /// <summary>
    /// Copies data from the new stream to the old stream
    /// </summary>
    public void CopyNewToOld()
    {
        Copy(innerStream, originalStream);
    }

    /// <summary>
    /// Returns true if the active inner stream is a new stream,
    /// i.e. SwitchToNewStream has been called
    /// 
    public bool IsNewStream
    {
        get
        {
            return (innerStream != originalStream);
        }
    }

    /// <summary>
    /// A link to the active inner stream
    /// </summary>
    public Stream InnerStream
    {
        get { return innerStream; }
    }

    #endregion

    #region Private members

    private static void Copy(Stream from, Stream to)
    {
        const int size = 4096;
        byte[] bytes = new byte[4096];
        int numBytes;
        while((numBytes = from.Read(bytes, 0, size)) > 0)
            to.Write(bytes, 0, numBytes);
    }

    #endregion

    #region Overridden members

    public override IAsyncResult BeginRead(byte[] buffer, int offset, 
           int count, AsyncCallback callback, object state)
    {
        return innerStream.BeginRead(buffer, offset, count, callback, state);
    }

    public override IAsyncResult BeginWrite(byte[] buffer, int offset, 
           int count, AsyncCallback callback, object state)
    {
        return innerStream.BeginWrite(buffer, offset, count, callback, state);
    }

    //other overriden abstract members of Stream go down here

    #endregion
}

#endregion

SOAP 扩展

要创建 SOAP 扩展,您必须实现一些抽象方法,例如 ChainStreamGetIntializerInitializeProcessMessageChainStream 的写法会比 MSDN 示例稍微简单一些。它只是将一个流包装在 TraceExtensionStream 中。

/// <summary>
/// Replaces soap stream with our smart stream
/// </summary>
public override Stream ChainStream(Stream stream)
{
    traceStream = new TraceExtensionStream(stream);
    return traceStream;
}

这里的 traceStream 是一个字段,我们将在将来使用它来存储对我们的流的引用。

接下来的方法我们不做任何处理,所以直接留空

public override object GetInitializer(LogicalMethodInfo methodInfo, 
                       SoapExtensionAttribute attribute)
{
    return null;
}

public override object GetInitializer(Type WebServiceType)
{
    return null;
}

public override void Initialize(object initializer)
{
}

信息通过 ProcessMessage 方法中的 SoapMessage 类型参数传递。实际上,传递的是 ClientSoapMessageServerSoapMessage 的实例,我们可以轻松检查参数类型。在这里,我们可以区分客户端消息和服务器消息。正如我们之前决定的,在这个例子中,我们只关心客户端消息。

ClientSoapMessage 类还有另一个有趣的属性 - Client。它指向从 SoapHttpClientProtocol 派生的客户端代理类。(反之,ServerSoapMessage 有一个名为 Server 的属性)。如果我们能够扩展它,我们就可以在运行时将任何信息传递给 Web 服务扩展!

让支持跟踪的客户端实现 ITraceable 接口,声明如下:

/// <summary>
/// Interface that a proxy class should implement to support tracing
/// </summary>
public interface ITraceable
{
    bool IsTraceRequestEnabled { get; set; }
    bool IsTraceResponseEnabled { get; set; }
    string ComponentName { get; set; }
}

它有以下成员:

  • IsTraceRequestEnabled - 如果 SOAP 请求的转储已开启,则返回 true
  • IsTraceResponseEnabled - 如果 SOAP 响应的转储已开启,则返回 true
  • ComponentName - 用于标记跟踪消息的组件名称。

现在,我们在扩展类中声明一个私有方法,该方法尝试从 ProcessMessage 的参数中获取 ITraceable 实例:

/// <summary>
/// Tries to get ITraceable instance
/// </summary>
private ITraceable GetTraceable(SoapMessage message)
{
    SoapClientMessage clientMessage = message as SoapClientMessage;
    if (clientMessage != null)
    {
        return clientMessage.Client as ITraceable;
    }

    return null;
}

现在,让我们来实现 ProcessMessage 本身。

对于单个 Web 调用,它会被调用四次,分别在特定阶段。阶段可以从 SoapMessageStage 属性读取,它可能有以下四种值之一:

  • BeforeSerialize - 在客户端请求(或服务器响应)序列化之前发生。在这里,我们可以根据需要准备我们的智能流以进行缓冲。
  • AfterSerialize - 在客户端请求(或服务器响应)序列化之后发生。现在,我们可以将缓冲区写入日志。
  • BeforeDeserialize - 在客户端响应(或服务器请求)反序列化之前发生。在这里,我们可以将响应复制到缓冲区并将其保存到日志中。之后,我们必须激活缓冲区并重置其位置。
  • AfterDeserialize - 在客户端响应(或服务器请求)反序列化之后发生。在此阶段我们不做任何操作。

这是实现

public override void ProcessMessage(SoapMessage message)
{
    ITraceable traceable = GetTraceable(message);
    //If proxy is not configured to be traced, return
    if (traceable == null) return;
    switch (message.Stage)
    {
        case SoapMessageStage.BeforeSerialize:
            //If tracing is enabled, switch to memory buffer
            if (traceable.IsTraceRequestEnabled)
            {
                traceStream.SwitchToNewStream();
            }
            break;
        case SoapMessageStage.AfterSerialize:
            //If message was written to memory buffer, 
            //write its content to log and copy to the SOAP stream
            if (traceStream.IsNewStream)
            {
                traceStream.Position = 0;
                WriteToLog(DumpType.Request, traceable);
                traceStream.Position = 0;
                traceStream.CopyNewToOld();
            }
            break;
        case SoapMessageStage.BeforeDeserialize:
            //If tracing is enabled, copy SOAP stream 
            //to the new stream and write its content to log
            if (traceable.IsTraceResponseEnabled)
            {
                traceStream.SwitchToNewStream();
                traceStream.CopyOldToNew();
                WriteToLog(DumpType.Response, traceable);
                traceStream.Position = 0;
            }
            break;
    }
}

就是这样。现在,您只需要让您的客户端协议支持 ITraceable 即可。

扩展 SoapHttpClientProtocol

如果您手动实现了客户端代理类(SoapHttpClientProtocol),添加一个额外的接口来支持并不困难。但是,如果它是自动生成的,您可能不想修改自动生成的文件。幸运的是,在该文件中,它被声明为 partial。这意味着代理类可以在另一个文件中进行扩展。

public partial class MyService : ITraceable
{
    private string componentName;
    private bool isTraceRequestEnabled;
    private bool isTraceResponseEnabled;

    public bool IsTraceRequestEnabled
    {
        get { return isTraceRequestEnabled; }
        set { isTraceRequestEnabled = value; }
    }

    public bool IsTraceResponseEnabled
    {
        get { return isTraceResponseEnabled; }
        set { isTraceResponseEnabled = value; }
    }

    public string ComponentName
    {
        get { return componentName; }
        set { componentName = value; }
    }
}

现在,您只需要设置这些属性的值,就可以在不永久影响性能的情况下控制您的 SOAP 扩展的使用。

如何使用代码

显然,您需要一个具有 Web 服务引用的应用程序。它可以是 Web 应用程序、Windows 应用程序或另一个 Web 服务。

首先,将附带的代码文件复制到您的项目中。您可能需要更改命名空间和跟踪方法。

然后,您应该按照上述说明扩展您的 Web 服务客户端代理类。如果存在定义日志记录是否发生的逻辑,它将在此处。

最后,请确保在 web.config(或 app.config)文件中包含对您的 SOAP 扩展的引用。您应该会看到类似这样的内容。

<configuration>
 <system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="Ideafixxxer.SoapDumper.TraceExtension" priority="1" group="0"/>
      </soapExtensionTypes>
    </webServices>
  </system.web>
</configuration>

 

希望这篇文章能帮助到大家。欢迎提出任何评论和问题。

© . All rights reserved.