【问题标题】:Logging SOAP request and response on server side在服务器端记录 SOAP 请求和响应
【发布时间】:2014-01-19 15:08:45
【问题描述】:

我正在尝试为对我的 ASP.NET Web 服务的所有 SOAP 方法调用创建一个日志记录服务。我一直在看 Log SOAP messages from a console application 和 MSDN 上的 SOAP 扩展演练 (http://msdn.microsoft.com/en-us/library/s25h0swd%28v=vs.100%29.aspx),但它们似乎并未完全涵盖它。

我不想更改 SOAP 消息,只需将其记录到数据库表中。 我要做的是读取 SOAP 消息流,将其解析为 XML,记录 XML,然后让调用顺利进行。但是当我阅读流时,它被花费/处置。我已尝试复制流内容以不中断流。

根据演练,ProcessMessage 方法应如下所示:

public override void ProcessMessage(SoapMessage message) 
{
   switch (message.Stage) 
   {
   case SoapMessageStage.BeforeSerialize:
       break;
   case SoapMessageStage.AfterSerialize:
       // Write the SOAP message out to a file.
       WriteOutput( message );
       break;
   case SoapMessageStage.BeforeDeserialize:
       // Write the SOAP message out to a file.
       WriteInput( message );
       break;
   case SoapMessageStage.AfterDeserialize:
       break;
   default:
       throw new Exception("invalid stage");
   }
}

我在BeforeDeserialize 阶段成功地解析了流,但随后在AfterSerialize 阶段再次调用ProcessMessage,到那时流已被使用并且不再包含任何数据。

根据使用 SOAP 扩展的 SOAP 消息修改 (http://msdn.microsoft.com/en-us/library/esw638yk%28v=vs.100%29.aspx),SOAP 调用经过以下步骤:

服务器端收到请求消息并准备响应

  1. Web 服务器上的 ASP.NET 接收 SOAP 消息。
  2. 在 Web 服务器上创建 SOAP 扩展的新实例。
  3. 在 Web 服务器上,如果这是第一次在服务器端使用此 Web 服务执行此 SOAP 扩展,则在服务器上运行的 SOAP 扩展上调用 GetInitializer 方法。
  4. Initialize 方法被调用。
  5. ChainStream 方法被调用。
  6. 调用 ProcessMessage 方法时将 SoapMessageStage 设置为 BeforeDeserialize
  7. ASP.NET 反序列化 XML 中的参数。
  8. 调用 ProcessMessage 方法时将 SoapMessageStage 设置为 AfterDeserialize
  9. ASP.NET 创建实现 Web 服务的类的新实例并调用 Web 服务方法,传入反序列化的参数。此对象与 Web 服务器位于同一台计算机上。
  10. Web 服务方法执行其代码,最终设置返回值和所有输出参数。
  11. 调用 ProcessMessage 方法时将 SoapMessageStage 设置为 BeforeSerialize。
  12. Web 服务器上的 ASP.NET 将返回值和输出参数序列化为 XML。
  13. 调用 ProcessMessage 方法时将 SoapMessageStage 设置为 AfterSerialize
  14. ASP.NET 通过网络将 SOAP 响应消息发送回 Web 服务客户端。

步骤 6 已正确执行并记录了 SOAP XML。然后它不应该再做任何事情,直到服务器处理完调用,完成它需要的(步骤 10)并返回响应(步骤 13)。相反,它会立即在AfterSerialize 阶段再次调用ProcessMessage,但是这次流已经用完,并且在我尝试记录它时会引发异常。

根据演练,我应该使用ChainStream 方法,它应该在上面的第 5 步中运行。当我拨打电话时,它会运行两次,一次在BeforeDeserialize 之前,一次在AfterSerialize 之前。

我尝试将消息流复制到单独的流中并将其用于日志记录,如果BeforeDeserialize 已经运行,也可以设置某种状态,但问题仍然存在。

我仍然需要AfterSerialize 中的代码来处理发送回客户端的响应。但是,如果我尝试删除 AfterSerialize 中的代码并仅运行 BeforeDeserialize' I get aHTTP 400: Bad Request` 中的代码。

这一切都发生在实际的方法调用之前,所以我什至没有进入方法内部的代码(第 10 步)。

【问题讨论】:

标签: c# asp.net web-services soap soap-extension


【解决方案1】:

我的解决方案是基于mikebridge 的解决方案,但我不得不做一些改动。必须包含初始化程序,如果您尝试在不可用的阶段访问soap 消息信息,则会引发异常。

public class SoapLoggingExtension : SoapExtension
{
    private Stream _originalStream;
    private Stream _workingStream;
    private static String _initialMethodName;
    private static string _methodName;
    private static String _xmlResponse;

    /// <summary>
    /// Side effects: saves the incoming stream to
    /// _originalStream, creates a new MemoryStream
    /// in _workingStream and returns it.  
    /// Later, _workingStream will have to be created
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public override Stream ChainStream(Stream stream)
    {

        _originalStream = stream;
        _workingStream = new MemoryStream();
        return _workingStream;
    }

    /// <summary>
    /// Process soap message
    /// </summary>
    /// <param name="message"></param>
    public override void ProcessMessage(SoapMessage message)
    {
        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;

            case SoapMessageStage.AfterSerialize:
                //Get soap call as a xml string
                var xmlRequest = GetSoapEnvelope(_workingStream);
                //Save the inbound method name
                _methodName = message.MethodInfo.Name;
                CopyStream(_workingStream, _originalStream);
                //Log call
                LogSoapRequest(xmlRequest, _methodName, LogObject.Direction.OutPut);
                break;

            case SoapMessageStage.BeforeDeserialize:
                CopyStream(_originalStream, _workingStream);
                //Get xml string from stream before it is used
                _xmlResponse = GetSoapEnvelope(_workingStream);
                break;

            case SoapMessageStage.AfterDeserialize:
                //Method name is only available after deserialize
                _methodName = message.MethodInfo.Name;
                LogSoapRequest(_xmlResponse, _methodName, LogObject.Direction.InPut);
                break;
        }
    }

    /// <summary>
    /// Returns the XML representation of the Soap Envelope in the supplied stream.
    /// Resets the position of stream to zero.
    /// </summary>
    private String GetSoapEnvelope(Stream stream)
    {
        stream.Position = 0;
        StreamReader reader = new StreamReader(stream);
        String data = reader.ReadToEnd();
        stream.Position = 0;
        return data;
    }

    private void CopyStream(Stream from, Stream to)
    {
        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        writer.WriteLine(reader.ReadToEnd());
        writer.Flush();
    }

    public override object GetInitializer(Type serviceType)
    {
        return serviceType.FullName;
    }

    //Never needed to use this initializer, but it has to be implemented
    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        throw new NotImplementedException();
        //return ((TraceExtensionAttribute)attribute).Filename;
    }

    public override void Initialize(object initializer)
    {
        if (String.IsNullOrEmpty(_methodName))
        {
            _initialMethodName = _methodName;
            _waitForResponse = false;
        }
    }

    private void LogSoapRequest(String xml, String methodName, LogObject.Direction direction)
    {

        String connectionString = String.Empty;
        String callerIpAddress = String.Empty;
        String ipAddress = String.Empty;

        try
        {
            //Only log outbound for the response to the original call
            if (_waitForResponse && xml.IndexOf("<" + _initialMethodName + "Response") < 0)
            {
                return;
            }

            if (direction == LogObject.Direction.InPut) {
                _waitForResponse = true;
                _initialMethodName = methodName;
            }

            connectionString = GetSqlConnectionString();
            callerIpAddress = GetClientIp();
            ipAddress = GetClientIp(HttpContext.Current.Request.UserHostAddress);

            //Log call here

            if (!String.IsNullOrEmpty(_methodName) && xml.IndexOf("<" + _initialMethodName + "Response") > 0)
            {
                //Reset static values to initial
                _methodName = String.Empty;
                _initialMethodName = String.Empty;
                _waitForResponse = false;
            }
        }
        catch (Exception ex)
        {
            //Error handling here
        }
    }
    private static string GetClientIp(string ip = null)
    {
        if (String.IsNullOrEmpty(ip))
        {
            ip = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
        }
        if (String.IsNullOrEmpty(ip) || ip.Equals("unknown", StringComparison.OrdinalIgnoreCase))
        {
            ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
        }
        if (ip == "::1")
            ip = "127.0.0.1";
        return ip;
    }
}

