程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WCF後續之旅(13) 創建一個簡單的WCF SOAP Message攔截、轉發工具

WCF後續之旅(13) 創建一個簡單的WCF SOAP Message攔截、轉發工具

編輯:關於.NET

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:

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved