(郁闷,不知道字数上限,发现上传不了了,才被迫一分为二,;P)
<system.serviceModel> <services> <service behaviorConfiguration="WcfDuplexServiceInIIS.StockServiceBehavior" name="WcfDuplexServiceInIIS.StockService"> <!--将绑定更改为双向--> <endpoint address="ws" binding="wsDualHttpBinding" contract="WcfDuplexServiceInIIS.IStockService"> <identity> <dns value="localhost" /> </identity> </endpoint> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> </service> </services> <behaviors> <serviceBehaviors> <behavior name="WcfDuplexServiceInIIS.StockServiceBehavior"> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
上面程序中有一个问题,就是为每个客户端创建线程。这种情况下,客户端个数不可预期(可能是数百万),但是股票个数却有限制(数千)。因此,为每只股票创建线程比为每个客户端创建线程会更好。
下面代码显示了更改。这个例子中,使用HashTable为每一个客户端的更新请求维护股票信息。Update类存储在HashTable中,而每个Update类都运行在自己的线程上。客户回调列表存储在Update类的本地线程中,所以Update类可以关注所有客户端所关联的股票。注意,lock关键字,当访问客户端列表集合时,无论是通过StockService类还是Update类本身都出现了,这样是必要的。一面客户端列表集合不会在StockService类修改之后,在此被Update类重复修改。
using System; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.ComponentModel; using System.Threading; using System.Collections.Generic; namespace WcfDuplexServiceInIIS { public class Update { #region per Stock public string ticker; public List<IClientCallback> callbacks = new List<IClientCallback>(); #endregion public void SendUpdateToClient() { #region per Stock Random w = new Random(); Random p = new Random(); while (true) { Thread.Sleep(w.Next(5000)); //假设更新在此发生 lock (callbacks) foreach (IClientCallback c in callbacks) try { c.PriceUpdate(ticker, 100.00 + p.NextDouble() * 10); } catch (Exception ex) { Console.WriteLine("Error sending cache to client: {0}", ex.Message); } } #endregion } } }1: using System;2: using System.Collections.Generic;3: using System.Linq;4: using System.Runtime.Serialization;5: using System.ServiceModel;6: using System.Text;7: using System.Threading;8: using System.Collections;9:
10: namespace WcfDuplexServiceInIIS11: {
12: /// <summary>13: /// 实现双向服务契约的服务端类使用PerSession的InstanceContextMode存储结果。14: /// 一个服务的实例将被绑定到每个双向会话上。15: /// </summary>16: public class StockService : IStockService17: {
18: #region per Stock19: #region IStockService Members20:
21: public class Worker22: {
23: public string ticker;24: public Update workerProcess;25: }
26:
27: public static Hashtable workers = new Hashtable();28:
29: void IStockService.RegisterForUpdates(string ticker)30: {
31: Worker w = null;32:
33: //如果需要,创建一个新的Worker,并将它添加到hashtable中,而且在新线程中启动它。34: if (!workers.ContainsKey(ticker))35: {
36: w = new Worker();37: w.ticker = ticker;
38: w.workerProcess = new Update();39: w.workerProcess.ticker = ticker;
40: workers[ticker] = w;
41:
42: Thread t = new Thread(new ThreadStart(w.workerProcess.SendUpdateToClient));43: t.IsBackground = true;44: t.Start();
45: }
46:
47: //获取当前指定股票的worker,然后添加客户端代理到它的回调列表中。48: w = (Worker)workers[ticker];
49: IClientCallback c = OperationContext.Current.GetCallbackChannel<IClientCallback>();
50: lock (w.workerProcess.callbacks)51: w.workerProcess.callbacks.Add(c);
52: }
53:
54: #endregion55: #endregion56: }
57: }
无论是使用线程/客户端实现还是使用线程/股票实现,都存在着可靠性的问题。例如,如果服务无法调用客户端回发操作,它记录一个消息到输出设备上,但是它从未重试。服务端应该重试吗?如果重试频率是多大?到什么时候为止呢?或者,如果有一个预定的窗口,通过它客户端可以在知道服务端将无法接收更新时,更新请求可以被排队,使他们可以推迟时间?这些都是非常重要的问题。这些问题都可以通过使用如BizTalk或类似的消息处理产品解决。消息处理通常其系统的核心功能具有持久存储(数据库、文件系统或消息队列)功能,而且包含强大的配置工具来制定传输和重试策略。但是他们仍然承担性能、复杂度和成本上的考虑。所以这种解决方案非常依赖需求方的意见。
实现双向契约的客户部分
为了加入双向消息交换模式,客户端必须实现WCF的ABCs,即它必须在客户端定义一个服务端发送消息的地址,一个指示服务端如何发送消息到客户端的绑定以及一个明确定义消息格式的契约。幸运的是,当你生成客户端代理和底层管道结构运行时时需要关心的问题。
为了生成客户端代理,可以使用svcutil.exe或者添加服务引用。代理定义一个使用与服务端相同名称的借口,使用Callback在结尾追加。如果服务契约接口是IStockService,客户端借口就是IStockServiceCallback。客户端必须实现一个继承接口的类。
在运行时,就想服务端,客户端通过严格的端点定义然后接收消息。服务端点和客户端点主要的不同是客户端点是WCF动态创建的。在客户端代码中没有配置文件或明确的ServiceHost调用。再次,WCF照顾到这一点,素以客户端只需要实现一个继承自生成的接口的类。
下面的代码显示了客户端调用StockServcie服务的RegisterForUpdates方法来请求定期的更新。它同样实现了回调接口,PriceUpdate,正如服务端所要求的那样,实现股票价格的更新。注意,一个InstanceContext对象被实例化,并用来创建代理。InstanceContext对象为服务存储内容信息,如提及的在客户端代表创建的传入和传出管道。
1: using System;2: using System.Collections.Generic;3: using System.Linq;4: using System.Text;5: using System.ServiceModel;6: using DuplexClient;7: using DuplexClient.DuplexStockServiceReference;8:
9: namespace DuplexClient10: {
11: /// <summary>12: /// 1. 创建一个服务,它包含两个接口。一个是服务接口,另一个是回调接口。13: /// 2. 部署服务。14: /// 3. 使用svcutil.exe生成契约(接口)。15: ///16: /// 5. 创建一个InstanceContext类的实例。构造函数要求一个客户端类的实例。17: /// 6. 使用构造函数创建一个WCF客户端实例,构造函数需要InstanceContext对象。18: /// 构造函数的第二个参数是配置文件中端点的名字。19: /// 7. 调用方法请求。20: /// </summary>21: class Program22: {
23: static void Main(string[] args)24: {
25: InstanceContext site = new InstanceContext(new CallbackHandler());26:
27: StockServiceClient proxy = new StockServiceClient(site, "WSDualHttpBinding_IStockService");28: try29: {
30: proxy.RegisterForUpdates("中软国际");31: proxy.RegisterForUpdates("中软股份");32: proxy.RegisterForUpdates("中软香港");33: }
34: catch (TimeoutException timeProblem)35: {
36: Console.WriteLine("The service operation timed out. " + timeProblem.Message);37: proxy.Abort();
38: Console.Read();
39: }
40: catch (CommunicationException commProblem)41: {
42: Console.WriteLine("There was a communication problem. " + commProblem.Message);43: proxy.Abort();
44: Console.Read();
45: }
46:
47: Console.WriteLine("Press Enter or any key to exit");48: Console.ReadLine();
49: }
50: }
51: }
1: using System;2: using System.Collections.Generic;3: using System.Linq;4: using System.Text;5: using DuplexClient;6: using DuplexClient.DuplexStockServiceReference;7:
8: namespace DuplexClient9: {
10: /// <summary>11: /// 4. 在客户端类中实现回调接口。12: /// </summary>13: public class CallbackHandler : IStockServiceCallback14: {
15:
16: #region IStockServiceCallback Members17:
18: void IStockServiceCallback.PriceUpdate(string ticker, double price)19: {
20: Console.WriteLine("Received alert at : {0}. {1}:{2}",System.DateTime.Now,ticker,price);21: }
22:
23: #endregion24: }
25: }
一个服务中的多契约与多端点
服务被定义为端点的集合。每个端点都有地址、绑定和契约。契约是指抛出的端点的什么功能,地址是指应用程序(服务)在网络上所处的位置,而绑定是指如何访问他们。
端点与契约之间有一对多的关系。一个端点只能由一个契约,但是一个契约可以被多个端点引用。虽然端点只能定义一个契约,接口的聚合可以使一个合同抛出多个接口。此外,多个使用相同的绑定的端点,但是契约不同,也可以使用相同的地址。可以想象一个单独的端点可以实现两个契约。
在一个服务中通过多个端点暴露一个契约,你可以通过多种绑定方式实现。你可以使用WS-I基本概念绑定定义一个端点来暴露一个契约,同时可以通过另一个使用TCP协议和二进制编码的端点来暴露它以获得更高性能。通过整合多个接口为一个接口,你可以通过一个服务提供功能的综合访问。
下面代码实现两个服务契约,IGoodStockService和IGreatStockService,共同聚集为第三个服务契约,IStockService。这些接口中的方法定义也聚集在一起。虽然服务接口可以继承,但是[ServiceContract]属性必须定义在每个接口上。
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Runtime.Serialization;
5: using System.ServiceModel;
6: using System.Text;
7:
8: namespace WcfMultiServiceInIIS
9: {
10: [ServiceContract]
11: public interface IGoodStockService
12: {
13: double GetStockPrice(string ticker);
14: }
15: }
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Runtime.Serialization;
5: using System.ServiceModel;
6: using System.Text;
7:
8: namespace WcfMultiServiceInIIS
9: {
10: [ServiceContract]
11: public interface IGreatStockService
12: {
13: double GetStockPriceFast(string ticker);
14: }
15: }
1: using System;2: using System.Collections.Generic;3: using System.Linq;4: using System.Runtime.Serialization;5: using System.ServiceModel;6: using System.Text;7:
8: namespace WcfMultiServiceInIIS9: {
10: [ServiceContract]
11: public interface IAllStockService : IGoodStockService,IGreatStockService12: {
13: }
14: }
1: using System;2: using System.Collections.Generic;3: using System.Linq;4: using System.Runtime.Serialization;5: using System.ServiceModel;6: using System.Text;7: using System.Threading;8:
9: namespace WcfMultiServiceInIIS10: {
11: public class AllStockService : IAllStockService12: {
13: #region IGoodStockService Members14:
15: double IGoodStockService.GetStockPrice(string ticker)16: {
17: Thread.Sleep(5000);
18: return 94.85;19: }
20:
21: #endregion22:
23: #region IGreatStockService Members24:
25: double IGreatStockService.GetStockPriceFast(string ticker)26: {
27: return 94.85;28: }
29:
30: #endregion31: }
32: }
下面代码显示了一个配置文件中为三个契约暴露多个端点。一个为IGoodStockService契约,两个端点为IGreatStockService契约,另一个端点为IAllStockService契约。
由于多个端点使用绑定的共享地址,所以必须为每个端点定义不同的地址。相对地址的使用,使每个端点的完整地址就是服务端地址加上相对地址。
<system.serviceModel> <services> <service behaviorConfiguration="WcfMultiServiceInIIS.AllStockServiceBehavior" name="WcfMultiServiceInIIS.AllStockService"> <host> <baseAddresses> <add baseAddress="http://chinasofti-eric/WCFMultiServiceInIIS/"/> </baseAddresses> </host> <endpoint name="GoodStockService" binding="wsHttpBinding" contract="WcfMultiServiceInIIS.IGoodStockServcie"/> <endpoint name="BetterStockService" address="better" binding="basicHttpBinding" contract="WcfMultiServiceInIIS.IGreatStockService"/> <endpoint name="BestStockService" address="best" binding="wsHttpBinding" contract="WcfMultiServiceInIIS.IGreatStockService"/> <endpoint name="AllStockService" address="all" binding="wsHttpBinding" contract="WcfMultiServiceInIIS.IAllStockService"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> </service> </services> <behaviors> <serviceBehaviors> <behavior name="WcfMultiServiceInIIS.AllStockServiceBehavior"> <serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>由于IGreatStockService契约在多个端点中暴露,客户端应用程序在创建代理类实例时必须通过名字引用端点。如果端点名没有定义,WCF会抛出一个错误,因为它无法知道那个端点正要被使用。下面代码显示了如何使用GreatStockServiceClient代理类两次:第一次访问BetterStockService使用basicHttpBinding,第二次访问BestStockService使用wsHttpBinding。
1: #region 访问多契约服务2: static void Main(string[] args)3: {
4: using (MultiServiceReference.GreatStockServiceClient proxy =5: new Client.MultiServiceReference.GreatStockServiceClient("BetterStockService"))6: {
7: Console.WriteLine(proxy.GetStockPriceFast("中软国际"));8: }
9:
10: using (MultiServiceReference.GreatStockServiceClient proxy =11: new Client.MultiServiceReference.GreatStockServiceClient("BestStockService"))12: {
13: Console.WriteLine(proxy.GetStockPriceFast("中软国际"));14: }
15:
16: Console.WriteLine("Operation is complete.");17: Console.WriteLine("Press <ENTER> to terminate service.");18: Console.WriteLine();
19: Console.ReadLine();
20: }
21: #endregion
WSDL中的操作的名字,类型,活动和命名空间
WCF基于服务源代码中的命名类和属性定义生成一个向外抛出的文件。这个文件通过服务的MEX端点暴露,一般通过客户端在设计时以WSDL的形式使用。在客户端,WSDL然后使用它编写代码来构建匹配的消息格式进行与服务的通信。所以类名、方法名、参数名的命名与选择的影响远远超出服务本身的生命周期范围。
然而,通常在服务接口中暴露内部名称和细节是非常差的选择。例如,你也许有一个分配算法叫做BurgerMaster,而你希望将他以Resource的命名暴露在外部。或者也许受到编码标准的限制你必须决定以什么命名接口。幸运的是,你可以在服务端控制所有服务抛出的名字。通过修改[ServiceContract]、[OperationContract]和[ServiceBehavior]属性。下表列出了如何使用源代码中的WCF属性控制WSDL的格式。
WSDL标签
Wcf 属性
targetNamespace
默认是http://tempuri.org。可以使用代码中的[ServiceBehavior]属性修改。 wsdl:service 和 wsdl:definitions
[ServiceBehavior(Name=”myServiceName”)] wsdl:prottype
[ServiceContract(Name=”myContractName”)] wsdl:operation 和 soap:operation
[OperationContract(Name=”myOperationName”)] xs:element
[MesageParameter(Name=”myParamName”)] wsdl:input 和 wsdl:output
[OperationContract(Action=”myOperationAction”,ReplyAction=”myOperationReplyAction”)] wsdl:Binding
使用和命名[DataContract]和[ServiceContract]属性
下面的例子使用WCF中的属性来重写默认生成的定义。
1: using System;2: using System.Collections.Generic;3: using System.Linq;4: using System.Runtime.Serialization;5: using System.ServiceModel;6: using System.Text;7:
8: namespace WcfWSDLServiceInIIS9: {
10:
11: [ServiceContract(
12: Name = "MyBurgerMasterName",13: Namespace = "http://MyBurgerMasterNamespace")]14: public interface IBurgerMasterService15: {
16: [return: MessageParameter(Name="MyBurgerMasterStockPrice")]17: [OperationContract(
18: Name = "MyGetBurgerMasterStockPrice",19: Action = "MyGetBurgerMasterStockPriceAction",20: ReplyAction = "MyGetBurgerMasterStockPriceReplyAction")]21: double GetStockPrice(string ticker);22: }
23: }
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Runtime.Serialization;
5: using System.ServiceModel;
6: using System.Text;
7:
8: namespace WcfWSDLServiceInIIS
9: {
10: [ServiceBehavior(Namespace=http://MyBurgerMasterService)]
public class BurgerMasterService : IBurgerMasterService
11: {
12: #region IBurgerMasterService Members
13:
14: double IBurgerMasterService.GetStockPrice(string ticker)
15: {
16: return 100.00;
17: }
18:
19: #endregion
20: }
21: }
svcutil.exe工具具有-t:metadata选项可以用来从服务端生成WSDL。或者,如果服务通过http绑定抛出一个MEX端口,WSDL可以IE使用基本的地址访问到。使用svcutil.exe访问或IE访问时的WSDL会有些不同,这些不同微不足道,只是与包相关。这两种情况下,下表下面显示了使用代码列表访问的WSDL。wsdl:portType,wsdl:operation,和wsdl:action名字都被代码控制了。注意,wsdl:portType的名字是MyBurgerMasterName,而不是默认的BurgerMaster。
<?xml version="1.0" encoding="utf-8" ?> - <wsdl:definitions name="BurgerMasterService" targetNamespace="http://MyBurgerMasterService" ... ... ... ... xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex"> <wsdl:import namespace="http://tempuri.org/" location="http://localhost:1877/BurgerMasterService.svc?wsdl=wsdl1" /> <wsdl:types /> - <wsdl:service name="BurgerMasterService"> - <wsdl:port name="WSHttpBinding_MyBurgerMasterName" binding="i0:WSHttpBinding_MyBurgerMasterName"> <soap12:address location="http://localhost:1877/BurgerMasterService.svc" /> - <wsa10:EndpointReference> <wsa10:Address>http://localhost:1877/BurgerMasterService.svc</wsa10:Address> - <Identity xmlns="http://schemas.xmlsoap.org/ws/2006/02/addressingidentity"> <Dns>localhost</Dns> </Identity> </wsa10:EndpointReference> </wsdl:port> </wsdl:service> </wsdl:definitions>