[WCF REST] WebServiceHost有何特别之处?

WCF为REST服务的寄宿提供了一个新的ServiceHost,即WebServiceHost。WebServiceHost是ServiceHost的子类,而WebServiceHostFactory是对应的ServiceHostFactory,在基于IIS/WAS寄宿中被使用。由于对REST服务绝大部分功能的支持都是通过WebHttpBehavior这么一个终结点行为实现的,所以WebServiceHost的核心功能就是将该终结点行为应用到寄宿服务的所有终结点。除此之外,WebServiceHost还具有一些额外的功能,这些功能都是通过重写OnOpening方法实现的。

一、ServiceDebugBehavior与ServiceMetadataBehavior

由于WebHttpBehavior提供了帮助页面,所以当我们通过WebServiceHost对REST服务进行寄宿的时候会屏蔽掉通过ServiceDebugBehavior服务行为提供的帮助页面。由于通过服务行为ServiceMetadataBehavior实现的元数据发布机制是基于SOAP的,如果我们在寄宿服务上应用了ServiceMetadataBehavior行为,WebServiceHost也会将基于HTTP-GET的元数据发布功能屏蔽。我们可以通过一个简单的实例来证实这一点。

   1: using (WebServiceHost host = new WebServiceHost(typeof(EmployeesService)))
   2: {
   3:     ServiceDebugBehavior serviceDebug = 
   4:     host.Description.Behaviors.Find<ServiceDebugBehavior>();
   5:     if (null == serviceDebug)
   6:     {
   7:         serviceDebug = new ServiceDebugBehavior();
   8:         host.Description.Behaviors.Add(serviceDebug);
   9:     }
  10:     serviceDebug.HttpHelpPageEnabled = true;
  11:     serviceDebug.HttpsHelpPageEnabled = true;
  12:  
  13:     ServiceMetadataBehavior serviceMetadata = 
  14:     host.Description.Behaviors.Find<ServiceMetadataBehavior>();
  15:     if (null == serviceMetadata)
  16:     {
  17:         serviceMetadata = new ServiceMetadataBehavior();
  18:         host.Description.Behaviors.Add(serviceMetadata);
  19:     }
  20:     serviceMetadata.HttpGetEnabled = true;
  21:     serviceMetadata.HttpsGetEnabled = true;
  22:     host.Open();
  23:  
  24:     Console.WriteLine("ServiceDebugBehavior");
  25:     Console.WriteLine("\t{0, -20}: {1}", "HttpHelpPageEnabled", serviceDebug.HttpHelpPageEnabled);
  26:     Console.WriteLine("\t{0, -20}: {1}\n", "HttpsHelpPageEnabled", serviceDebug.HttpsHelpPageEnabled);
  27:  
  28:     Console.WriteLine("ServiceMetadataBehavior");
  29:     Console.WriteLine("\t{0, -20}: {1}", "HttpGetEnabled", serviceMetadata.HttpGetEnabled);
  30:     Console.WriteLine("\t{0, -20}: {1}", "HttpsGetEnabled", serviceMetadata.HttpsGetEnabled);
  31: }

在如上所示的代码片断中,我们分别将服务行为ServiceDebugBehavior和ServiceMetadataBehavior应用在寄宿服务上,并将其HttpHelpPageEnabled/HttpsHelpPageEnabled和HttpGetEnabled/HttpsGetEnabled属性设置为True。在开启WebServiceHost之后我们将两个服务行为的这4个属性打印出来。从如下所示的输出结果我们可以看出这四个属性最终被设置成了False。

   1: ServiceDebugBehavior
   2:     HttpHelpPageEnabled : False
   3:     HttpsHelpPageEnabled: False
   4:  
   5: ServiceMetadataBehavior
   6:     HttpGetEnabled      : False
   7:     HttpsGetEnabled     : False

二、添加标准终结点

