閱讀659 返回首頁    go 阿裏雲 go 技術社區[雲棲]


WCF技術剖析之二十七: 如何將一個服務發布成WSDL[基於WS-MEX的實現](提供模擬程序)

通過《如何將一個服務發布成WSDL[編程篇]》的介紹我們知道了如何可以通過編程或者配置的方式將ServiceMetadataBehavior這樣一個服務形式應用到相應的服務上麵,從而實現基於HTTP-GET或者WS-MEX的元數據發布機製。那麼在WCF內部具體的實現原理又是怎樣的呢?相信很多人對此都心存好奇,本篇文章的內容將圍繞著這個主題展開。

一、 從WCF分發體係談起

如果讀者想對WCF內部的元數據發布機製的實現原理有一個全麵而深入的了解,必須對WCF服務端的分發體係有一個清晰的認識。在這裏我們先對該分發體係作一個概括性的介紹。WCF整個分發體係在進行服務寄宿(Hosting)時被構建,該體係的基本結構基本上可以通過圖1體現。

image 圖1 WCF服務端分發體係

當我們創建ServiceHost對象成功寄宿某個服務後,WCF會根據監聽地址的不同為該ServiceHost對象創建一到多個ChannelDispatcher對象。每個ChannelDispatcher都擁有各自的ChannelListener,這些ChannelListener綁定到相應的監聽地址監聽來自外界的請求。對於每一個ChannelListener對象,有個自己具有一到多個EndpointDispatcher對象與之匹配,每一個EndpointDispatcher對應著某個終結點。

而針對每一個EndpointDispatcher,在其初始化的時候會為之創建一個運行時,即DispatchRuntimeDispatchRuntime擁有一係列處理請求、激活對象和執行方法等操作的運行時對象,在這裏我們主要關注一個稱為InstanceContextProvider的對象。InstanceContextProvider用於提供封裝有相應服務實例的InstanceContext對象。

二、基於WS-MEX模式下的元數據發布是如何實現的?

現在我們再把話題移到元數據發布上來,先來談談基於WS-MEX協議的元數據發布方式。在這種元數據發布模式下,服務端通過MEX終結點發布元數據,客戶端創建相應的MEX終結點獲取元數據,這和一般意義上的服務調用並沒有本質的不同。你完全可以將元數據的獲取當成是一個某個服務,而該服務就是提供元數據。

如果我們通過編程或者配置的方式為某個服務添加了一個MEX終結點後,當服務被成功寄宿後,WCF會為之創建一個ChannelDispatcher。該ChannelDispatcher擁有一個用於監聽元數據請求的ChannelListener,監聽的地址及元數據發布的地址。基於該MEX終結點的EndpointDispatcher對象也會被創建,並與該ChannelDispatcher關聯在一起。在EndpointDispatcher初始化的時候,關聯DispatchRuntime也隨之被創建。與普通終結點關聯的DispatchRuntime一樣,基於MEX終結點的DispatchRuntime同樣擁有相同的運行時對象集合。但是,由於並沒有一個真正用於提供元數據的服務被寄宿,DispatchRuntime的InstanceContextProvider(默認是PerSessionInstanceContextProvider)是獲取不到包含有真正服務實例的InstanceContext對象的。

那麼,如果能夠定製DispatchRuntime的InstanceContextProvider,使它能夠正常提供一個InstanceContext,而該InstanceContext包含真正能夠提供元數據的服務實例,並且服務類類實現MEX終結點的契約接口IMetadataExchange,那麼一切問題都迎刃而解。實際上,ServiceMetadataBehavior內部就是這麼做的,而這個用於提供元數據的服務類型是定義在WCF內部的一個internal類型:WSMexImpl。

   1: internal class WSMexImpl : IMetadataExchange
   2: {
   3:     //其他成員  
   4:     public IAsyncResult BeginGet(Message request, AsyncCallback callback, object state);
   5:     public Message EndGet(IAsyncResult result);
   6:     private MetadataSet GatherMetadata(string dialect, string identifier);
   7:     public Message Get(Message request);
   8: }