methodName 变量用于确定我们等待响应的入站呼叫。这当然是可选的,但在我的解决方案中,我对其他 Web 服务进行了几次调用,但我只想记录对第一次调用的响应。

第二部分是您需要在 web.config 中添加正确的行。显然它对不包括整个类类型定义很敏感(在this example 中只定义了类名,这不起作用。该类从未初始化。):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
    <webServices>
        <soapExtensionTypes>
            <add group="High" priority="1" type="WsNs.SoapLoggingExtension, WsNs, Version=1.0.0.0, Culture=neutral" />
        </soapExtensionTypes>
    </webServices>
</system.web>
</configuration>

【讨论】:

  • 抱歉,是的,我忘记了 XML 注册。如果我有更多这些事情要做,我会在一个基于事件的框架中重写整个混乱,其中事件处理程序永远不会获得未初始化的数据。
  • 你好熊平山。我在完成这项工作时遇到了严重的麻烦。我创建了类,添加了配置部分,甚至创建了一个属性并将其添加到我的网络方法中,但似乎没有任何效果。我将您的 LogSoapRequest 更改为仅将字符串 xml 写入文件,但似乎永远无法达到。你能帮我吗?
【解决方案2】:

这是我的第一次尝试,灵感来自 thisthis

SoapExtension 在处理流以及初始化或未初始化变量时具有各种副作用和隐藏的时间依赖性,因此这是脆弱的代码。我发现关键是将原始流复制到内存流中,然后在正确的时刻再次返回。

