• 【应用篇】WCF学习笔记(一):Host、Client、MetadataExchage


    虽然已经在多个项目中成功应用过WCF,但是感觉自己对WCF的知识只知道一些皮毛而已。上次学习WCF也是为了项目需要,囫囵吞枣。这不是我学习方法的态度。所以时至今日,又重新拾卷,再仔细的将WCF一些细节知识,边边角角自己回顾一番。

    Host

    三种Host的方式:IIS Host、WAS Host、Self-Host。

    IIS Host

    IIS这种非常简单,但只支持HTTP协议。不过,你可以借助IIS来管理服务的生命周期。在IIS上发布WCF Service是极其简单的,只需要写一个后缀名为svc的文件就ok了:

    <%@ ServiceHost Language=”C#” Debug=”false” CodeBehind=”~/App_Code/MyService.cs” Service=”MyService” %>

    还记得Web Service的asmx文件么?是不是如出一辙!

    Self-Host

    顾名思义,就是自托管了。开发人员完全控制,你可以将服务驻留在Console Application、WinForm、Windows Service。只需要保证服务先于客户端启动就Ok了。使用这种托管方式,开发人员可以完全的控制,可以使用任何协议,灵活性最大。

    WAS Host

    WAS的全称是Windows Activation Service,是个系统服务,跟随Vista发布的,是IIS 7的一部分,但是也可以单独安装和配置。要使用WAS,和IIS Host一样,也需要提供一个svc文件。但是,WAS不仅仅可以使用HTTP,可以使用WCF可以使用的任何协议。

    WAS提供了很多Self-Host没有的优点,比如应用程序池、回收、身份管理等。

    Endpoint

    WCF中最重要的概念莫过于Endpoint了,Endpoint就是服务的接口。一个Endpoint包括三个要素:Address、Binding、Contract。

    这三个方面实际上表达的是Where?How?What?的意思。

    Address就是我到哪里(Where)寻找这个服务?

    Binding就是我如何(How)与这个服务交互?

    Contract就是这个服务是什么(What)?

    每个Endpoint必须具有三个要素,缺一不可。

    ServiceHost

    Host架构

    hostarchitecture

    Service就驻留在ServiceHost实例中,每个ServiceHost实例只能托管一个服务。每个ServiceHost可以有多个Endpoint。一个进程里面可以有多个ServiceHost实例,同个宿主进程里不同的ServiceHost实例可以享用相同的BaseAddress。

    使用Visual Studio自动生成服务端

    Visual Studio的项目模板里已经为我们准备了WCF Service Application,使用Visual Studio创建的WCF Service Application项目默认是使用IIS托管的(WAS的托管方式与IIS的类似):
    addwcfserviceapplication

    生成后的工程(经过修改):
    wcfserviceapplication

    如果采用Selft-Host该怎么办呢?Visual Studio里还有一个WCF Service Library的项目模板,我们可以使用这个模板为Self-Host生成很多代码:
    addwcfservicelibrary
    添加后生成的工程(经修改):
    wcfservicelibrary

    但不管是WCF Service Application还是WCF Service Library,我觉得这种自动生成的方式都不太好。从上面几个图我们可以看出,这两种项目模板都将服务契约与服务的实现放在同一个项目中,最后编译出来服务契约与服务实现也在同一个程序集中,既然如此那为何又要将契约和服务分开?不是多次一举么?对于Best Practice来讲,我们应该永远都为每个服务创建一个接口,而将[ServiceContract]特性加在这些接口上,然后在另一个项目里编写服务的实现类,引用服务契约的项目,实现这些接口(契约)。所以无论从学习还是Best Practice来讲,我们都应该具有手动从头到尾编写服务契约、实现服务、服务托管的代码的能力:

    代码示例:

       1: //订单项
       2: [DataContract]
       3: public class OrderItem
       4: {
       5:     [DataMember]
       6:     public int Id{get;set;}
       7:     [DataMember]
       8:     public int ProductId{get;set;}
       9: }
      10: //订单
      11: [DataContract]
      12: public class Order
      13: {
      14:     [DataMember]
      15:     public int OrderId{get;set;}
      16:  
      17:     [DataMember]
      18:     public IList<OrderItem> OrderItems{get;set;}
      19: }
       1: //订单服务契约
       2: [ServiceContract]
       3: public interface IOrderService
       4: {
       5:     [OperationContract]
       6:     bool CreateOrder(Order order);
       7:  
       8:     [OperationContract]
       9:     bool DeleteOrder(int orderId);
      10:  
      11:     [OperationContract]
      12:     bool CancelOrder(int orderId);
      13: }
       1: //订单服务
       2: public class OrderService : IOrderService
       3: {
       4:     public void CreateOrder(Order order)
       5:     {
       6:         return true;
       7:     }
       8:     public bool DeleteOrder(int orderId)
       9:     {
      10:         return false;
      11:     }
      12:     public bool CancelOrder(int orderId)
      13:     {
      14:         return false;
      15:     }
      16: }

    服务托管(Self-Host)

       1: static void Main()
       2: {
       3:     //binding,how?
       4:     Binding tcpBinding = new NetTcpBinding();
       5:     ServiceHost host = new ServiceHost(typeof(OrderService),new Uri("net.tcp://localhost:8000/"));
       6:     host.AddServiceEndpoint(typeof(IOrderService),tcpBinding,"OrderService");
       7:     host.Open();
       8:     Console.ReadLine();
       9:     host.Close();
      10: }

    使用配置的方式:

       1: <?xml version="1.0" encoding="utf-8" ?>
       2: <configuration>
       3:     <system.serviceModel>
       4:         <services>
       5:             <!--注意,这里的name要与服务的类名是一致的-->
       6:             <service name="OrderService">
       7:                 <host>
       8:                     <baseAddresses>
       9:                         <add baseAddress="net.tcp://localhost:8000/" />
      10:                     </baseAddresses>
      11:                 </host>
      12:                 <endpoint contract="IOrderService" binding="netTcpBinding" address="OrderService" />
      13:             </service>
      14:     </system.serviceModel>
      15: </configuration>

    MetadataExchange(元数据交换)

    启用元数据交换有居多的好处,客户端可以使用SvcUtil工具自动的从服务元数据中生成客户端代理已经数据契约。

    WCF启用元数据交换有两种方式:

    1、使用HttpGet

    2、使用一个专门的Endpoint用来进行元数据交换

    HttpGet

    先来看实例,HttpGet:

       1: Uri httpBaseAddress = new Uri("http://locahost/");
       2: Uri tcpBaseAddress = new Uri("net.tcp://localhost:8000/");
       3: ServiceHost host = new ServiceHost(typeof(OrderService),httpBaseAddress,tcpBaseAddress);
       4: ServiceMetadataBehavior metadataBehavior;
       5: metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
       6: if(metadataBehavior == null)
       7: {
       8:     metadataBehavior = new ServiceMetadataBehavior();
       9:     //启用http-get方式
      10:     metadataBehavior.HttpGetEnabled = true;
      11:     host.Description.Behaviors.Add(metadataBehavior);
      12: }
      13: host.AddServiceEndpoint(typeof(IOrderService),new NetTcpBinding(),"OrderService");
      14: host.Open();
      15: Console.ReadLine();
      16: host.Close();

    既然是Http-Get的方式,顾名思义,肯定是使用http协议,所以必须有一个http的baseaddress。上面是使用代码的方式启用http-get,看看如何用配置打开http-get:

       1: <?xml version="1.0" encoding="utf-8" ?>
       2: <configuration>
       3:     <system.serviceModel>
       4:         <services>
       5:             <service name="OrderService" behaviorConfiguration="EnableHttpGetBehavior">
       6:                 <host>
       7:                     <baseAddresses>
       8:                         <add baseAddress="net.tcp://localhost:8000/" />
       9:                         <add baseAddress="http://localhost/" />
      10:                     </baseAddresses>
      11:                 </host>
      12:                 <endpoint contract="IOrderService" binding="netTcpBinding" address="OrderService" />
      13:             </service>
      14:             <behaviors>
      15:             <serviceBehaviors>
      16:               <behavior name="EnableHttpGetBehavior">
      17:                 <serviceMetadata httpGetEnabled="true" />
      18:                </behavior>
      19:             </serviceBehaviors>
      20:            </behaviors>
      21:     </system.serviceModel>
      22: </configuration>

    添加专用Endpoint

    编程添加

       1: Uri httpBaseAddress = new Uri("http://locahost/");
       2: Uri tcpBaseAddress = new Uri("net.tcp://localhost:8000/");
       3: ServiceHost host = new ServiceHost(typeof(OrderService),httpBaseAddress,tcpBaseAddress);
       4: //虽然不使用http-get的方式,ServiceMetadataBehavior还是要加的,而且是先加这个Behavior,然后再添加
       5: //专门用于元数据交换的Endpoint,否则会抛出异常
       6: ServiceMetadataBehavior metadataBehavior;
       7: metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
       8: if(metadataBehavior == null)
       9: {
      10:     metadataBehavior = new ServiceMetadataBehavior();
      11:     //使用这种方式,HttpGetEnabled是否为true都无所谓,HttpGetEnabled默认值是false
      12:     host.Description.Behaviors.Add(metadataBehavior);
      13: }
      14: host.AddServiceEndpoint(typeof(IOrderService),new NetTcpBinding(),"OrderService");
      15: //注意这里
      16: BindingElement bindingElement = new TcpTransportBindingElement();
      17: Binding customBinding = new CustomBinding(bindingElement);
      18: //添加一个专门用于元数据交换的Endpoint
      19: host.AddServiceEndpoint(typeof(IMetadataExchange),customBinding,"MEX");
      20: host.Open();
      21: Console.ReadLine();
      22: host.Close();

    配置的方式添加

       1: <?xml version="1.0" encoding="utf-8" ?>
       2: <configuration>
       3:     <system.serviceModel>
       4:         <services>
       5:             <service name="OrderService" behaviorConfiguration="EnableHttpGetBehavior">
       6:                 <host>
       7:                     <baseAddresses>
       8:                         <add baseAddress="net.tcp://localhost:8000/" />
       9:                         <add baseAddress="http://localhost/" />
      10:                         </baseAddresses>
      11:                 </host>
      12:                 <endpoint contract="IOrderService" binding="netTcpBinding" address="OrderService" />
      13:                 <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="MEX" />
      14:             </service>
      15:             <behaviors>
      16:             <serviceBehaviors>
      17:               <behavior name="EnableHttpGetBehavior">
      18:                 <serviceMetadata />
      19:                </behavior>
      20:             </serviceBehaviors>
      21:            </behaviors>
      22:     </system.serviceModel>
      23: </configuration>

    从上面的代码我们基本上就知道了如何启用元数据交换了。使用http-get的方式简单,只需要一条设置就ok了,但是只能使用http或https,使用专用的endpoint则可以使用所有的协议。很多内容写在注释里了,仔细阅读。

    服务已经Host了,Endpoint也已添加了,现在就要看看如何编写Client端。

    Client端编程

    使用Visual Studio自动生成客户端

    客户端是通过一个代理与服务端交互的,我们可以使用Visual Studio的Add Service Reference的功能添加远程服务,这样Visual Studio就会自动的帮我们生成客户端服务的代理。要使用Add Service Reference首先你得服务端必须启用了元数据交换。如果你是使用Visual Studio在同一个解决方案下创建的WCF Service Application或WCF Service Library,在Add Service Reference窗口中,还可以使用Discovery按钮自动的找到本解决方案下的所有服务:
    addservicereference

    添加服务引用以后:
    updateservicereference

    在这个窗口中,点击“Advanced”还可以对生成的代理做一些设置,在Namespace里可以设置生成服务的命名空间。这样做非常简便、高效,而且,当服务端修改了什么,我们只需要在客户端项目的Service Reference文件件下选择对应的服务,然后点击右键中的“Update Service Reference”,客户端代理就可以自动更新成新版本了。具有这样的优势很有诱惑力,但我们对Visual Studio生成了什么还是一无所知,不能自己完全的控制。所以你可以决定自己编写客户端代理。

    Client Proxy

       1: public class OrderServiceProxy : ClientBase<IOrderService>,IOrderService
       2: {
       3:     public OrderServiceProxy(){}
       4:     public OrderServiceProxy(string endpointName):base(endpointName){}
       5:     public OrderServiceProxy(Binding binding,EndpointAddress remoteAddress):base(binding,remoteAddress){}
       6:  
       7:     public bool DeleteOrder(int orderId)
       8:     {
       9:         return this.Channel.DeleteOrder(orderId);
      10:     }
      11:     public bool CancelOrder(int orderId)
      12:     {
      13:         return this.Channel.CancelOrder(orderId);
      14:     }
      15:     public bool CreateOrder(Order order)
      16:     {
      17:         return this.Channel.CreateOrder(order);
      18:     }
      19: }

    这样一个本地的代理就创建好了,如何去使用这个代理呢?也有两种方式,第一种,我们可以编写代码创建一个本地代理,第二种,我们可以将配置保存在配置文件中。

    使用代理

    编程方式

       1: static void Main()
       2: {
       3:     Binding tcpBinding = new NetTcpBinding();
       4:     EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000/OrderService");
       5:     OrderServiceProxy proxy = new OrderServiceProxy(tcpBinding,address);
       6:     
       7:     //打开到远程服务的连接
       8:     proxy.Open();
       9:     //调用远程服务,是不是像调用本地方法一样
      10:     proxy.CancelOrder(5);
      11:     //关闭
      12:     proxy.Close();
      13:     
      14: }

    编程的方式的优点是能得到编译器的检查,但是如果想修改一下,比如日后改为http协议访问就得修改源代码。我们还可以使用配置的方式,在上面代理的代码中,我们发现代理还有一个构造器接受一个“endpointName”的参数,这个参数就是指配置文件中Endpoint的名称:

       1: <?xml version="1.0" encoding="utf-8" ?>
       2: configuration>
       3:    <system.serviceModel>
       4:          <client>
       5:            <endpoint address="net.tcp://localhost:8000/OrderService"
       6:                binding="netTcpBinding"
       7:                contract="IOrderService" name="OrderService">
       8:            </endpoint>
       9:        </client>
      10:    </system.serviceModel>
      11: </configuration>

    然后可以这样使用代理:

       1: OrderServiceProxy proxy = new OrderServiceProxy("OrderService");
       2: proxy.Open();
       3: proxy.CancelOrder(5);
       4: proxy.Close();

    这种方式虽然不能获得编译时的检查,配置文件如果写错了,只有等到运行时才可以发现,但是将配置保存在程序员可以带来非常大的灵活性。

    使用ChannelFactory创建代理

    实际上,还有一种方式:

       1: Binding binding = new NetTcpBinding();
       2: EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000/OrderService")
       3: IOrderService proxy = ChannelFactory<IOrderService>.CreateChannel(binding,address );
       4: //代理使用后一定要关闭,看看下面的方式
       5: using(proxy as IDisposable)
       6: {
       7:   proxy.Login();
       8: }
       9: //或者这种方式也可以
      10: ICommunicationObject channel = proxy as ICommunicationObject;
      11: channel.Close();

    元数据除了协助Visual Studio发现服务,自动生成代码(客户端代理,数据契约)还有什么用?我们可以使用编程的方式访问元数据么?答案是肯定的,下一节我们看看如果使用编程方式访问元数据。

    元数据导入

    我们可以编写代码,判断一个服务是否提供我们期望的Contract,这将怎么实现呢?比如我们要做一个小程序,遍历出某个指定address里暴露的所有Contract。WCF为我们提供了MetadataExchangeClient类。

       1: //MetadataExchangeClient的构造器有几个重载
       2: MetadataExchangeClient mexClient = new MetadataExchangeClient(new Uri("net.tcp://localhost:8000/MEX"),
       3:                         MetadataExchangeClientMode.MetadataExchange);
       4: //GetMetadata方法也有好几个重载
       5: MetadataSet metadataSet = mexClient.GetMetadata();
       6: WsdlImporter importer = new WsdlImporter(metadataSet);
       7: ServiceEndpointCollection endpoints = importer.ImportAllEndpoints();
       8: foreach(ServiceEndpoint endpoint in endpoints)
       9: {
      10:     ContractDescription contract = endpoint.Contract;
      11:     Console.WriteLine("Namespace:{0},Name:{1}",contract.Namespace,contract.Name);
      12: }

    WCF架构

    wcfarchitecture

    这个架构图对于日后的WCF扩展非常重要。

    本文为学习WCF笔记,文章大部分内容“抄袭”自《Programming WCF Services》。

  • 相关阅读:
    Apache虚拟主机配置(多个域名访问多个目录)(转)
    What I Learned as a Junior Developer Writing Tests for Legacy Code(转载)
    java.text包
    高性能前端框架React详解
    vue.js快速搭建图书管理平台
    vue.js用法和特性详解
    最接近原生APP体验的高性能前端框架——MUI
    用JS制作一个信息管理平台完整版
    JQuery实用技巧--学会你也是大神(1)——插件的制作技巧
    用JS制作一个信息管理平台(1)
  • 原文地址:https://www.cnblogs.com/yuyijq/p/1589277.html
Copyright © 2020-2023  润新知