天天看點

Wcf通訊基礎架構方案(二)——集中配置

從這次開始在幾個方面簡單闡述一下實作,集中配置是這個架構很大的一個目的,首先在資料庫中會有這麼一些表:

其實可以看到這些表的結構,應該是和<system.serviceModel>配置節點中的層次有對應的

1) Service表描述的是服務,主要儲存服務行為以及服務的配置。在這裡,ServiceConfig是架構内用到的配置,比如各種日志是否要記錄等等。服務對應到服務的叢集,叢集公開一個位址,用戶端通路這個位址,也就是一個負載均衡的虛拟IP位址。

2) 一個Service可以有多個ServiceEndpoint,ServiceEndpoint中定義了契約的版本,類型和行為,以及涉及到位址的端口、端點名字等。用戶端和服務端部署的服務契約版本号不一定是一緻的,在選擇端點的時候優先選擇比對的版本。

3) 一個ServiceEndpoint對應一個Binding,一個Binding也可以對應多個ServiceEndpoint。Binding表中記錄了綁定類型、優先級、以及協定字首和綁定的Xml配置。在選擇服務端點的時候會優先選擇優先級别比較高的綁定。

4) ClientAccess表主要用于限制哪些用戶端機器可以通路哪些服務叢集。

5) ClientEndpoint表主要用于設定ClientEndpoint的行為Xml(和ServiceEndpoint行為Xml不能保持一緻,對于綁定用戶端和服務端是公用的)。

上述這些邏輯可以展現在配置服務的兩個方法實作中:

public WcfService GetWcfService(string serviceType, string serviceContractVersion, string machineName)
        {
            using (WcfConfigDataContext data = new WcfConfigDataContext())
            {
                var wcfService = (from service in data.Services
                                  where service.ServiceType == serviceType && (service.ServerMachineName == "*" || service.ServerMachineName == machineName)
                                  select new WcfService
                                  {
                                      ServiceType = serviceType,
                                      ServiceBehaviorXml = service.ServiceBehaviorXml.ToString(),
                                      Endpoints = (from ep in data.ServiceEndpoints
                                                   where ep.ServiceType == serviceType && ep.ServiceContractVersion == serviceContractVersion
                                                   select new WcfServiceEndpoint
                                                   {
                                                       EndpointBehaviorXml = ep.ServiceEndpointBehaviorXml.ToString(),
                                                       EndpointBindingName = ep.ServiceEndpointBindingName,
                                                       EndpointName = ep.ServiceEndpointName,
                                                       EndpointPort = ep.ServiceEndpointPort,
                                                       ServiceContractType = ep.ServiceContractType,
                                                       EndpointBindingType = ep.Binding.BindingType,
                                                       EndpointBindingXml = ep.Binding.BindingXml.ToString(),
                                                       EndpointProtocol = ep.Binding.BindingProtocol
                                                   }).ToArray()
                                  }).SingleOrDefault();


                return wcfService;
            }
        }

        public WcfClientEndpoint GetWcfClientEndpoint(string serviceContractType, string serviceContractVersion, string machineName)
        {
            using (WcfConfigDataContext data = new WcfConfigDataContext())
            {
                var wcfClientEndpoint = (from ep in (data.ServiceEndpoints
                                         .Where(s => s.ServiceContractType == serviceContractType).ToList())
                                         where
                                             (ep.ServiceContractVersion == "" || float.Parse(ep.ServiceContractVersion) >= float.Parse(serviceContractVersion))
                                         orderby ep.ServiceContractVersion ascending, ep.Binding.BindingPriority descending
                                         select new WcfClientEndpoint
                                         {
                                             EndpointName = ep.ServiceEndpointName,
                                             EndpointPort = ep.ServiceEndpointPort,
                                             ServiceContractType = ep.ServiceContractType,
                                             EndpointBindingType = ep.Binding.BindingType,
                                             EndpointBindingXml = ep.Binding.BindingXml.ToString(),
                                             EndpointProtocol = ep.Binding.BindingProtocol,
                                             EndpointAddress = ep.Service.ServerFarm.ServerFarmAddress,
                                             ServerFarmName = ep.Service.ServerFarmName,
                                         }).FirstOrDefault();

                var accessableFarmNames = data.ClientAccesses.Where(acc => acc.ClientMachineName == "*" || acc.ClientMachineName == machineName).Select(a => a.AccessServerFarmName).ToList();
                if (!accessableFarmNames.Contains("*") && !accessableFarmNames.Contains(wcfClientEndpoint.ServerFarmName)) return null;

                var query =
                    (from ce in data.ClientEndpoints
                     where ce.ServiceContractType == wcfClientEndpoint.ServiceContractType
                     select ce.ClientEndpointBehaviorXml).FirstOrDefault();

                wcfClientEndpoint.EndpointBehaviorXml = query != null ? query.ToString() : "";

                return wcfClientEndpoint;
            }
        }      

