WCF 面向服务的SOAP消息
2011-07-26 16:21 田志良 阅读(4335) 评论(1) 编辑 收藏 举报显然地,面向服务系统开发首先应该是创建契约。为了例子简单,一个订单包含一个产品ID(product ID)、数量(quantity)和状态消息(status message)。有了这三个字段,一个订单可以使用下面的伪schema代码表示:
<Order> <ProdID>xs:integer</ProdID> <Qty>xs:integer</Qty> <Status>xs:string</Status> </Order>
从我们消息自治和配置地址的讨论,我们知道消息需要更多的地址结构,如果我们想使用WS-Addressing。在我们的订单处理服务里,消息发送者和接收者统一使用遵守WS-Addressing规范的SOAP消息来限制消息的结构。有了这个规则,下面就是一个结构合理的消息例子:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsa="http:// schemas.xmlsoap.org/ws/2004/08/addressing"> <s:Header> <wsa:Action s:mustUnderstand="1">urn:SubmitOrder</wsa:Action> <wsa:MessageID>4</wsa:MessageID> <wsa:ReplyTo> <wsa:Address> http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous </wsa:Address> </wsa:ReplyTo> <wsa:To s:mustUnderstand="1">http://localhost:8000/Order</wsa:To> </s:Header> <s:Body> <Order> <ProdID>6</ProdID> <Qty>6</Qty> <Status>order placed</Status> </Order> </s:Body> </s:Envelope>
在创建完描述我们的消息的schema之后,下一个步骤就是定义接受消息的终结点。当使用C#表示契约的时候,我们可以选择定义一个类或者接口。下面是用C#接口定义一个契约的例子:
// file: Contracts.cs using System; using System.ServiceModel; using System.ServiceModel.Channels; // define the contract for the service [ServiceContract(Namespace = "http://wintellect.com/ProcessOrder")] public interface IProcessOrder { [OperationContract(Action="urn:SubmitOrder")] void SubmitOrder(Message order); }
无论我们选择什么方式表达契约,它都应该在发送者和接收者程序进一步开发以前确定。实际上,接收者定义需要的消息结构契约,发送者通常尝试按照这个契约构建和发送消息。
没什么能阻止发送者不按照消息接收者定义的契约来发送消息。因此,接收者第一个任务应该是验证接收到的消息是否符合契约。这个方法保证了接收者的数据结构不会被破坏。这些观点经常在分布式开发社区里讨论。
这个契约可以被编译为一个程序集。一旦编译完成,程序集可以指派给发送者和接收者。这个程序集代表了发送者和接收者之间的契约。当契约还会变化确定的次数的时候,我们应该认为在共享以后它是不可变的。
// File: Receiver.cs // Implement the interface defined in the contract assembly public sealed class MyService : IProcessOrder { public void SubmitOrder(Message order) { // Do work here } } // File: Receiver.cs using System; using System.Xml; using System.IO; using System.ServiceModel; using System.ServiceModel.Channels; // Implement the interface defined in the contract assembly实现程序集里定义的契约接口 public sealed class MyService : IProcessOrder { public void SubmitOrder(Message order) { // Create a file name from the MessageID根据MessageID创建文件名 String fileName = "Order" + order.Headers.MessageId.ToString() + ".xml"; // Signal that a message has arrived提示消息到达 Console.WriteLine("Message ID {0} received", order.Headers.MessageId.ToString()); // create an XmlDictionaryWriter to write to a file创建一个XmlDictionaryWriter去写文件 XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter( new FileStream(fileName, FileMode.Create)); // write the message to a file写消息到文件里 order.WriteMessage(writer); writer.Close(); } }
我们下一个任务就是让MyService类型去接受请求消息。为了接受消息:
- MyService必须加载进AppDomain。
- MyService(或者另外一个类型)必须侦听内部消息。
- 适当的时候,比如创建一个此类型的实例,并且只要需要就要引用它(为了阻止垃圾收集器GC释放对象的内存)。
- 当一个消息到来时,它必须被分发给MyService的实例,激活SubmitOrder方法。
这些任务通常由宿主(Host)来执行。假设我们的AppDomain托管在一个控制台程序里,管理和分发消息到MyService对象的类型是System.ServiceModel.ServiceHost。我们的控制台应用代码如下:
// File: ReceiverHost.cs using System; using System.Xml; using System.ServiceModel; internal static class ReceiverHost { public static void Main() { // Define the binding for the service定义服务绑定 WSHttpBinding binding = new WSHttpBinding(SecurityMode.None); // Use the text encoder binding.MessageEncoding = WSMessageEncoding.Text; // Define the address for the service定义服务地址 Uri addressURI = new Uri(@"http://localhost:4000/Order"); // Instantiate a Service host using the MyService type使用MyService实例化服务宿主 ServiceHost svc = new ServiceHost(typeof(MyService)); // Add an endpoint to the service with the给服务增加一个终结点 // contract, binding, and address svc.AddServiceEndpoint(typeof(IProcessOrder), binding, addressURI); // Open the service host to start listening打开服务宿主开始侦听 svc.Open(); Console.WriteLine("The receiver is ready"); Console.ReadLine(); svc.Close(); } }
每个服务包含一个地址、一个绑定和一个契约。这些机制常常被成为WCF的ABC。现在,假设如下:
- 一个地址描述了服务侦听请求消息的地方
- 一个绑定描述了服务如何侦听消息
- 一个契约描述了服务会将会接收什么样的消息
你也许想知道当一个消息到达at http://localhost:8000/Order时会发生什么。答案取决于到达终结点的消息类型。就此来说,我们来做个简单的例子。更高层次上,我们的消息发送者必须要知道下面三点:
- 服务在哪里(地址address)
- 服务期望消息如何被传递(绑定binding)
- 服务期望什么类型的消息(契约contract)
假设这些大家都知道,下面就是一个合格的消息发送应用:
// File: Sender.cs using System; using System.Text; using System.Xml; using System.ServiceModel; using System.Runtime.Serialization; using System.IO; using System.ServiceModel.Channels; public static class Sender { public static void Main(){ Console.WriteLine("Press ENTER when the receiver is ready"); Console.ReadLine(); // address of the receiving application接受程序的地址 EndpointAddress address = new EndpointAddress(@"http://localhost:4000/Order"); // Define how we will communicate with the service定义一个我们通信的服务 // In this case, use the WS-* compliant HTTP binding这个例子里,我们可以使用WS的HTTP绑定 WSHttpBinding binding = new WSHttpBinding(SecurityMode.None); binding.MessageEncoding = WSMessageEncoding.Text; // Create a channel创建通道 ChannelFactory<IProcessOrder> channel = new ChannelFactory<IProcessOrder>(binding, address); // Use the channel factory to create a proxy使用通道工厂创建代理 IProcessOrder proxy = channel.CreateChannel(); // Create some messages创建一些消息 Message msg = null; for (Int32 i = 0; i < 10; i++) { // Call our helper method to create the message调用我们的helper方法创建消息 // notice the use of the Action defined in注意使用在IProcessOrder契约里定义的Action // the IProcessOrder contract msg = GenerateMessage(i,i); // Give the message a MessageID SOAP headerSOAP消息头里加MessageID UniqueId uniqueId = new UniqueId(i.ToString()); msg.Headers.MessageId = uniqueId; Console.WriteLine("Sending Message # {0}", uniqueId.ToString()); // Give the message an Action SOAP headerSOAP消息头里加Action msg.Headers.Action = "urn:SubmitOrder"; // Send the message发送消息 proxy.SubmitOrder(msg); } } // method for creating a Message创建消息的方法 private static Message GenerateMessage(Int32 productID, Int32 qty) { MemoryStream stream = new MemoryStream(); XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter( stream, Encoding.UTF8, false); writer.WriteStartElement("Order"); writer.WriteElementString("ProdID", productID.ToString()); writer.WriteElementString("Qty", qty.ToString()); writer.WriteEndElement(); writer.Flush(); stream.Position = 0; XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader( stream, XmlDictionaryReaderQuotas.Max); // Create the message with the Action and the body使用Action和body创建消息 return Message.CreateMessage(MessageVersion.Soap12WSAddressing10, String.Empty, reader); } }
在发送程序里按回车(ENTER)键,我们应该可以在接受程序看到下面的结果:
The receiver is ready Message ID 0 received Message ID 1 received Message ID 2 received Message ID 3 received Message ID 4 received Message ID 5 received Message ID 6 received Message ID 7 received Message ID 8 received Message ID 9 received
恭喜!你已经使用WCF完成了一个面向服务的程序。记住服务把内部消息写进文件里。如果我们检查服务写过的文件,会看到如下:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:SubmitOrder</a:Action> <a:MessageID>1</a:MessageID> <a:ReplyTo> <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> </a:ReplyTo> <a:To s:mustUnderstand="1">http://localhost:4000/Order</a:To> </s:Header> <s:Body> <Order> <ProdID>1</ProdID> <Qty>1</Qty> </Order> </s:Body> </s:Envelope>
消息头应该和我们在WS-Addressing规范里看到的一样看起来有些奇怪,并且它们的值应该与我们在消息发送应用里设置的属性很一样。事实上,System.ServiceModel.Message类型暴露了一个叫做Headers的属性,它是属于System.ServiceModel.MessageHeaders类型。这个MessageHeaders类型暴露的其它属性可以表示WS-Addressing的消息头。这里的想法就是我们可以使用WCF面向对象的编程模型去影响面向服务的SOAP消息。