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






4.74/5 (25投票s)
介绍如何在实现 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 扩展,您必须实现一些抽象方法,例如 ChainStream
、GetIntializer
、Initialize
和 ProcessMessage
。ChainStream
的写法会比 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
类型参数传递。实际上,传递的是 ClientSoapMessage
或 ServerSoapMessage
的实例,我们可以轻松检查参数类型。在这里,我们可以区分客户端消息和服务器消息。正如我们之前决定的,在这个例子中,我们只关心客户端消息。
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 调用,它会被调用四次,分别在特定阶段。阶段可以从 SoapMessage
的 Stage
属性读取,它可能有以下四种值之一:
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>
希望这篇文章能帮助到大家。欢迎提出任何评论和问题。