public class SoapLoggingExtension : SoapExtension
{

    private Stream _originalStream;
    private Stream _workingStream;
    private string _methodName;
    private List<KeyValuePair<string, string>> _parameters;
    private XmlDocument _xmlResponse;
    private string _url;

    /// <summary>
    /// Side effects: saves the incoming stream to
    /// _originalStream, creates a new MemoryStream
    /// in _workingStream and returns it.  
    /// Later, _workingStream will have to be created
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public override Stream ChainStream(Stream stream)
    {

        _originalStream = stream;
        _workingStream = new MemoryStream();
        return _workingStream;
    }

    /// <summary>
    /// AUGH, A TEMPLATE METHOD WITH A SWITCH ?!?
    /// Side-effects: everywhere
    /// </summary>
    /// <param name="message"></param>
    public override void ProcessMessage(SoapMessage message)
    {
        switch (message.Stage)
        {
            case SoapMessageStage.BeforeSerialize:
                break;

            case SoapMessageStage.AfterSerialize:

                var xmlRequest = GetSoapEnvelope(_workingStream);
                CopyStream(_workingStream, _originalStream);
                LogResponse(xmlRequest, GetIpAddress(), _methodName, _parameters); // final step
                break;

            case SoapMessageStage.BeforeDeserialize:

                CopyStream(_originalStream, _workingStream);
                _xmlResponse = GetSoapEnvelope(_workingStream);
                _url = message.Url;
                break;

            case SoapMessageStage.AfterDeserialize:

                SaveCallInfo(message);                    
                break;
        }
    }

    private void SaveCallInfo(SoapMessage message)
    {
        _methodName = message.MethodInfo.Name;

        // the parameter value is converted to a string for logging, 
        // but this may not be suitable for all applications.
        ParameterInfo[] parminfo = message.MethodInfo.InParameters;
        _parameters = parminfo.Select((t, i) => new KeyValuePair<string, String>(
                t.Name, Convert.ToString(message.GetInParameterValue(i)))).ToList();

    }

    private void LogResponse(
        XmlDocument xmlResponse,
        String ipaddress,
        string methodName, 
        IEnumerable<KeyValuePair<string, string>> parameters)
    {
        // SEND TO LOGGER HERE!
    }

    /// <summary>
    /// Returns the XML representation of the Soap Envelope in the supplied stream.
    /// Resets the position of stream to zero.
    /// </summary>
    private XmlDocument GetSoapEnvelope(Stream stream)
    {
        XmlDocument xml = new XmlDocument();
        stream.Position = 0;
        StreamReader reader = new StreamReader(stream);
        xml.LoadXml(reader.ReadToEnd());
        stream.Position = 0;
        return xml;
    }

    private void CopyStream(Stream from, Stream to)
    {
        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        writer.WriteLine(reader.ReadToEnd());
        writer.Flush();
    }

    // GLOBAL VARIABLE DEPENDENCIES HERE!!
    private String GetIpAddress()
    {
        try
        {
            return HttpContext.Current.Request.UserHostAddress;
        }
        catch (Exception)
        {
            // ignore error;
            return "";
        }
    }

【讨论】:

  • 我发现这也会拦截 OUTGOING soap 调用——在这种情况下,如果您尝试在内部调用 GetInParameterValue,此代码将抛出异常“在消息阶段 AfterDeserialize 中可能无法访问该值”反序列化后。如果您有 SoapClientMessages,您可以将逻辑包装在“if (message is SoapServerMessage)”中,以防止它拦截它们。
  • 感谢您的帮助!我设法使用您的示例作为开始使解决方案起作用。上面的示例缺少 SoapExtension 类的初始化程序,这有点令人困惑。我使用了在this example 中找到的初始化程序。此外,为了确保使用扩展名,您必须在 web.config 中添加几行(使用 this example 作为开始)。我正在添加自己的解决方案作为答案。
猜你喜欢
  • 2011-11-02
  • 2015-03-11
  • 1970-01-01
  • 1970-01-01
  • 2023-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-16
相关资源
最近更新 更多