我正在尝试在当前项目中拍摄Web服务客户端。我不确定服务服务器的平台(很可能是LAMP)。我相信他们的做法是错误的,因为我已经消除了与客户的潜在问题。客户端是从服务WSDL自动生成的标准ASMX类型的Web参考代理。
我需要了解的是RAW SOAP消息(请求和响应)
最好的方法是什么?
Answers:
我进行了以下更改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();
}
myEnvelope
来的?
尝试Fiddler2,它将使您检查请求和响应。可能值得注意的是Fiddler可以同时处理HTTP和HTTP流量。
如果对Web参考的调用引发异常,则似乎Tim Carter的解决方案不起作用。我一直在尝试获取原始Web共振,因此一旦引发异常,就可以在错误处理程序中检查(在代码中)。但是,我发现当调用引发异常时,由Tim的方法编写的响应日志为空。我不完全理解代码,但是看来.tim已经在.Net已经失效并丢弃了Web响应之后切入了过程。
我正在与使用低级编码手动开发Web服务的客户端一起工作。此时,他们将自己的内部过程错误消息作为HTML格式的消息添加到SOAP格式的响应之前的响应中。当然,.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方法将起作用,因为您已将流类型从ConnectStream更改为ChainStream中的MemoryStream,并且MemoryStream确实允许查找操作。(我尝试将ConnectStream强制转换为MemoryStream-不允许。)
因此..... Microsoft应该允许对ChainStream类型进行查找操作,或者使SoapMessage.Stream真正像预期的那样成为只读副本。(写信给国会议员,等等。)
还有一点。在创建了一种在异常发生后检索原始HTTP响应的方法后,我仍然没有获得完整的响应(由HTTP嗅探器确定)。这是因为当开发Web服务将HTML错误消息添加到响应的开头时,它没有调整Content-Length标头,因此Content-Length值小于实际响应主体的大小。我得到的只是Content-Length值的字符数-其余的都丢失了。显然,当.Net读取响应流时,它仅读取Content-Length字符数,并且不允许Content-Length值可能是错误的。这是应该的;但是,如果Content-Length标头值错误,则获取整个响应正文的唯一方法是使用HTTP嗅探器(我来自http://www.ieinspector.com)。
我希望框架通过挂接一个日志流来为您进行日志记录,该日志流在框架处理该基础流时进行记录。以下内容并不尽如人意,因为您无法在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);
}
}
}
这是最佳答案的简化版本。将此添加到<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
和tracemode
属性,但它们会增加可记录的数据量,并避免以十六进制记录所有内容。
我意识到我参加聚会已经很晚了,并且由于实际上并未指定语言,因此这是一个基于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之前,函数中的replace调用是从输出中删除vbCrLf字符。我的文件在生成的xml中有很多,但是显然这要取决于您要序列化的内容,我认为在将对象发送到Web服务期间它们可能会被剥离。