有了配置,那麼用戶端和服務端怎麼應用上這些ABC呢?先來看看服務端:

public class WcfServiceHostFactory : ServiceHostFactory
    {
        protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
        {
            return CreateServiceHost(serviceType);
        }

        public static ServiceHost CreateServiceHost<T>()
        {
            return CreateServiceHost(typeof(T));
        }

        public static ServiceHost CreateServiceHost(Type serviceType)
        {
            string typeName = serviceType.FullName;
            var serviceHost = new ServiceHost(serviceType);
            if (!typeof(IWcfConfigService).IsAssignableFrom(serviceType) && !typeof(IWcfLogService).IsAssignableFrom(serviceType))
            {
                if (serviceHost.Description.Behaviors.Find<ServiceErrorBehavior>() == null)
                    serviceHost.Description.Behaviors.Add(new ServiceErrorBehavior());
                if (serviceHost.Description.Behaviors.Find<ActionInterceptBehavior>() == null)
                    serviceHost.Description.Behaviors.Add(new ActionInterceptBehavior());
                if (serviceHost.Description.Behaviors.Find<UnityServiceBehavior>() == null)
                    serviceHost.Description.Behaviors.Add(new UnityServiceBehavior());

                serviceHost.Description.Endpoints.Clear();
                var wcfService = GetWcfServiceConfiguration(serviceType);
                if (wcfService == null)
                    throw new Exception("Can not locate any Wcf service, please check metadata config!");

                var bindingCache = new Dictionary<string, Binding>();

                foreach (var ep in wcfService.Endpoints)
                {
                    string address = ConfigHelper.CreateAddress(
                        ep.EndpointProtocol,
                        Environment.MachineName,
                        ep.EndpointPort,
                        ep.EndpointName);

                    Binding binding;
                    if (!bindingCache.TryGetValue(address, out binding))
                    {
                        binding = ConfigHelper.CreateBinding(ep.EndpointBindingType, ep.EndpointBindingXml);
                        bindingCache[address] = binding;
                    }

                    serviceHost.AddServiceEndpoint(ep.ServiceContractType, binding, address);

                    if (!string.IsNullOrEmpty(ep.EndpointBehaviorXml))
                    {
                        AddEndPointBehavior(serviceHost.Description.Endpoints.Last(), ep.EndpointBehaviorXml);
                    }
                }

                foreach (var ep in serviceHost.Description.Endpoints)
                {
                    ep.Behaviors.Add(new MessageInspectorEndpointBehavior());
                }

                if (!string.IsNullOrEmpty(wcfService.ServiceBehaviorXml))
                    AddServiceBehavior(serviceHost, wcfService.ServiceBehaviorXml);

                WcfLogManager.InitLog(serviceType, LogType.Service);

                serviceHost.Opened += (sender, o) =>
                {
                    if (WcfLogManager.Current(serviceType).StartInfo.Server.Enabled)
                    {
                        var log = WcfLogProvider.GetServerStartInfo(serviceType.FullName, "WcfServiceHostFactory.CreateServiceHost", wcfService);
                        WcfServiceLocator.GetLogService().LogWithoutException(log);
                    }

                };
            }

            return serviceHost;
        }

        private static void AddEndPointBehavior(ServiceEndpoint serviceEndpoint, string behavior)
        {
            var doc = new XmlDocument();
            doc.LoadXml(behavior);
            var endpointBehaviorElement = new EndpointBehaviorElement();
            ConfigHelper.Deserialize(doc.OuterXml, endpointBehaviorElement);
            foreach (var item in endpointBehaviorElement)
            {
                ConfigHelper.SetBehavior(serviceEndpoint.Behaviors, item);
            }
        }

        private static void AddServiceBehavior(ServiceHost serviceHost, string behavior)
        {
            var doc = new XmlDocument();
            doc.LoadXml(behavior);
            var serviceBehaviorElement = new ServiceBehaviorElement();
            ConfigHelper.Deserialize(doc.OuterXml, serviceBehaviorElement);
            foreach (var item in serviceBehaviorElement)
            {
                ConfigHelper.SetBehavior(serviceHost.Description.Behaviors, item);
            }
        }

        private static WcfService GetWcfServiceConfiguration(Type serviceType)
        {
            var version = serviceType.GetInterfaces().First().Assembly.GetName().Version;
            try
            {
                using (var scf = WcfServiceClientFactory.CreateServiceClient<IWcfConfigService>())
                {
                    return scf.Channel.GetWcfService(serviceType.FullName, version.Major + "." + version.Minor, Environment.MachineName);
                }
            }
            catch (Exception ex)
            {
                LocalLogService.Log(ex.ToString());
                throw new Exception("Can not get service metadata, error message is " + ex.Message);
            }
        }
    }
}      

