WCF是.NET平台下实现SOA的一种手段,SOA的一个重要的特征就基于Message的通信方式。从Messaging的角度讲,WCF可以看成是对Message进行发送、传递、接收、基础的工具。对于一个消息交换的过程,很多人只会关注message的最初的发送端和最终的接收端。实际上在很多情况下,在两者之间还存在很多的中间结点(Intermediary),这些中间结点在可能在实际的应用中发挥中重要的作用。比如,我们可以创建路由器(Router)进行消息的转发,甚至是Load Balance;可以创建一个消息拦截器(Interceptor)获取request或者response message,并进行Audit、Logging和Instrumentation。今天我们就我们的目光转向这些充当着中间人角色的Intermediary上面来。
在本篇文章中,我们将会创建一个message的拦截和转发工具(message interceptor)。它将被置于WCF调用的client和service之间,拦截并转发从client到service的request message,以及service到client的response message,并将request message和response message显示到一个可视化的界面上。我们将讨论这个message interceptor若干种不同的实现方式。
有一点需要明确说明的是,这个工具的创建并非我写作这篇文章的目的,我的目的是通过一个具体的例子让大家以一种直观方式对WCF的Addressing机制有一个深刻的认识。在介绍message interceptor的创建过程中,我会穿插介绍一个WCF的其它相关知识,比如Message Filtering、Operation Selection、Must Understand Validation等等。
一、创建一个简单的WCF应用
由于我们将要创建的message interceptor需要应用到具体的WCF应用中进行工作和检验,我们需要首先创建一个简单的WCF应用。我们创建一个简单的Calculation的例子。这个solution采用我们熟悉的四层结构(Interceptor用于host我们的message intercept service):
1、Contract:Artech.MessageInterceptor.Contracts.ICalculate
1: using System.ServiceModel;
2: namespace Artech.MessageInterceptor.Contracts
3: {
4: [ServiceContract]
5: public interface ICalculate
6: {
7: [OperationContract]
8: double Add(double x, double y);
9: }
10: }
2、Service:Artech.MessageInterceptor.Services.CalculateService
1: using Artech.MessageInterceptor.Contracts;
2: namespace Artech.MessageInterceptor.Services
3: {
4: public class CalculateService : ICalculate
5: {
6: #region ICalculate Members
7:
8: public double Add(double x, double y)
9: {
10: return x + y;
11: }
12:
13: #endregion
14: }
15: }
16:
3、Hosting:Artech.MessageInterceptor.Hosting.Program
1: using System;
2: using System.ServiceModel;
3: using Artech.MessageInterceptor.Services;
4: namespace Artech.MessageInterceptor.Hosting
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ServiceHost host = new ServiceHost(typeof(CalculateService)))
11: {
12: host.Opened += delegate
13: {
14: Console.WriteLine("The calculate service has been started up!");
15: };
16: host.Open();
17: Console.Read();
18: }
19: }
20: }
21: }
22:
Configuration
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <customBinding>
6: <binding name="MyCustomeBinding">
7: <textMessageEncoding />
8: <httpTransport />
9: </binding>
10: </customBinding>
11: </bindings>
12: <services>
13: <service name="Artech.MessageInterceptor.Services.CalculateService">
14: <endpoint binding="customBinding" bindingConfiguration="MyCustomeBinding"
15: contract="Artech.MessageInterceptor.Contracts.ICalculate"
16: address="http://127.0.0.1:9999/calculateservice"/>
17: </service>
18: </services>
19: </system.serviceModel>
20: </configuration>
在host我们的calculateservice的时候,我们使用了抛弃了系统定义的binding,而采用一个custom binding。是因为custom binding基于更好的可扩展能力,以利于我们后续的介绍。为了简单起见,我们仅仅需要bing为了提供最基本的功能:传输与编码,为此我仅仅添加了两个binding element:textMessageEncoding 和httpTransport。我们将在后面部分应用其他的功能,比如WS-Security.
4、Client:Artech.MessageInterceptor.Clients.Program
1: using System;
2: using System.ServiceModel;
3: using Artech.MessageInterceptor.Contracts;
4: namespace Artech.MessageInterceptor.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculate> channelFactory = new ChannelFactory<ICalculate>("calculateservice"))
11: {
12: ICalculate calculator = channelFactory.CreateChannel();
13: using (calculator as IDisposable)
14: {
15: Console.WriteLine("x + y = {2} where x = {0} ans y = {1}", 1, 2, calculator.Add(1, 2));
16: }
17: }
18:
19: Console.Read();
20: }
21: }
22: }
23:
Configuration:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <customBinding>
6: <binding name="MyCustomBinding">
7: <textMessageEncoding />
8: <httpTransport />
9: </binding>
10: </customBinding>
11: </bindings>
12: <client>
13: <endpoint name="calculateservice" address="http://127.0.0.1:9999/calculateservice" binding="customBinding" bindingConfiguration="MyCustomBinding"
14: contract="Artech.MessageInterceptor.Contracts.ICalculate" />
15: </client>
16: </system.serviceModel>
17: </configuration>
18:
二、创建Message Interceptor
现在我们正式开始进行我们的消息拦截与转发工具的创建。这个工具本质是一个WCF service(我们姑且称它为Intercept service),在该service中定义一个operation进行消息的拦截、处理、转发的功能(如下图所示)。
一般地我们有两种不同的方案来来实现我们的功能:
- Client调用service的时候,主动将message发送到Intercept service;Intercept service获取request并对其进行相应处理后,将message原封不动地转发到真正的service,并接受response message。对response message进行相应处理后,将其返回给client。
- Client照常访问service,但是将Intercept service监听地址设置为service的地址(并对service的监听地址也作相应的修改),那么 client对service访问过程中发送的message将会被Intercept service截获,Intercept service向上面一样进行message处理和转发。
我们先采用第一种实现方案。
1、Contract定义:Artech.MessageInterceptor.Contracts.IIntercept
我们来介绍Intercept service的定义,先来看看Contract的定义(Intercept service的contract和ICalculate定义在同一个project中):
1: using System.ServiceModel.Channels;
2: using System.ServiceModel;
3: namespace Artech.MessageInterceptor.Contracts
4: {
5: [ServiceContract]
6: public interface IIntercept
7: {
8: [OperationContract(Action ="*", ReplyAction="*")]
9: Message Intercept(Message request);
10: }
11: }
12:
Intercept service的contract具有如下两个特点:
- Intercept的参数和返回值都是Message对象。
- Operation的Action和ReplyAction为*。
我们先来讲将第一个特征,之所以我们要使用untyped message作为参数和返回值,是因为我们要将Intercept打造成一个“万能”的操作:能够处理任何请求和返回。我们知道,虽然我们在进行WCF service调用的时候,我们的参数列表,无论是个数、数据类型和次序,都千差万别,我们的返回值类型也各有不同,但是WCF service的调用最终是基于Message的,所以我们的参数或者返回值最终都将转变成message对象(input参数:request message;ref/out 参数和返回值:response message),我们我们的Intercept将是一个“万能”的operation。
至于第二个问题,我们就需要了解WCF的一个重要的机制了:Operation Selection。WCF的Channel Listener监听并接收request message后,Channel Dispatcher通过Contract Message Filter和Address Message Filter选择对应的Endpoint Dispatcher;Endpoint Dispatcher通过InstanceContext/InstanceProvider获得或者创建service intance,并通过reflection调用对应Operation。但是Operation是如何选择的呢?默认的情况下是根据Message的Action Header进行选择的,一般地将会按照这样的匹配规则进行:Contract Namespace(default:http://tempuri.org)/Contract Name(default:Interface name)/Action(default:method name)= action in SOAP header。如果将Action设为“*”将意味着:对intercept service的调用,无路SOAP Header中action是什么,都将交付Intercept来处理。
2、Service的定义:Artech.MessageInterceptor.Services.InterceptService
Intercept service将会完整这样的功能:拦截request message并将其显示到一个Windows form的TextBox中;将message原封不动地向service转发;向处理request message一样拦截并显示response message。
1: using System;
2: using Artech.MessageInterceptor.Contracts;
3: using System.ServiceModel.Channels;
4: using System.Threading;
5: using System.ServiceModel;
6: using System.ServiceModel.Description;
7: namespace Artech.MessageInterceptor.Services
8: {
9: [ServiceBehavior(UseSynchronizationContext = false, AddressFilterMode = AddressFilterMode.Any)]
10: public class InterceptService : IIntercept
11: {
12: private const string CalculateServiceEndpoint = "calculateService";
13: public static SynchronizationContext SynchronizationContext
14: { get; set; }
15: public static System.Windows.Forms.TextBox MessageDisplayPanel
16: { get; set; }
17:
18: #region IIntercept Members
19:
20: public Message Intercept(Message request)
21: {
22: using (ChannelFactory<IIntercept> channelFactory = new ChannelFactory<IIntercept>(CalculateServiceEndpoint))
23: {
24: IIntercept interceptor = channelFactory.CreateChannel();
25: using (interceptor as IDisposable)
26: {
27: MessageBuffer requstBuffer = request.CreateBufferedCopy(int.MaxValue);
28: Message response = interceptor.Intercept(requstBuffer.CreateMessage());
29: MessageBuffer responseBuffer = response.CreateBufferedCopy(int.MaxValue);
30: SynchronizationContext.Post(delegate
31: {
32: MessageDisplayPanel.Text += string.Format("Request:{0}{1}{0}", Environment.NewLine, request);
33: MessageDisplayPanel.Text += string.Format("Response:{0}{1}{0}", Environment.NewLine, response);
34: }, null);
35: return responseBuffer.CreateMessage();
36: }
37: }
38: }
39:
40: #endregion
41: }
42: }
43:
对于InterceptService的定义,有下面几点需要说明:
- UseSynchronizationContext 和SynchronizationContext:这是关于Windows Form 线程关联性的相关设置与应用,在我的前两篇已有详细的介绍,不清楚的可以参阅这篇文章(WCF下的线程关联性)
- AddressFilterMode = AddressFilterMode.Any:在上面我们提到过,ChannelDispatcher在选择EndpointDispacher的时候是基于两个Message Filter:Address Filter和Contract Filter。也就是说,ChannelDispatcher通过这两个Filter选择合适Endpoint。在默认的情况下,Address Filter是根据SOAP的To Message Header的URI来进行栓选的,所以需要Endpoint的Address和To Header中的Addres完全匹配。但是在我们CalculateService的例子中,由于Client最终是访问的时CalculateService,所以生成的SOAP的To Headler的地址是CalculateService的地址:http://127.0.0.1:9999/calculateservice,而我们需要是用InterceptService 来处理该请求,Address Filtering肯定是不能通过的。好在我们可以在ServiceBehavior设置AddressFilterMode 来改变Address Filtering的方式。AddressFilterMode = AddressFilterMode.Any意味着,Address Filtering会被忽略。
- Message的转发,直接通过CalculateService的endpoint name创建的Proxy对象的service调用完成。
- CreateBufferedCopy:可能有人会奇怪,为什么不对request message和response message进行直接操作(将他们显示在TextBox上)?这是应为Message在WCF有一个特殊的处理机制:只有Message的State为Created的时候,才能获取MessageBody的内容,否则会抛出异常。而我们在对Message进行相应操作的时候,会改变Message 的State(Read,Written,Copied,Closed)。所以对response message来讲,对message的显示实际上将Sate改为Read,如何将response message直接返回到client,对该message的读取操作将是不允许的,所以先调用CreateBufferedCopy创建该message的一个memory buffer,最有返回的时通过该buffer重新创建的Message。
3、Service的Hosting:
我们创建了一个Windows Form Application来host InterceptService,并在一个Form的Load事件中完成host。
1: using System;
2: using System.Windows.Forms;
3: using System.ServiceModel;
4: using Artech.MessageInterceptor.Services;
5: using System.Threading;
6: namespace Artech.MessageInterceptor.Interceptor
7: {
8: public partial class MessageInterceptor : Form
9: {
10: private ServiceHost _serviceHost;
11: public MessageInterceptor()
12: {
13: InitializeComponent();
14: }
15:
16: private void MessageInterceptor_Load(object sender, EventArgs e)
17: {
18: this._serviceHost = new ServiceHost(typeof(InterceptService));
19: this._serviceHost.Opened += delegate
20: {
21: this.Text += ":Started";
22: };
23:
24: InterceptService.SynchronizationContext = SynchronizationContext.Current;
25: InterceptService.MessageDisplayPanel = this.textBoxMessage;
26: this._serviceHost.Open();
27: }
28: }
29: }
30:
下面是configuration:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <bindings>
5: <customBinding>
6: <binding name="MyCustomBinding">
7: <textMessageEncoding />
8: <httpTransport manualAddressing="true" />
9: </binding>
10: </customBinding>
11: </bindings>
12: <client>
13: <endpoint address="http://127.0.0.1:9999/calculateservice" binding="customBinding"
14: bindingConfiguration="MyCustomBinding" contract="Artech.MessageInterceptor.Contracts.IIntercept"
15: name="calculateService" />
16: </client>
17: <services>
18: <service name="Artech.MessageInterceptor.Services.InterceptService">
19: <endpoint binding="customBinding" bindingConfiguration="MyCustomBinding"
20: contract="Artech.MessageInterceptor.Contracts.IIntercept"
21: address="http://127.0.0.1:8888/Interceptservice"/>
22: </service>
23: </services>
24: </system.serviceModel>
25: </configuration>
26:
这里需要注意的client的配置,可能有人会有这样的疑惑:Address是CalculateService的地址,但是Contract确是InterceptService的Contract,这不是不匹配吗?实际上由于IIntercept中Intercept方式的参数和返回值都是Message,所以他们代表一切操作。
三、应用InteceptService
现在我们将我们创建InteceptService应用到我们CalculateService中。我们在上面已经提到过,我们现在是方案时要client自动将message发送到InteceptService。在WCF中有一个特殊的EndpointBehavior。(System.ServiceModel.Description.ClientViaBehavior),来实现这样的功能:Message真正发送的地址不同是service真正的地址。基本的原理如下图所示:
我们现在只需要改变client端的配置即可:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <endpointBehaviors>
6: <behavior name="ClientViaBehavior">
7: <clientVia viaUri="http://127.0.0.1:8888/Interceptservice" />
8: </behavior>
9: </endpointBehaviors>
10: </behaviors>
11: <bindings>
12: <customBinding>
13: <binding name="MyCustomBinding">
14: <textMessageEncoding />
15: <httpTransport />
16: </binding>
17: </customBinding>
18: </bindings>
19: <client>
20: <endpoint name="calculateservice" address="http://127.0.0.1:9999/calculateservice" behaviorConfiguration="ClientViaBehavior"
21: binding="customBinding" bindingConfiguration="MyCustomBinding"
22: contract="Artech.MessageInterceptor.Contracts.ICalculate" />
23: </client>
24: </system.serviceModel>
25: </configuration>
26:
当我们运行我们的程序(先启动两个host程序,然后是client),Interceptor Windows Forms Appliction的窗体上将会看到被拦截的request message和response message:
WCF后续之旅:
WCF后续之旅(1): WCF是如何通过Binding进行通信的
WCF后续之旅(2): 如何对Channel Layer进行扩展——创建自定义Channel
WCF后续之旅(3): WCF Service Mode Layer 的中枢—Dispatcher
WCF后续之旅(4):WCF Extension Point 概览
WCF后续之旅(5): 通过WCF Extension实现Localization
WCF后续之旅(6): 通过WCF Extension实现Context信息的传递
WCF后续之旅(7):通过WCF Extension实现和Enterprise Library Unity Container的集成
WCF后续之旅(8):通过WCF Extension 实现与MS Enterprise Library Policy Injection Application Block 的集成
WCF后续之旅(9):通过WCF的双向通信实现Session管理[Part I]
WCF后续之旅(9): 通过WCF双向通信实现Session管理[Part II]
WCF后续之旅(10): 通过WCF Extension实现以对象池的方式创建Service Instance
WCF后续之旅(11): 关于并发、回调的线程关联性(Thread Affinity)
WCF后续之旅(12): 线程关联性(Thread Affinity)对WCF并发访问的影响
WCF后续之旅(13): 创建一个简单的WCF SOAP Message拦截、转发工具[上篇]
WCF后续之旅(13):创建一个简单的SOAP Message拦截、转发工具[下篇]
WCF后续之旅(14):TCP端口共享
WCF后续之旅(15): 逻辑地址和物理地址
WCF后续之旅(16): 消息是如何分发到Endpoint的--消息筛选(Message Filter)
WCF后续之旅(17):通过tcpTracer进行消息的路由
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。