ServiceMetadataBehaviorApplyDispatchBehavior方法被執行的時候,ServiceMetadataBehavior會創建WSMexImpl對象,據此創建InstanceContext對象,並將其作為MEX終結點DispatchRuntime的SingletonInstanceContext。然後創建一個SingletonInstanceContextProvider作為該DispatchRuntime的InstanceContextProvider。那麼,MEX終結點的DispatchRuntime就能使用其InstanceContextProvider提供封裝有WSMexImpl實例的InstanceContext了。

上訴的這些內容雖然不算負責,但是要求讀者對WCF的實例上下文機製有清晰的認識,對此不太熟悉的讀者,可以參數《WCF技術剖析(卷1)》第9章。為了加深讀者對基於WS-MEX元數據發布機製的理解,接下來我會作一個簡單的實例演示。

三、 實例演示:模擬ServiceMetadataBehavior實現基於WS-MEX元數據發布

接下來,我會完全基於ServiceMetadataBehavior的實現原理,即在上麵介紹的原理,創建一個自定義服務行為用於基於WS-MEX的元數據發布,Source Code從這裏下載。首先我們先來編寫一些輔助性質的代碼。由於在本例中我需要創建一些與DispatchRuntime相關的運行時對象,而且很多對象並沒有被公開出來(很多是internal類型,比如SingletonInstanceContextProvider),我需要通過反射的機製來創建它們。此外,我們需要為某些對象的一些私有或者內部屬性賦值,同樣需要利用反射,所以我寫了下麵兩個輔助方法:

   1: using System;
   2: using System.Globalization;
   3: using System.Reflection;
   4: namespace ServiceMetadataBehaviorSimulator
   5: {
   6:    public static class Utility
   7:     {
   8:        public static T CreateInstance<T>(string typeQname, Type[]parameterTypes, object[] parameters) where T:class
   9:        {
  10:            Type type = Type.GetType(typeQname);
  11:            BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
  12:            ConstructorInfo constructorInfo = type.GetConstructor(bindingFlags, Type.DefaultBinder, parameterTypes, null);
  13:            return Activator.CreateInstance(type, bindingFlags, Type.DefaultBinder, parameters, CultureInfo.InvariantCulture) as T;     
  14:        }
  15:  
  16:        public static void SetPropertyValue(object target, string propertyName, object propertyValue)
  17:        {
  18:            BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance;
  19:            PropertyInfo propertyInfo = target.GetType().GetProperty(propertyName, bindingFlags);
  20:            propertyInfo.SetValue(target, propertyValue, null);
  21:        }
  22:     }
  23: }

接下來,我仿照IMetadataExchange接口定義了如下一個接口:IMetadataProvisionService。為了簡化,我省略了異步模式定義的Get操作(BeginGet/EndGet)。Get操作的Action和ReplyAction同樣基於WS-Transfer規範定義。通過ServiceContractAttribute特性將契約的Name和ConfigurationName設定成IMetadataProvisionService

   1: using System.ServiceModel;
   2: using System.ServiceModel.Channels;
   3: namespace ServiceMetadataBehaviorSimulator
   4: {
   5:     [ServiceContract(ConfigurationName = "IMetadataProvisionService", Name = "IMetadataProvisionService", Namespace = "https://schemas.microsoft.com/2006/04/mex")]
   6:     public interface IMetadataProvisionService
   7:     {       
   8:         [OperationContract(Action = "https://schemas.xmlsoap.org/ws/2004/09/transfer/Get", ReplyAction = "https://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse")]
   9:         Message Get(Message request);
  10:     }
  11: }