其實沒什麼複雜的,無非是應用配置,但是這裡注意到幾點:

1) 在我們自己的WcfServiceHostFactory裡面,我們除了從配置服務讀取配置,并且應用之外,還加入了架構本身需要的一些擴充的ServiceBehavior。如果以後有其它需要擴充的,用戶端和服務端也隻需要更新一下架構的dll即可。

2) 此外,可以看到在服務啟動的時候,根據開關,加上了StartInfo的日志,有關日志部分更詳細的東西會在以後介紹。這也是架構的另外一個好處,内置大量的橫切關注點的考慮。

再來看看用戶端的代碼,其實和服務端差不多:

private static ChannelFactory<T> CreateChannelFactory<T>(WcfClientEndpoint endpoint)
        {
            if (endpoint == null)
                throw new Exception("Can not locate any endpoint, please check metadata config!");

            var binding = ConfigHelper.CreateBinding(endpoint.EndpointBindingType, endpoint.EndpointBindingXml);
            if (binding is NetNamedPipeBinding)
                endpoint.EndpointAddress = "localhost";
            var address = ConfigHelper.CreateAddress(endpoint.EndpointProtocol, endpoint.EndpointAddress, endpoint.EndpointPort, endpoint.EndpointName);
            var factory = new ChannelFactory<T>(binding, address);
            factory.Endpoint.Behaviors.Add(new MessageInspectorEndpointBehavior());
            if (!string.IsNullOrEmpty(endpoint.EndpointBehaviorXml))
                AddEndpointBehavior(factory, endpoint.EndpointBehaviorXml);
            return factory;
        }

        private static void AddEndpointBehavior(ChannelFactory factory, string behaviorXml)
        {
            var doc = new XmlDocument();
            doc.LoadXml(behaviorXml);
            var endpointBehaviorElement = new EndpointBehaviorElement();
            ConfigHelper.Deserialize(doc.OuterXml, endpointBehaviorElement);
            foreach (var item in endpointBehaviorElement)
            {
                ConfigHelper.SetBehavior(factory.Endpoint.Behaviors, item);
            }
        }

        private static WcfClientEndpoint GetWcfClientEndpointConfiguration(Type serviceContractType)
        {
            var version = serviceContractType.Assembly.GetName().Version;
            var versionstring = version.Major + "." + version.Minor;
            try
            {
                using (var scf = WcfServiceClientFactory.CreateServiceClient<IWcfConfigService>())
                {
                    return scf.Channel.GetWcfClientEndpoint(serviceContractType.FullName, versionstring, WcfLogProvider.MachineName);
                }
            }
            catch (Exception ex)
            {
                LocalLogService.Log(ex.ToString());
                throw new Exception("Can not get client metadata, error message is " + ex.Message);
            }
        }

        internal static WcfChannelWrapper<T> CreateServiceClient<T>() where T : class
        {
            string typeName = typeof(T).FullName;
            ChannelFactory cf;

            if (!channelFactoryCache.TryGetValue(typeName, out cf))
            {
                lock (cacheLocker)
                {
                    if (!channelFactoryCache.TryGetValue(typeName, out cf))
                    {
                        if (typeof(T) == typeof(IWcfConfigService))
                        {
                            var configServiceAddress = ConfigurationManager.AppSettings[WCFCONFIGSERVICE_ADDRESS_KEY];
                            if (string.IsNullOrEmpty(configServiceAddress))
                                throw new Exception("Please set " + WCFCONFIGSERVICE_ADDRESS_KEY + "appSettings in your config!");
                            var binding = new NetTcpBinding();
                            binding.Security.Mode = SecurityMode.None;
                            var address = string.Format("net.tcp://{0}/WcfConfigService", configServiceAddress);
                            cf = new ChannelFactory<IWcfConfigService>(binding, address);
                        }
                        else if (typeof(T) == typeof(IWcfLogService))
                        {
                            var logServiceAddress = ConfigurationManager.AppSettings[WCFLOGSERVICE_ADDRESS_KEY];
                            if (string.IsNullOrEmpty(logServiceAddress))
                                throw new Exception("Please set " + WCFLOGSERVICE_ADDRESS_KEY + "appSettings in your config!");
                            var elements = new List<BindingElement>();
                            elements.Add(new BinaryMessageEncodingBindingElement());
                            elements.Add(new TcpTransportBindingElement());
                            var binding = new CustomBinding(elements);
                            var address = string.Format("net.tcp://{0}/WcfLogService", logServiceAddress);
                            cf = new ChannelFactory<IWcfLogService>(binding, address);
                        }
                        else
                        {
                            if (redisSubThread == null)
                            {
                                CreateRedisSubThread();
                            }
                            var endpoint = GetWcfClientEndpointConfiguration(typeof(T));
                         
                            cf = CreateChannelFactory<T>(endpoint);

                            WcfLogManager.InitLog<T>(LogType.Client);

                            if (WcfLogManager.Current<T>().StartInfo.Client.Enabled)
                            {
                                var log = WcfLogProvider.GetClientStartInfo(typeof(T).FullName, "WcfServiceClientFactory.CreateServiceClient", endpoint);
                                WcfServiceLocator.GetLogService().LogWithoutException(log);
                            }
                        }

                        channelFactoryCache[typeName] = cf;
                    }
                }
            }

            return new WcfChannelWrapper<T>((cf as ChannelFactory<T>).CreateChannel());
        }      

在這裡要解釋幾點:

1) 用戶端會根據契約的類型來緩存信道工廠,每一次使用了信道之後都會正确關閉,這個是通過WcfChannelWrapper保證的。更多有關用戶端的内容會在下次介紹,本次隻是介紹配置集中。

2) 對于日志服務和配置服務的配置,沒有定義在資料庫中,而是寫死的,隻有位址是可以配置的配置檔案中的。日志服務和配置服務不但是業務的用戶端需要使用,業務的服務端也是需要使用的,是以不管是用戶端還是服務端都需要配置兩個基礎服務的位址。雖然是寫死在配置檔案中的,但是一般生産環境會配置一個虛拟IP位址,把這2個服務做成負載均衡的服務,位址一般不會變動。

3) 和服務端一樣,也加上了架構本身需要的一些擴充,比如MessageInspectorEndpointBehavior。

最後,配置集中的好處:

1) 可以靈活修改各種服務的行為、端點的行為,統一管理位址。修改之後,可以通過釋出訂閱機制通知到用戶端和服務端。

2) 可以做一些版本控制的路由、權限控制、綁定優先級控制等等。

3) 可以基于這些中繼資料做一些監控等。

作者:

lovecindywang

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。