我正在尝试解决当前项目中的 Web 服务客户端问题。我不确定服务服务器的平台(很可能是 LAMP)。我相信他们这边有问题,因为我已经消除了我客户的潜在问题。客户端是从服务 WSDL 自动生成的标准 ASMX 类型 Web 引用代理。
我需要得到的是 RAW SOAP 消息(请求和响应)
解决此问题的最佳方法是什么?
我正在尝试解决当前项目中的 Web 服务客户端问题。我不确定服务服务器的平台(很可能是 LAMP)。我相信他们这边有问题,因为我已经消除了我客户的潜在问题。客户端是从服务 WSDL 自动生成的标准 ASMX 类型 Web 引用代理。
我需要得到的是 RAW SOAP 消息(请求和响应)
解决此问题的最佳方法是什么?
我进行了以下更改web.config
以获取 SOAP(请求/响应)信封。这会将所有原始 SOAP 信息输出到文件trace.log
中。
<system.diagnostics>
<trace autoflush="true"/>
<sources>
<source name="System.Net" maxdatasize="1024">
<listeners>
<add name="TraceFile"/>
</listeners>
</source>
<source name="System.Net.Sockets" maxdatasize="1024">
<listeners>
<add name="TraceFile"/>
</listeners>
</source>
</sources>
<sharedListeners>
<add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener"
initializeData="trace.log"/>
</sharedListeners>
<switches>
<add name="System.Net" value="Verbose"/>
<add name="System.Net.Sockets" value="Verbose"/>
</switches>
</system.diagnostics>
您可以实现将完整请求和响应记录到日志文件的 SoapExtension。然后,您可以在 web.config 中启用 SoapExtension,这样可以轻松打开/关闭以进行调试。这是我找到并修改为我自己使用的示例,在我的情况下,日志记录是由 log4net 完成的,但您可以用自己的方法替换日志方法。
public class SoapLoggerExtension : SoapExtension
{
private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private Stream oldStream;
private Stream newStream;
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return null;
}
public override object GetInitializer(Type serviceType)
{
return null;
}
public override void Initialize(object initializer)
{
}
public override System.IO.Stream ChainStream(System.IO.Stream stream)
{
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
Log(message, "AfterSerialize");
CopyStream(newStream, oldStream);
newStream.Position = 0;
break;
case SoapMessageStage.BeforeDeserialize:
CopyStream(oldStream, newStream);
Log(message, "BeforeDeserialize");
break;
case SoapMessageStage.AfterDeserialize:
break;
}
}
public void Log(SoapMessage message, string stage)
{
newStream.Position = 0;
string contents = (message is SoapServerMessage) ? "SoapRequest " : "SoapResponse ";
contents += stage + ";";
StreamReader reader = new StreamReader(newStream);
contents += reader.ReadToEnd();
newStream.Position = 0;
log.Debug(contents);
}
void ReturnStream()
{
CopyAndReverse(newStream, oldStream);
}
void ReceiveStream()
{
CopyAndReverse(newStream, oldStream);
}
public void ReverseIncomingStream()
{
ReverseStream(newStream);
}
public void ReverseOutgoingStream()
{
ReverseStream(newStream);
}
public void ReverseStream(Stream stream)
{
TextReader tr = new StreamReader(stream);
string str = tr.ReadToEnd();
char[] data = str.ToCharArray();
Array.Reverse(data);
string strReversed = new string(data);
TextWriter tw = new StreamWriter(stream);
stream.Position = 0;
tw.Write(strReversed);
tw.Flush();
}
void CopyAndReverse(Stream from, Stream to)
{
TextReader tr = new StreamReader(from);
TextWriter tw = new StreamWriter(to);
string str = tr.ReadToEnd();
char[] data = str.ToCharArray();
Array.Reverse(data);
string strReversed = new string(data);
tw.Write(strReversed);
tw.Flush();
}
private void CopyStream(Stream fromStream, Stream toStream)
{
try
{
StreamReader sr = new StreamReader(fromStream);
StreamWriter sw = new StreamWriter(toStream);
sw.WriteLine(sr.ReadToEnd());
sw.Flush();
}
catch (Exception ex)
{
string message = String.Format("CopyStream failed because: {0}", ex.Message);
log.Error(message, ex);
}
}
}
[AttributeUsage(AttributeTargets.Method)]
public class SoapLoggerExtensionAttribute : SoapExtensionAttribute
{
private int priority = 1;
public override int Priority
{
get { return priority; }
set { priority = value; }
}
public override System.Type ExtensionType
{
get { return typeof (SoapLoggerExtension); }
}
}
然后将以下部分添加到 web.config 中,其中 YourNamespace 和 YourAssembly 指向 SoapExtension 的类和程序集:
<webServices>
<soapExtensionTypes>
<add type="YourNamespace.SoapLoggerExtension, YourAssembly"
priority="1" group="0" />
</soapExtensionTypes>
</webServices>
不知道为什么对 web.config 或序列化程序类大惊小怪。下面的代码对我有用:
XmlSerializer xmlSerializer = new XmlSerializer(myEnvelope.GetType());
using (StringWriter textWriter = new StringWriter())
{
xmlSerializer.Serialize(textWriter, myEnvelope);
return textWriter.ToString();
}
试试Fiddler2,它会让你检查请求和响应。值得注意的是,Fiddler 同时适用于 http 和 https 流量。
如果对 Web 引用的调用引发异常,那么 Tim Carter 的解决方案似乎不起作用。我一直在尝试获取原始的网络响应,以便一旦抛出异常,我就可以在错误处理程序中检查它(在代码中)。但是,我发现当调用抛出异常时,Tim 的方法写入的响应日志是空白的。我不完全理解代码,但似乎 Tim 的方法在 .Net 已经失效并丢弃 Web 响应之后切入了该过程。
我正在与一个使用低级编码手动开发 Web 服务的客户端合作。此时,他们将自己的内部流程错误消息作为 HTML 格式的消息添加到响应中,然后是 SOAP 格式的响应。当然,automagic .Net 网络参考对此大加赞赏。如果在抛出异常后我可以获取原始 HTTP 响应,我可以在混合返回的 HTTP 响应中查找并解析任何 SOAP 响应,并知道它们是否正确接收了我的数据。
之后 ...
这是一个有效的解决方案,即使在执行之后(请注意,我只是在响应之后 - 也可以得到请求):
namespace ChuckBevitt
{
class GetRawResponseSoapExtension : SoapExtension
{
//must override these three methods
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return null;
}
public override object GetInitializer(Type serviceType)
{
return null;
}
public override void Initialize(object initializer)
{
}
private bool IsResponse = false;
public override void ProcessMessage(SoapMessage message)
{
//Note that ProcessMessage gets called AFTER ChainStream.
//That's why I'm looking for AfterSerialize, rather than BeforeDeserialize
if (message.Stage == SoapMessageStage.AfterSerialize)
IsResponse = true;
else
IsResponse = false;
}
public override Stream ChainStream(Stream stream)
{
if (IsResponse)
{
StreamReader sr = new StreamReader(stream);
string response = sr.ReadToEnd();
sr.Close();
sr.Dispose();
File.WriteAllText(@"C:\test.txt", response);
byte[] ResponseBytes = Encoding.ASCII.GetBytes(response);
MemoryStream ms = new MemoryStream(ResponseBytes);
return ms;
}
else
return stream;
}
}
}
这是您在配置文件中配置它的方式:
<configuration>
...
<system.web>
<webServices>
<soapExtensionTypes>
<add type="ChuckBevitt.GetRawResponseSoapExtension, TestCallWebService"
priority="1" group="0" />
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
“TestCallWebService”应该替换为库的名称(恰好是我正在使用的测试控制台应用程序的名称)。
你真的不应该去ChainStream;您应该能够更简单地从 ProcessMessage 做到这一点:
public override void ProcessMessage(SoapMessage message)
{
if (message.Stage == SoapMessageStage.BeforeDeserialize)
{
StreamReader sr = new StreamReader(message.Stream);
File.WriteAllText(@"C:\test.txt", sr.ReadToEnd());
message.Stream.Position = 0; //Will blow up 'cause type of stream ("ConnectStream") doesn't alow seek so can't reset position
}
}
如果您查找 SoapMessage.Stream,它应该是一个只读流,此时您可以使用它来检查数据。这是一个错误,因为如果您确实读取了流,后续处理炸弹没有发现数据错误(流在末尾)并且您无法将位置重置为开头。
有趣的是,如果您同时使用 ChainStream 和 ProcessMessage 两种方法,则 ProcessMessage 方法将起作用,因为您在 ChainStream 中将流类型从 ConnectStream 更改为 MemoryStream,并且 MemoryStream 确实允许查找操作。(我尝试将 ConnectStream 转换为 MemoryStream - 不允许。)
所以.....微软应该要么允许对 ChainStream 类型进行搜索操作,要么让 SoapMessage.Stream 真正成为它应该的只读副本。(写你的国会议员,等等......)
还有一点。在创建了一种在异常后检索原始 HTTP 响应的方法后,我仍然没有得到完整的响应(由 HTTP 嗅探器确定)。这是因为开发 Web 服务在响应开头添加 HTML 错误消息时,并没有调整 Content-Length 标头,因此 Content-Length 值小于实际响应正文的大小。我得到的只是 Content-Length 值的字符数 - 其余的都丢失了。显然,当 .Net 读取响应流时,它只读取 Content-Length 字符数并且不允许 Content-Length 值可能是错误的。这是应该的;但是如果 Content-Length 标头值错误,您获得整个响应正文的唯一方法是使用 HTTP 嗅探器(我使用 HTTP Analyzer fromhttp://www.ieinspector.com)。
这是最佳答案的简化版本。将此添加到您的或文件的<configuration>
元素中。它将在您的项目文件夹中创建一个文件。或者,您可以使用该属性指定日志文件的绝对路径。web.config
App.config
trace.log
bin/Debug
initializeData
<system.diagnostics>
<trace autoflush="true"/>
<sources>
<source name="System.Net" maxdatasize="9999" tracemode="protocolonly">
<listeners>
<add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener" initializeData="trace.log"/>
</listeners>
</source>
</sources>
<switches>
<add name="System.Net" value="Verbose"/>
</switches>
</system.diagnostics>
它警告不允许使用maxdatasize
and属性,但它们会增加可以记录的数据量,并避免以十六进制记录所有内容。tracemode
我希望框架通过挂钩日志流来为您进行日志记录,该日志流记录为框架处理该底层流。以下内容不像我希望的那样干净,因为您无法在 ChainStream 方法中的请求和响应之间做出决定。以下是我的处理方式。感谢 Jon Hanna 提出了最重要的流创意
public class LoggerSoapExtension : SoapExtension
{
private static readonly string LOG_DIRECTORY = ConfigurationManager.AppSettings["LOG_DIRECTORY"];
private LogStream _logger;
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return null;
}
public override object GetInitializer(Type serviceType)
{
return null;
}
public override void Initialize(object initializer)
{
}
public override System.IO.Stream ChainStream(System.IO.Stream stream)
{
_logger = new LogStream(stream);
return _logger;
}
public override void ProcessMessage(SoapMessage message)
{
if (LOG_DIRECTORY != null)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
_logger.Type = "request";
break;
case SoapMessageStage.AfterSerialize:
break;
case SoapMessageStage.BeforeDeserialize:
_logger.Type = "response";
break;
case SoapMessageStage.AfterDeserialize:
break;
}
}
}
internal class LogStream : Stream
{
private Stream _source;
private Stream _log;
private bool _logSetup;
private string _type;
public LogStream(Stream source)
{
_source = source;
}
internal string Type
{
set { _type = value; }
}
private Stream Logger
{
get
{
if (!_logSetup)
{
if (LOG_DIRECTORY != null)
{
try
{
DateTime now = DateTime.Now;
string folder = LOG_DIRECTORY + now.ToString("yyyyMMdd");
string subfolder = folder + "\\" + now.ToString("HH");
string client = System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.Request != null && System.Web.HttpContext.Current.Request.UserHostAddress != null ? System.Web.HttpContext.Current.Request.UserHostAddress : string.Empty;
string ticks = now.ToString("yyyyMMdd'T'HHmmss.fffffff");
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
if (!Directory.Exists(subfolder))
Directory.CreateDirectory(subfolder);
_log = new FileStream(new System.Text.StringBuilder(subfolder).Append('\\').Append(client).Append('_').Append(ticks).Append('_').Append(_type).Append(".xml").ToString(), FileMode.Create);
}
catch
{
_log = null;
}
}
_logSetup = true;
}
return _log;
}
}
public override bool CanRead
{
get
{
return _source.CanRead;
}
}
public override bool CanSeek
{
get
{
return _source.CanSeek;
}
}
public override bool CanWrite
{
get
{
return _source.CanWrite;
}
}
public override long Length
{
get
{
return _source.Length;
}
}
public override long Position
{
get
{
return _source.Position;
}
set
{
_source.Position = value;
}
}
public override void Flush()
{
_source.Flush();
if (Logger != null)
Logger.Flush();
}
public override long Seek(long offset, SeekOrigin origin)
{
return _source.Seek(offset, origin);
}
public override void SetLength(long value)
{
_source.SetLength(value);
}
public override int Read(byte[] buffer, int offset, int count)
{
count = _source.Read(buffer, offset, count);
if (Logger != null)
Logger.Write(buffer, offset, count);
return count;
}
public override void Write(byte[] buffer, int offset, int count)
{
_source.Write(buffer, offset, count);
if (Logger != null)
Logger.Write(buffer, offset, count);
}
public override int ReadByte()
{
int ret = _source.ReadByte();
if (ret != -1 && Logger != null)
Logger.WriteByte((byte)ret);
return ret;
}
public override void Close()
{
_source.Close();
if (Logger != null)
Logger.Close();
base.Close();
}
public override int ReadTimeout
{
get { return _source.ReadTimeout; }
set { _source.ReadTimeout = value; }
}
public override int WriteTimeout
{
get { return _source.WriteTimeout; }
set { _source.WriteTimeout = value; }
}
}
}
[AttributeUsage(AttributeTargets.Method)]
public class LoggerSoapExtensionAttribute : SoapExtensionAttribute
{
private int priority = 1;
public override int Priority
{
get
{
return priority;
}
set
{
priority = value;
}
}
public override System.Type ExtensionType
{
get
{
return typeof(LoggerSoapExtension);
}
}
}
这是在 c# 中获取原始消息的命令:
OperationContext.Current.RequestContext.RequestMessage
我浪费了 3 天时间尝试使用肥皂扩展,这比我想象的要简单得多。不推荐使用肥皂扩展。我建议在 MSDN 上阅读这些操作方法:检查或修改客户端上的消息。并使用消息类
我意识到我参加聚会已经很晚了,因为实际上并没有指定语言,所以这里有一个基于 Bimmerbound 答案的 VB.NET 解决方案,以防有人碰巧遇到这个并需要解决方案。注意:如果您还没有,您需要在项目中引用 stringbuilder 类。
Shared Function returnSerializedXML(ByVal obj As Object) As String
Dim xmlSerializer As New System.Xml.Serialization.XmlSerializer(obj.GetType())
Dim xmlSb As New StringBuilder
Using textWriter As New IO.StringWriter(xmlSb)
xmlSerializer.Serialize(textWriter, obj)
End Using
returnSerializedXML = xmlSb.ToString().Replace(vbCrLf, "")
End Function
只需调用该函数,它就会返回一个字符串,其中包含您尝试传递给 Web 服务的对象的序列化 xml(实际上,这也适用于您想扔给它的任何对象)。
作为旁注,在返回 xml 之前函数中的替换调用是从输出中去除 vbCrLf 字符。我的在生成的 xml 中有一堆,但是这显然会根据您尝试序列化的内容而有所不同,我认为它们可能会在对象被发送到 Web 服务期间被剥离。