由於Get操作返回的是封裝有元數據(以MetadataSet的形式)的消息對象,為此我定義了如下一個以消息契約(Message Contract)形式定義的類型:MetadataMessage。MetadataMessage通過MessageBodyMemberAttribute特性直接將類型為MetadataSet的屬性定義成消息主體成員,並按照WS-MEX規範設置該成員的名稱和命名空間。

   1: using System.ServiceModel;
   2: using System.ServiceModel.Description;
   3: namespace ServiceMetadataBehaviorSimulator
   4: {
   5:     [MessageContract(IsWrapped = false)]
   6:     public class MetadataMessage
   7:     {
   8:         public MetadataMessage(MetadataSet metadata)
   9:         {
  10:             this.Metadata = metadata;
  11:         }
  12:  
  13:         [MessageBodyMember(Name = "Metadata", Namespace = "https://schemas.xmlsoap.org/ws/2004/09/mex")]
  14:         public MetadataSet Metadata { get; set; }
  15:     }
  16: }

接下來我們來創建真正用於提供元數據的服務類:MetadataProvisionService。MetadataProvisionService實現了上麵定義的服務契約接口IMetadataProvisionService,具有一個MetadataSet類型的屬性成員Metadata。在Get方法中,通過Metadata屬性表述的MetadataSet創建MetadataMessage對象,並將其轉化成Message對象返回。最終返回的消息具有WS-Transfer規定的Action:https://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse

   1: using System;
   2: using System.ServiceModel.Channels;
   3: using System.ServiceModel.Description;
   4: namespace ServiceMetadataBehaviorSimulator
   5: {
   6:     public class MetadataProvisionService : IMetadataProvisionService, 
   7:     {
   8:         public MetadataSet Metadata
   9:         { get; private set; }
  10:  
  11:         public MetadataProvisionService(MetadataSet metadata)
  12:         {
  13:             if (null == metadata)
  14:             {
  15:                 throw new ArgumentNullException("metadata");
  16:             }
  17:             this.Metadata = metadata;
  18:         }        
  19:  
  20:         public Message Get(System.ServiceModel.Channels.Message request)
  21:         {
  22:             MetadataMessage message = new MetadataMessage(this.Metadata);
  23:             TypedMessageConverter converter = TypedMessageConverter.Create(typeof(MetadataMessage), "https://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse");
  24:             return converter.ToMessage(message, request.Version);
  25:         }
  26:     }
  27: }