终结点是由地址、绑定和契约三要素构成,所谓标准终结点,就是基于典型的通信场景选择组成终结点的要素(主要是绑定和契约)进而创建出一个标准的终结点。如果我们在使用WebServiceHost进行服务寄宿的时候指定一个基地址,在没有添加任何终结点的情况下WebServiceHost会在开启过程中自动添加一个类型(Kind)为webHttpEndpoint 的标准终结点。我们同样可以通过实例演示来证实这一点。

   1: Uri baseAddress = new Uri("http://127.0.0.1:3721/employees");
   2: using (WebServiceHost host = 
   3:     new WebServiceHost(typeof(EmployeesService), baseAddress))
   4: {
   5:     host.Open();
   6:     ServiceEndpoint endpoinit = host.Description.Endpoints[0];
   7:     Console.WriteLine("{0,-8}: {1}", "Address", endpoinit.Address);
   8:     Console.WriteLine("{0,-8}: {1}", "Binding", endpoinit.Binding);
   9:     Console.WriteLine("{0,-8}: {1}", "Contract", endpoinit.Contract.ContractType);
  10: }

在没有提供任何配置的情况下,我们通过如上的代码借助于WebServiceHost进行服务寄宿。在基于服务类型创建的WebServiceHost中指定了一个基地址,并在开启WebServiceHost之后,我们获取第一个终结点并将其三要素打印出来。从如下所示的输出结果我们可以看出:WebServiceHost基于服务类型实现的契约接口IEmployees创建了一个终结点,该终结点采用指定的基地址作为其地址,绑定类型为WebHttpBinding。

   1: Address : http://127.0.0.1:3721/employees
   2: Binding : System.ServiceModel.WebHttpBinding
   3: Contract: Artech.WcfServices.Service.Interface.IEmployees

三、添加WebHttpBehavior终结点行为

正如上面所说,WebServiceHost除了完成从ServiceHost继承下来的服务寄宿功能之外,其只要的职责就是将WebHttpBehavior行为应用到寄宿服务的所有终结点上。也就是说,当我们开启WebServiceHost的时候,如果终结点没有应用WebHttpBehavior行为,会自动创建一个新的WebHttpBehavior对象应用到终结点上。WebHttpBehavior的自动添加可以通过如下代码中的调试断言来体现。

   1: using (WebServiceHost host = new WebServiceHost(typeof(EmployeesService)))
   2: {
   3:     foreach (ServiceEndpoint endpoint in host.Description.Endpoints)
   4:     {
   5:         WebHttpBehavior behavior = endpoint.Behaviors.Find<WebHttpBehavior>();
   6:         if (null != behavior)
   7:         {
   8:             endpoint.Behaviors.Remove(behavior);
   9:         }
  10:     }        
  11:     host.Open();
  12:     foreach (ServiceEndpoint endpoint in host.Description.Endpoints)
  13:     {
  14:         Debug.Assert(null != endpoint.Behaviors.Find<WebHttpBehavior>());
  15:     }      
  16: }

可以说WebHttpBinding和WebHttpBehavior是整个Web HTTP编程模型最为核心的两个类型,前者主要解决消息编码问题,而余下的工作基本上落在了终结点行为WebHttpBehavior上。WebHttpBehavior属性HelpEnabled和AutomaticFormatSelectionEnabled是“帮助页面”与“自动消息格式选择”这两个特性的总开关。[“自动消息格式(JSON/XML)选择”源代码从这里下载]

   1: public class WebHttpBehavior : IEndpointBehavior, ...
   2: {
   3:     //其他成员    
   4:     public virtual bool HelpEnabled { get; set; }
   5:     public virtual bool AutomaticFormatSelectionEnabled { get; set; }
   6: }

一、 帮助页面

WCF 4.0为REST服务提供了帮助页面功能,我们可以通过浏览器访问服务帮助页面的地址得到所有操作的基本信息。但是这个功能在默认的情况下是关闭的,我们需要通过应用在终结点上的WebHttpBehavior行为的HelpEnabled属性开启该功能。

   1: <configuration>
   2:     <system.serviceModel>
   3:         <behaviors>
   4:             <endpointBehaviors>
   5:                <behavior>
   6:                    <webHttp helpEnabled="true" />
   7:                </behavior>
   8:             </endpointBehaviors>
   9:         </behaviors>
  10:         <services>
  11:             <service name="Artech.WcfServices.Service.EmployeesService">
  12:                 <endpoint address="http://127.0.0.1:3721/employees"
  13:                           binding="webHttpBinding" 
  14:                           contract="Artech.WcfServices.Service.Interface.IEmployees"/>
  15:             </service>
  16:         </services>
  17:     </system.serviceModel>
  18: </configuration>

