在前几篇博客中我有说到服务的寄宿,就是服务要运行起来必须采取的几种方式,相当于我们可以照葫芦画瓜的效果运行一个wcf服务,但是那只是实践,我们知其然更要知其所以然,所以从今天开始我们开始介绍wcf的三大部分:契约、绑定以及服务。当然这其中会有很多的细节问题,比如说终结点等等,我们穿插着进行。今天就先来预习一下契约,也称为合同(Contract).
wcf的契约一共包括几种:数据契约、服务契约、消息契约、错误契约。既然是预习,那么我们就一起来学习一下这几种契约的作用。
数据契约:DataContract 是用来对wcf的数据传输进行串行化。说到串行化,.net中也有串行化,但是.Net的串行化是把所有数据:私有成员、公有成员以及私有属性、公有属性等全部串行化,但是wcf的数据串行化是需要显式指定的,只有我们标记了DataContract属性的类上的DataMember属性之后才会串行化。数据契约涉及到版本问题,也就是数据字段的添加或删除?这个问题留给亲爱的你来解决吧。
服务契约:ServiceContract 只有标记了ServiceContract的类或接口才可以作为契约对外发布,并且契约中的方法只有标记了OperationContract,才会被客户端访问到。也就是说服务契约对外发布的接口方法也是需要显式指定的。其中有双向绑定、单工模式、请求响应模式。在服务契约的开发中,我们如果采用wcf的并发模式,那么我们一定要注意死锁的问题?ServiceBehavior 特性的ConcurrencyMode我们可以通过制定Multiple来启动多线程访问。
错误契约:FaultContract 是用来对客户端显式错误信息。可以自定义要显式的错误信息。
消息契约:MessageContract 可以对Soap消息进行完全的控制。但是一般用不到,所以我们有机会再深入研究这个契约。
简洁的介绍了一下这几种契约,下面我们通过一个简单的示例综合上面的除了消息契约以外的几种契约,让我们有一个大概的认识。项目还是沿用我们原来的那个项目版本,我在那个上面更改,具体的项目结构我就不再赘述。
首先,我们创建一个契约IPerson 表示Person的契约,
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 namespace Chinaer.WcfDemo.Contracts 7 { 8 [ServiceContract] 9 public interface IPerson 10 { 11 [OperationContract] 12 [FaultContract(typeof(StateStatus))] 13 PersonInfo GetPerson(); 14 15 /// <summary> 16 /// 请注意我们没有制定Check方法的OperationContract属性 它不会被客户端调用到 17 /// </summary> 18 /// <param name="perInfo"></param> 19 /// <returns></returns> 20 bool Check(PersonInfo perInfo); 21 } 22 }
其中在服务契约方法可能会使用到自定义异常信息的方法,我们添加了泛型FaultContract
添加PersonInfo实体类以及错误契约的声明
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 using System.Runtime.Serialization; 7 namespace Chinaer.WcfDemo.Contracts 8 { 9 [DataContract] 10 public class PersonInfo 11 { 12 [DataMember] 13 public string Name { get; set; } 14 15 [DataMember] 16 public string Address { get; set; } 17 /// <summary> 18 /// 请注意Age属性我们没有添加DataMember属性 就表示它不会被序列化 19 /// </summary> 20 public int Age { get; set; } 21 } 22 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 using System.Runtime.Serialization; 7 namespace Chinaer.WcfDemo.Contracts 8 { 9 [DataContract] 10 public class PersonInfo 11 { 12 [DataMember] 13 public string Name { get; set; } 14 15 [DataMember] 16 public string Address { get; set; } 17 /// <summary> 18 /// 请注意Age属性我们没有添加DataMember属性 就表示它不会被序列化 19 /// </summary> 20 public int Age { get; set; } 21 } 22 }
上面是PersonInfo实体类说明
错误信息实体类声明:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 using System.Runtime.Serialization; 7 namespace Chinaer.WcfDemo.Contracts 8 { 9 [DataContract] 10 /// <summary> 11 /// 错误契约 12 /// </summary> 13 public class StateStatus 14 { 15 [DataMember] 16 public int BadState { get; set; } 17 18 } 19 }
我们添加服务类,我们在服务类方法中故意抛出一个自定义异常信息。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using Chinaer.WcfDemo.Contracts; 6 using System.ServiceModel; 7 namespace Chinaer.WcfDemo.Services 8 { 9 /// <summary> 10 /// 这是服务实现类 11 /// </summary> 12 public class Person : IPerson 13 { 14 15 public PersonInfo GetPerson() 16 { 17 18 PersonInfo pInfo = new PersonInfo() { Address = "济南", Age = 25, Name = "郭志奇" }; 19 ///我们故意抛出一个自定义异常信息 20 if (pInfo != null) 21 { 22 //创建两个表示错误信息的数组 23 FaultReasonText[] text = new FaultReasonText[2]; 24 text[0] = new FaultReasonText("simple message", new System.Globalization.CultureInfo("en-us")); 25 text[1] = new FaultReasonText("中文表示 简单的错误信息", new System.Globalization.CultureInfo("zh-cn")); 26 FaultReason faultReason = new FaultReason(text); 27 throw new FaultException<StateStatus>(new StateStatus() { BadState = int32.MaxValue }, faultReason); 28 29 } 30 31 return pInfo; 32 } 33 34 public bool Check(PersonInfo perInfo) 35 { 36 bool flag = false; 37 if (perInfo.Name.ToLower() == "guozhiqi") 38 { 39 flag = true; 40 } 41 return flag; 42 } 43 } 44 }
其中错误异常信息FaultReasonText 表示错误信息说明。
上面就实现了服务契约、错误契约、以及数据契约的示例。大家可以看到,其实错误契约是在一个数据契约定义的基础上在服务契约的方法中添加FaultContract 来实现的自定义异常信息。并且在操作方法中还要包装要抛出的异常信息。
我们在客户端调用来查看是否可以正常的返回异常信息。其中我们要创建宿主。
ConsoleHosting就是控制台宿主类,我们通过它类运行服务。
配置文件:
1 <?xml version="1.0" encoding="utf-8" ?> 2 <configuration> 3 <system.serviceModel> 4 <behaviors> 5 <serviceBehaviors > 6 <behavior name="metaDataBehavior"> 7 <serviceMetadata httpGetEnabled="true" httpGetUrl="http://127.0.0.1:8888/personService/mex"/> 8 <serviceDebug includeExceptionDetailInFaults="true"/> 9 </behavior> 10 </serviceBehaviors> 11 </behaviors> 12 13 <services> 14 <service name="Chinaer.WcfDemo.Services.CalService" behaviorConfiguration="metaDataBehavior"> 15 <endpoint address="http://127.0.0.1:8888/calService" binding="wsHttpBinding" contract="Chinaer.WcfDemo.Contracts.ICal"></endpoint> 16 </service> 17 <service name="Chinaer.WcfDemo.Services.Person" behaviorConfiguration="metaDataBehavior"> 18 <endpoint address="http://127.0.0.1:8888/personService" binding="wsHttpBinding" contract="Chinaer.WcfDemo.Contracts.IPerson" 19 ></endpoint> 20 21 </service> 22 </services> 23 24 <bindings></bindings> 25 26 </system.serviceModel> 27 </configuration>
配置文件很简单,服务宿主的运行依然很简单ServiceHost
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.ServiceModel; 6 using Chinaer.WcfDemo.Contracts; 7 using System.ServiceModel.Description; 8 using Chinaer.WcfDemo.Services; 9 using System.ServiceModel.Dispatcher; 10 namespace Chinaer.WcfDemo.ConsoleHosting 11 { 12 class Program 13 { 14 static void Main(string[] args) 15 { 16 using (ServiceHost host = new ServiceHost(typeof(Person))) 17 { 18 //1.创建ServiceHost对象 这里我们添加了服务的地址,如果这里没有添加服务地址,那么我们在添加服务终结点的时候就需要添加完整路径 19 //2/添加服务终结点 注意后面的地址我设置为空 因为我在前面添加了地址 20 ////////host.AddServiceEndpoint(typeof(ICal), new WSHttpBinding(), ""); 21 if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null) 22 { 23 //3.添加元数据终结点 上面添加了这个判断的原因 就是查找配置文件中是否配置了元数据行为 因为我们重命名了配置文件 所以元数据行为肯定为null 24 ServiceMetadataBehavior metaDataBehavior = new ServiceMetadataBehavior();//创建元数据行为对象 25 metaDataBehavior.HttpGetEnabled = true;//重点 启用元数据行为的访问 26 metaDataBehavior.HttpGetUrl = new Uri("http://127.0.0.1:9999/person/msdl"); 27 host.Description.Behaviors.Add(metaDataBehavior);//添加元数据行为 28 } 29 //添加打开服务宿主的执行函数 本例为匿名函数 30 31 32 33 host.Opened += delegate 34 { 35 Console.WriteLine("服务已启动 按任意键退出"); 36 }; 37 try 38 { 39 if (host.State != CommunicationState.Opened) 40 { 41 host.Open(); 42 Console.Read(); 43 } 44 45 } 46 catch (Exception ex) 47 { 48 throw ex; 49 } 50 51 Console.Read(); 52 } 53 54 } 55 } 56 }
通过上面的步骤我们基本上就可以在浏览器中查看服务的运行情况,我们在客户端调用服务查看异常的详细信息。
由于采用的是MVC,所以我们把调用服务的操作放到了一个Action中,
1 public ActionResult Index() 2 { 3 try 4 { 5 ViewBag.Message = "修改此模板以快速启动你的 ASP.NET MVC 应用程序。"; 6 WSHttpBinding wsHttpbinding = new WSHttpBinding(); 7 EndpointAddress endAddress = new EndpointAddress("http://127.0.0.1:8888/" + 8 "personservice"); 9 using (ChannelFactory<IPerson> factory = new ChannelFactory<IPerson>(wsHttpbinding, endAddress)) 10 { 11 IPerson person = factory.CreateChannel(); 12 13 person.GetPerson(); 14 } 15 } 16 catch (FaultException<StateStatus> ex) 17 { 18 Response.Write(ex.Detail.BadState); 19 } 20 return View(); 21 }
通过在客户端创建信道来调用运程服务程序,我们调用了服务的方法。如果可以正常运行,我们会看到异常信息打印到浏览器上,而且是int32的最大值.
这里有一个调试技巧,有园友问我,每次调试都要单独的执行控制台宿主,然后再执行客户端程序,太麻烦了,有没有简单的办法。这里倒是有一个。我们选择解决方案的属性,然后选择宿主程序和客户端程序同时启动就可以。
这样我们运行程序,在浏览器中如果看到-1 就表示我们的异常信息成功显示。
显示了int32的最大值,说明我们的异常信息正确显示了。
总结一下:这一篇博客有点乱,没有头绪。服务契约就是定义应该具有的操作,由OperationContract具体控制。
数据契约主要是用来让数据序列化,由DataMember实际控制。
错误契约 显示自定义的异常信息,防止详细信息被客户端看到,保护程序的安全。但是错误契约需要提前了解到哪个方法会引发哪种类型的异常。这个是很难确定的。所以我们在捕获异常信息的时候,除了捕获已知的异常信息外,我们还需要捕获FaultException 和CommunicationException异常,这两个类是我们自定义异常信息的基类。通过捕获CommunicationException 我们还可以捕获与wcf通信相关的其他异常信息。
契约的范围很广,我们慢慢叙述,其中每种契约的各种属性的对照,我们需要深入学习。因为每个属性的误用都可能会导致程序运行出现异常。
我又回来了,回到了技术最前线,