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
using System.ServiceModel;
namespace Artech.MessageInterceptor.Contracts
{
[ServiceContract]
public interface ICalculate
{
[OperationContract]
double Add(double x, double y);
}
}
2、Service:Artech.MessageInterceptor.Services.CalculateService
using Artech.MessageInterceptor.Contracts;
namespace Artech.MessageInterceptor.Services
{
public class CalculateService : ICalculate
{
#region ICalculate Members
public double Add(double x, double y)
{
return x + y;
}
#endregion
}
}
3、Hosting:Artech.MessageInterceptor.Hosting.Program
using System;
using System.ServiceModel;
using Artech.MessageInterceptor.Services;
namespace Artech.MessageInterceptor.Hosting
{
class Program
{
static void Main(string[] args)
{
using (ServiceHost host = new ServiceHost(typeof(CalculateService)))
{
host.Opened += delegate
{
Console.WriteLine("The calculate service has been started up!");
};
host.Open();
Console.Read();
}
}
}
}
Configuration
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<customBinding>
<binding name="MyCustomeBinding">
<textMessageEncoding />
<httpTransport />
</binding>
</customBinding>
</bindings>
<services>
<service name="Artech.MessageInterceptor.Services.CalculateService">
<endpoint binding="customBinding" bindingConfiguration="MyCustomeBinding"
contract="Artech.MessageInterceptor.Contracts.ICalculate"
address="http://127.0.0.1:9999/calculateservice"/>
</service>
</services>
</system.serviceModel>
</configuration>
在host我們的calculateservice的時候,我們使用了拋棄了系統定義的binding,而采用一個custom binding。是因為custom binding基於更好的可擴展能力,以利於我們後續的介紹。為了簡單起見,我們僅僅需要bing為了提供最基本的功能:傳輸與編碼,為此我僅僅添加了兩個binding element:textMessageEncoding 和httpTransport。我們將在後面部分應用其他的功能,比如WS-Security.
4、Client:Artech.MessageInterceptor.Clients.Program
using System;
using System.ServiceModel;
using Artech.MessageInterceptor.Contracts;
namespace Artech.MessageInterceptor.Clients
{
class Program
{
static void Main(string[] args)
{
using (ChannelFactory<ICalculate> channelFactory = new ChannelFactory<ICalculate>("calculateservice"))
{
ICalculate calculator = channelFactory.CreateChannel();
using (calculator as IDisposable)
{
Console.WriteLine("x + y = {2} where x = {0} ans y = {1}", 1, 2, calculator.Add(1, 2));
}
}
Console.Read();
}
}
}
Configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<customBinding>
<binding name="MyCustomBinding">
<textMessageEncoding />
<httpTransport />
</binding>
</customBinding>
</bindings>
<client>
<endpoint name="calculateservice" address="http://127.0.0.1:9999/calculateservice" binding="customBinding" bindingConfiguration="MyCustomBinding"
contract="Artech.MessageInterceptor.Contracts.ICalculate" />
</client>
</system.serviceModel>
</configuration>
二、創建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中):
using System.ServiceModel.Channels;
using System.ServiceModel;
namespace Artech.MessageInterceptor.Contracts
{
[ServiceContract]
public interface IIntercept
{
[OperationContract(Action ="*", ReplyAction="*")]
Message Intercept(Message request);
}
}
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。
using System;
using Artech.MessageInterceptor.Contracts;
using System.ServiceModel.Channels;
using System.Threading;
using System.ServiceModel;
using System.ServiceModel.Description;
namespace Artech.MessageInterceptor.Services
{
[ServiceBehavior(UseSynchronizationContext = false, AddressFilterMode = AddressFilterMode.Any)]
public class InterceptService : IIntercept
{
private const string CalculateServiceEndpoint = "calculateService";
public static SynchronizationContext SynchronizationContext
{ get; set; }
public static System.Windows.Forms.TextBox MessageDisplayPanel
{ get; set; }
#region IIntercept Members
public Message Intercept(Message request)
{
using (ChannelFactory<IIntercept> channelFactory = new ChannelFactory<IIntercept>(CalculateServiceEndpoint))
{
IIntercept interceptor = channelFactory.CreateChannel();
using (interceptor as IDisposable)
{
MessageBuffer requstBuffer = request.CreateBufferedCopy(int.MaxValue);
Message response = interceptor.Intercept(requstBuffer.CreateMessage());
MessageBuffer responseBuffer = response.CreateBufferedCopy(int.MaxValue);
SynchronizationContext.Post(delegate
{
MessageDisplayPanel.Text += string.Format("Request:{0}{1}{0}", Environment.NewLine, request);
MessageDisplayPanel.Text += string.Format("Response:{0}{1}{0}", Environment.NewLine, response);
}, null);
return responseBuffer.CreateMessage();
}
}
}
#endregion
}
}
對於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。
using System;
using System.Windows.Forms;
using System.ServiceModel;
using Artech.MessageInterceptor.Services;
using System.Threading;
namespace Artech.MessageInterceptor.Interceptor
{
public partial class MessageInterceptor : Form
{
private ServiceHost _serviceHost;
public MessageInterceptor()
{
InitializeComponent();
}
private void MessageInterceptor_Load(object sender, EventArgs e)
{
this._serviceHost = new ServiceHost(typeof(InterceptService));
this._serviceHost.Opened += delegate
{
this.Text += ":Started";
};
InterceptService.SynchronizationContext = SynchronizationContext.Current;
InterceptService.MessageDisplayPanel = this.textBoxMessage;
this._serviceHost.Open();
}
}
}
下面是configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<customBinding>
<binding name="MyCustomBinding">
<textMessageEncoding />
<httpTransport manualAddressing="true" />
</binding>
</customBinding>
</bindings>
<client>
<endpoint address="http://127.0.0.1:9999/calculateservice" binding="customBinding"
bindingConfiguration="MyCustomBinding" contract="Artech.MessageInterceptor.Contracts.IIntercept"
name="calculateService" />
</client>
<services>
<service name="Artech.MessageInterceptor.Services.InterceptService">
<endpoint binding="customBinding" bindingConfiguration="MyCustomBinding"
contract="Artech.MessageInterceptor.Contracts.IIntercept"
address="http://127.0.0.1:8888/Interceptservice"/>
</service>
</services>
</system.serviceModel>
</configuration>
這裡需要注意的client的配置,可能有人會有這樣的疑惑:Address是CalculateService的地址,但是Contract確是InterceptService的Contract,這不是不匹配嗎?實際上由於IIntercept中Intercept方式的參數和返回值都是Message,所以他們代表一切操作。
三、應用InteceptService
現在我們將我們創建InteceptService應用到我們CalculateService中。我們在上面已經提到過,我們現在是方案時要client自動將message發送到InteceptService。在WCF中有一個特殊的EndpointBehavior。(System.ServiceModel.Description.ClientViaBehavior),來實現這樣的功能:Message真正發送的地址不同是service真正的地址。基本的原理如下圖所示:
我們現在只需要改變client端的配置即可:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="ClientViaBehavior">
<clientVia viaUri="http://127.0.0.1:8888/Interceptservice" />
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
<customBinding>
<binding name="MyCustomBinding">
<textMessageEncoding />
<httpTransport />
</binding>
</customBinding>
</bindings>
<client>
<endpoint name="calculateservice" address="http://127.0.0.1:9999/calculateservice" behaviorConfiguration="ClientViaBehavior"
binding="customBinding" bindingConfiguration="MyCustomBinding"
contract="Artech.MessageInterceptor.Contracts.ICalculate" />
</client>
</system.serviceModel>
</configuration>
當我們運行我們的程序(先啟動兩個host程序,然後是client),Interceptor Windows Forms Appliction的窗體上將會看到被攔截的request message和response message:
本文配套源碼