同样以之前演示的EmployeesService为例,我们通过如上的配置将终结点行为WebHttpHehavior应用在服务唯一的终结点上(默认终结点),并将HelpEnabled属性设置为True。那么基于终结点的帮助页面将以地址{终结点地址}/Help发布出来,我们通过浏览器访问这个地址就会得到如下图所示帮助页面。

image

如上图所示,帮助页面列出了包括相对地址、HTTP方法和基本描述在内的所有操作的基本信息。我们通过点击HTTP方法对应的链接可以获得包括基于相应格式(Xml和Json)消息结构(Schema)和实例。在默认的情况下,帮助页面中表示操作描述信息的格式为“Service at {操作地址}”,我们可以在定义服务契约的时候再操作方法上应用特性DescriptionAttribute来定义出现在帮助页面中的描述信息。

   1: [ServiceContract]
   2: public interface IEmployees
   3: {
   4:     [WebGet(UriTemplate = "all")]
   5:     [Description("获取所有员工列表")]
   6:     IEnumerable<Employee> GetAll();
   7:  
   8:     [WebGet(UriTemplate = "{id}")]
   9:     [Description("获取指定ID的员工")]
  10:     Employee Get(string id);
  11:  
  12:     [WebInvoke(UriTemplate = "/", Method = "POST")]
  13:     [Description("创建一个新的员工")]
  14:     Employee Create(Employee employee);
  15:  
  16:     [WebInvoke(UriTemplate = "/", Method = "PUT")]
  17:     [Description("修改现有员工信息")]
  18:     void Update(Employee employee);
  19:  
  20:     [WebInvoke(UriTemplate = "{id}", Method = "DELETE")]
  21:     [Description("删除指定ID的员工")]
  22:     void Delete(string id);
  23: }

如上面的代码片断所示,我们在契约接口IEmployees中的所有操作方法上应用了DescriptionAttribute特性并指定了相应的描述信息。这些描述信息就是出现在如下图所示的帮助页面中。

image

二、 自动消息格式选择

REST服务具有两种基本的消息格式(Xml和Json)。在定义服务契约的时候,我们可以通过应用在操作方法上的WebGetAttribute和WebInvokeAttribute指定回复消息的格式。如果没有通过这种方式对消息格式进行显式设置,我们还可以通过终结点行为WebHttpBehavior为回复消息设置一个默认的消息格式。除了这种显示设置方式之外,WCF还提供一种自动消息格式选择机制。

所谓消息格式的自动选择,就是服务根据请求消息来选择一种适合的格式进行消息的序列化。在默认的情况下,这种自动选择机制是关闭的,我们需要通过WebHttpBehavior的AutomaticFormatSelectionEnabled属性开启该机制。具体的消息格式选择机制策略(顺序)如下:

  • 如果作为请求的HTTP消息具有Accept报头,则根据该报头决定回复消息的格式;
  • 如果作为请求的HTTP消息具有Content-Type报头,则根据该报头决定回复消息的格式;
  • 如果在定义服务契约时通过WebGetAttribute或者WebInvokeAttribute对回复消息的格式进行了显式设置,则采用该消息格式;
  • 如果通过终结点行为WebHttpBehavior设置了对回复消息的格式进行了显式设置,则采用该消息格式;
  • 采用默认消息格式Xml(WebMessageFormat枚举的默认值)。

我们同样通过之前创建的EmployeesService的实例来演示消息格式的自动选择机制。如下面的配置片断所示,我们将WebHttpBehavior行为应用到了寄宿服务的唯一终结点上,并且将AutomaticFormatSelectionEnabled属性设置为True。

   1: <configuration>
   2:     <system.serviceModel>
   3:         <behaviors>
   4:             <endpointBehaviors>
   5:                 <behavior name="webHttp">
   6:                     <webHttp automaticFormatSelectionEnabled="true" />
   7:                 </behavior>
   8:             </endpointBehaviors>
   9:         </behaviors>
  10:     <services>
  11:         <service name="Artech.WcfServices.Service.EmployeesService">
  12:             <endpoint address="http://127.0.0.1:3721/employees" 
  13:                       behaviorConfiguration="webHttp"
  14:                       binding="webHttpBinding"  
  15:                       contract="Artech.WcfServices.Service.Interface.IEmployees"/>
  16:             </service>
  17:         </services>
  18:     </system.serviceModel>
  19: </configuration>