最後我們就可以創建用於實現元數據發布的服務行為了,在這裏使用了與ServiceMetadataBehavior相同的名字,並將其定義成特性,那麼我們就可以直接通過特性的方式應用到服務類型上。所有的實現體現在ApplyDispatchBehavior方法中,該方法先後執行以下兩組操作:

  • 導出元數據:直接通過WsdlExporter將服務相關的所有終結點導出生成MetadataSet,需要注意的是,在進行終結點收集的時候,需要過濾到MEX終結點;元數據導出的所有操作實現在GetExportedMetadata方法中;
  • 定製MEX終結點的DispatchRuntimeServiceHostEndpointDispatcher列表中,篩選出基於MEX終結點的EndpointDispatcher,然後定製它們的DispatchRuntime:創建SingletonInstanceContextProvider作為IntanceContextProvider;根據導出的MetadataSet創建MetadataProvisionService對象,並將其封裝在InstanceContext中,而該InstanceContext直接設定為DispatchRuntime的SingletonInstanceContext。
   1: using System;
   2: using System.Collections.ObjectModel;
   3: using System.Reflection;
   4: using System.ServiceModel;
   5: using System.ServiceModel.Channels;
   6: using System.ServiceModel.Description;
   7: using System.ServiceModel.Dispatcher;
   8: using System.Xml;
   9: namespace ServiceMetadataBehaviorSimulator
  10: {
  11:     [AttributeUsage(AttributeTargets.Class)]
  12:     public class ServiceMetadataBehaviorAttribute:Attribute, IServiceBehavior
  13:     {
  14:         private const string MexContractName = "IMetadataProvisionService";
  15:         private const string MexContractNamespace = "https://schemas.microsoft.com/2006/04/mex";
  16:         private const string SingletonInstanceContextProviderType = "System.ServiceModel.Dispatcher.SingletonInstanceContextProvider,System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
  17:         public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters){}
  18:  
  19:         public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  20:         {
  21:             MetadataSet metadata = GetExportedMetadata(serviceDescription);
  22:             CustomizeMexEndpoints(serviceDescription, serviceHostBase, metadata);           
  23:         }
  24:  
  25:         private static MetadataSet GetExportedMetadata(ServiceDescription serviceDescription)
  26:         {
  27:             Collection<ServiceEndpoint> endpoints = new Collection<ServiceEndpoint>();
  28:             foreach (var endpoint in serviceDescription.Endpoints)
  29:             {
  30:                 if (endpoint.Contract.ContractType == typeof(IMetadataProvisionService))
  31:                 {
  32:                     continue;
  33:                 }
  34:                 ServiceEndpoint newEndpoint = new ServiceEndpoint(endpoint.Contract, endpoint.Binding, endpoint.Address);
  35:                 newEndpoint.Name = endpoint.Name;
  36:                 foreach (var behavior in endpoint.Behaviors)
  37:                 {
  38:                     newEndpoint.Behaviors.Add(behavior);
  39:                 }
  40:                 endpoints.Add(newEndpoint);
  41:             }
  42:             WsdlExporter exporter = new WsdlExporter();
  43:             XmlQualifiedName wsdlServiceQName = new XmlQualifiedName(serviceDescription.Name, serviceDescription.Namespace);
  44:             exporter.ExportEndpoints(endpoints, wsdlServiceQName);
  45:             MetadataSet metadata = exporter.GetGeneratedMetadata();
  46:             return metadata; 
  47:         }
  48:  
  49:         private static void CustomizeMexEndpoints(ServiceDescription description, ServiceHostBase host,MetadataSet metadata)
  50:         {
  51:             foreach(ChannelDispatcher channelDispatcher in host.ChannelDispatchers)
  52:             {
  53:                 foreach(EndpointDispatcher endpoint in channelDispatcher.Endpoints)
  54:                 {
  55:                     if (endpoint.ContractName == MexContractName && endpoint.ContractNamespace == MexContractNamespace)
  56:                     {
  57:                         DispatchRuntime dispatchRuntime = endpoint.DispatchRuntime;
  58:                         dispatchRuntime.InstanceContextProvider = Utility.CreateInstance<IInstanceContextProvider>(SingletonInstanceContextProviderType, new Type[] { typeof(DispatchRuntime) }, new object[] { dispatchRuntime });
  59:                         MetadataProvisionService serviceInstance = new MetadataProvisionService(metadata);
  60:                         dispatchRuntime.SingletonInstanceContext = new InstanceContext(host, serviceInstance);
  61:                     }
  62:                 }
  63:             } 
  64:         }
  65:  
  66:         public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }       
  67:     }
  68: }

以我們熟悉的計算服務的為例,我們將ServiceMetadataBehaviorAttribute直接應用CalculatorService上。下麵是CalculatorService的定義,之所以讓它實現我們定義的IMetadataProvisionService接口,是為了在進行服務寄宿是滿足服務類型比如實現終結點契約接口的約束。如果直接使用WCF提供IMetadataExchange,由於其內部進行了相應的處理,服務類型與MEX終結點契約接口無關時允許的。

   1: using System;
   2: using System.ServiceModel.Channels;
   3: using System.ServiceModel.Description;
   4: using Artech.Contracts;
   5: using ServiceMetadataBehaviorSimulator;
   6: namespace Artech.Services
   7: {
   8:     [ServiceMetadataBehavior()]
   9:     public class CalculatorService : ICalculator, IMetadataProvisionService
  10:     {
  11:         public double Add(double x, double y)
  12:         {
  13:             return x + y;
  14:         }
  15:  
  16:         public Message Get(Message request)
  17:         {
  18:             throw new NotImplementedException();
  19:         }
  20:     }
  21: }