对于契约接口IEmployees来说,我们通过WebGetAttribute特性用于返回所有员工列表的GetAll操作的回复消息格式设置为Xml。

   1: [ServiceContract]
   2: public interface IEmployees
   3: {
   4:     //其他成员
   5:     [WebGet(UriTemplate = "all",ResponseFormat = WebMessageFormat.Xml)]
   6:     IEnumerable<Employee> GetAll();
   7: }

对于REST服务调用来说,其本质就是一种普通的HTTP请求,与针对某个网页的访问并没有什么本质的不同,所以我们完全可以手工生成HTTP请求来进行服务的访问。为此我们创建了如下一个静态方法GetAllEmployees方法通过WebClient对服务的GetAll操作进行调用,并将整个回复消息打印出来,该方法的两个参数分别是作为请求的HTTP消息的Content-Type和Accept报头值。

   1: static void GetAllEmployees(string contentType, string accept)
   2: {
   3:     WebClient webClient = new WebClient();
   4:     if (!string.IsNullOrEmpty(contentType))
   5:     {
   6:         webClient.Headers.Add("Content-Type", contentType);
   7:     }
   8:  
   9:     if (!string.IsNullOrEmpty(accept))
  10:     {
  11:         webClient.Headers.Add("Accept", accept);
  12:     }
  13:     using (StreamReader reader = new StreamReader(webClient.OpenRead(
  14:         "http://127.0.0.1:3721/employees/all")))
  15:     {
  16:         Console.WriteLine(reader.ReadToEnd());
  17:     }
  18: }

然后我们通过调用GetAllEmployees方法进行三次服务调用。第一次调用既没有指定Content-Type报头也没有指定Accept报头,第二次和第三次调用在分别将这两个报头指定为“application/json”。

   1: string contentType = "application/json";
   2:  
   3: Console.WriteLine("Content-Type = N/A; Accept = N/A:");
   4: GetAllEmployees("", "");
   5: Console.WriteLine();
   6:  
   7: Console.WriteLine("Content-Type = application/json; Accept = N/A:");
   8: GetAllEmployees(contentType, "");
   9: Console.WriteLine();
  10:  
  11: Console.WriteLine("Content-Type = N/A, Accept = application/json:");
  12: GetAllEmployees("", contentType);

从如下所示的输出结果我们可以看出:由于服务调用请求没有指定任何媒体类型相关的报头,所以回复消息采用的是契约接口中设置的消息格式Xml。对于后两次服务调用中,由于请求消息中分别通过Content-Type和Accept报头将“期望”的媒体类型设置为application/json,所以Json最终作为回复消息的格式。(S1006)

   1: Content-Type = N/A; Accept = N/A:
   2: <ArrayOfEmployee xmlns="http://www.artech.com/" xmlns:i="http://www.w3.org/2001/
   3: XMLSchema-instance"><Employee><Department>开发部</Department><Grade>G7</Grade><I
   4: d>001</Id><Name>张三</Name></Employee><Employee><Department>人事部</Department><
   5: Grade>G6</Grade><Id>002</Id><Name>李四</Name></Employee></ArrayOfEmployee>
   6:  
   7: Content-Type = application/json; Accept = N/A:
   8: [{"Department":"开发部","Grade":"G7","Id":"001","Name":"张三"},{"Department":"人
   9: 事部","Grade":"G6","Id":"002","Name":"李四"}]
  10:  
  11: Content-Type = N/A, Accept = application/json:
  12: [{"Department":"开发部","Grade":"G7","Id":"001","Name":"张三"},{"Department":"人
  13: 事部","Grade":"G6","Id":"002","Name":"李四"}]
作者:Artech 
出处:http://artech.cnblogs.com 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
原文地址:https://www.cnblogs.com/Leo_wl/p/2343381.html