那麼在進行服務寄宿的時候,我們就可以采用WCF如下的方式添加MEX終結點了。可以看到這與WCF本身支持的MEX終結點的配置一本上是一樣的,唯一不同的是這裏的契約是我們自定義IMetadataProvisionService,而不是IMetadataExchange。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>   
   3:     <system.serviceModel>
   4:         <services>
   5:             <service name="Artech.Services.CalculatorService">
   6:                 <endpoint address="https://127.0.0.1:3721/calculatorservice" binding="ws2007HttpBinding" contract="Artech.Contracts.ICalculator" />
   7:                 <endpoint address="https://127.0.0.1:9999/calculatorservice/mex"
   8:                     binding="mexHttpBinding" contract="IMetadataProvisionService" />
   9:             </service>
  10:         </services>
  11:     </system.serviceModel>
  12: </configuration>

在客戶端就可以采用與一般服務調用完全一樣的方式獲取服務的元數據了,下麵是客戶端的配置。注意,這裏配置的終結點並不是調用Calculatorservice的終結點,而是為了獲取元數據的MEX終結點。地址是服務端MEX終結點的地址,契約是IMetadataProvisionService,采用的綁定是標準的基於HTTP的MEX綁定

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:     <system.serviceModel>       
   4:         <client>
   5:             <endpoint address="https://127.0.0.1:9999/calculatorservice/mex"
   6:                 binding="mexHttpBinding" contract="IMetadataProvisionService"
   7:                 name="mex" />
   8:         </client>
   9:     </system.serviceModel>
  10: </configuration>

下麵是基於ChannelFactory<TChannel>創建服務代理的客戶端代碼,可以看到與一般的服務調用並無二致。獲取的元數據最終被寫入一個XML文件並被打開。

   1: using System.Diagnostics;
   2: using System.ServiceModel;
   3: using System.ServiceModel.Channels;
   4: using System.ServiceModel.Description;
   5: using System.Text;
   6: using System.Xml;
   7: using ServiceMetadataBehaviorSimulator;
   8: namespace Artech.Client
   9: {
  10:     class Program
  11:     {
  12:         static void Main(string[] args)
  13:         {
  14:             using (ChannelFactory<IMetadataProvisionService> channelFactory = new ChannelFactory<IMetadataProvisionService>("mex"))
  15:             {
  16:                 IMetadataProvisionService proxy = channelFactory.CreateChannel();
  17:                 Message request = Message.CreateMessage(MessageVersion.Default, "https://schemas.xmlsoap.org/ws/2004/09/transfer/Get");
  18:                 Message reply = proxy.Get(request);
  19:                 MetadataSet metadata = reply.GetBody<MetadataSet>();
  20:                 using (XmlWriter writer = new XmlTextWriter("metadata.xml", Encoding.UTF8))
  21:                 {
  22:                     metadata.WriteTo(writer);
  23:                 }
  24:                 Process.Start("metadata.xml");
  25:             }
  26:         }
  27:     }
  28: }

上麵的應用如果正常執行,包含所有元數據信息的XML文件將會通過IE(假設使用IE作為開啟XML文件的默認應用程序)開啟,圖2是運行後的截圖:

image 圖2 獲取的元數據在IE中的顯示

下一篇中我們將采用同樣的方式來模擬基於HTTP-GET的元數據發布時如何實現的。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 16:34:16

  上一篇:go  提升效率必備,9 篇論文幫你積累知識點 | PaperDaily #06
  下一篇:go  WCF技術剖析之二十七: 如何將一個服務發布成WSDL[基於HTTP-GET的實現](提供模擬程序)