您的位置:首页 > 其它

第十六章 使用回调合约发布和订阅事件

2012-06-16 19:33 761 查看
到目前为止,本书中你看到的练习与例子都集中于C/S模型。在该模型中,一个服务器提供一个服务,该服务等待客户端主动发出的请求,接收到客户端的请求后,处理请求,然后选择性地向客户端程序发送响应。客户端程序是活动的参与者,提交请求并有效地决定了服务何时开始执行工作。然而这只是大多数的情况,WCF还支持其他的处理体系,比如点对点网络和客户端回调。

在点对点场景中,没有被动的服务。所有参与者都是自主的客户端,因为参与者之间可以平等地通讯。此时,不存在客户端/服务器关系,因此参与者任何时候都应当准备处理向其发送的消息。

使用客户端回调,服务可以调用客户端程序中的一个方法,实际上转换了C/S管理中的客户端和服务器,回调时原先的服务端和客户端将发生对调,服务端成为客户端,客户端成为服务端。在本章,你将调查如何定义客户端回调,以及如何使用客户端回调实现一个简单的事件机制,该事件机制用于通知关注服务状态的客户端服务的状态发生了改变。


实现和调用一个客户端的回调

在传统的C/S协议中,当宿主程序打开ServiceHost对象后,服务在一个或者多个由WCF服务架构建立的端点上侦听消息;然而客户端程序可能期望仅仅接收响应消息,而且该响应消息是客户端通过隐式请求发送到客户端的。一旦客户端打开了与服务之间的通道,WCF运行时将激活服务向客户端发送额外的消息(用于建立通讯),当客户端发出一个已经接收到额外消息的声明后,服务停止发送这类额外消息。WCF提供了两个特性来实现该功能:回调合约和双向通道。使用回调时非常重要的一点是:回调仅仅能在客户端向服务发送请求的处理过程中使用;而且还必须在由客户端初始化和用于发送请求的通道中调用回调。


定义一个回调合约

一个回调合约定义了服务可以在客户端中回调的操作。一个回调合约与服务合约非常相似,因为它们都是一个接口或一个类,其包含的操作标记了OperationContract特性。从语义方面来讲接口和类是有区别的,区别在于是否使用ServcieContract特性。下面的例子定义了一个方法,服务可以调用该方法通知客户端产品的价格发生改变。



侦听回调的客户端实现了回调合约中的每个方法。服务识别客户端的回调有两个必要条件:服务实现了服务合约中定义的回调操作,客户端必须引用了该服务合约(实现回调合约的服务合约)。为了达到该目的,你可以使用ServiceContract特性类中的CallbackContract属性标记服务合约,如下面的代码所示:



该代码的目的是:客户端程序调用ChangePrice操作以更新某个特定产品的价格;当产品的价格修改后,服务在客户端调用OnPriceChanged操作,然后回传一个修改后的产品到服务。你应当注意IProductsServiceV3服务合约中的其他操作也可以调用OnPriceChanged操作,这是因为回调合约与服务合约捆绑在一起,而不是与服务合约中的讴歌操作捆绑在一起。


在一个回调合约中实现一个操作

当你试图构建一个客户端代理去访问在服务合约中关联了回调合约的服务时,该代理类必须基于System.ServiceModel.DuplexClientBase类。该代理类的结构大致如下:



上图中***方框内的内容显示了与普通代理之间的不同,因为普通的代理不需要定义回调合约。开发人员应创建一个客户端程序,在这个客户端程序中应包含一个类,此类实现了IProductsServiceV3Callback接口和接口的OnPriceChanged方法。

ProductsServiceV3Client代理类扩展了DuplexClientBase<IProductsServiceV3>类,而且它还包含许多构造函数,客户端程序可以使用这些构造函数实例化代理对象。上诉代码片段仅仅显示了其中的两个构造函数,所有构造函数的主要特点都是:第一个参数均为一个InstanceContext对象。这是服务可以调用客户端中操作关键特性。

你现在应该已经非常清楚一个服务中通过InstacneContextMode为该服务指定的"instance context"的含义。如果不清楚也没关系。让我们来回忆一下,服务的每个实例运行在自己的上下文中,并且这些上下文中保存实例的状态信息。服务的每个实例都有自己的上下文。当实例化服务实例时,WCF运行时创建并自动地初始化这些上下文。那么什么时候开始初始化这些上下文呢?下面列出了初始化上下文的三种情况:

客户端程序开始一个新 会话(如果服务指定PerSession实例上下文模式)

客户端调用服务的一个操作(如果服务指定PerCall实例上下文模式)

服务的宿主程序启动服务(如果服务指定Single实例上下文模式)

当客户端连接到一个服务实例时,在客户端和服务之间传输消息的通讯通道中保存了某个特定服务实例的信息,因此WCF运行时可以引导消息到正确的服务实例。

当实现一个客户端回调时,你必须提供相同的功能以使服务端WCF运行时可以把消息路由回正确的客户端。为了实现该目标,你需要创建一个InstanceContext对象,该对象指向一个特定的客户端程序实例,当通过代理连接到服务时把InstaceContext对象传递到服务,客户端的WCF运行时自动地把客户端实例的信息包含在请求消息中,然后把请求消息发送之服务。如果服务需要调用回调合约中的操作,那么服务通过上下文对象直接调用适当的客户端实例。

下面的代码展示了如何在客户端实现客户端代理中IProductsServiceV3Callback接口:



InstanceContext的构造器中的this参数指向实现了IProductsServcieV3Callback合约的对象。DoSomeWork方法中创建proxy对象的语句引用了InstanceContext对象。如果服务通过该InstanceContext对象调用OnPriceChanged操作,那么服务端的WCF运行时将调用该客户端实例中的OnPriceChanged方法。

请注意,CallbackClient类还实现了IDisposable接口;Dispose方法用于关闭代理。一旦客户端连接到服务并且发送了一条初始化消息之后,服务可能在任意时刻回调客户端实例。如果客户端程序在DoSomeWork方法中,向服务发送完请求之后立即关闭代理;那么服务试图回调客户端实例时将失败,因为客户端实例对象不再有效。正如上面的代码所展示,如果客户端实例在DoSomwWork方法之后继续存在,那么在Dispose方法中关闭代理对象能确保服务可以在任意时刻回调客户端实例,除非客户端程序终止或者客户端对象被显示地销毁。


在回调合约中调用一个操作

为了调用回调合约中的一个操作,服务必须获取向服务发送请求的客户端实例的引用。正如你刚刚看到的,服务端的WCF运行时通过该服务的操作上下文获取客户端实例信息。你可以通过惊呆书香OperationContext.Current属性访问操作上下文,该属性返回一个OperationContext对象。OperationContext类提供generic的GetCallbackChannel方法,该方法可以返回一个通道的引用;这个通道就是服务和调用服务的客户端之间进行通讯的那个通道。GetCallbackChannel方法的返回值回调合约类型的引用;你可以通过这个引用调用操作。正如下面的代码所示:



很有可能在客户端调用完服务的操作和服务正在回调客户端的时间内,尤其当客户端通过单向方式调用服务的操作后,客户端开始终止或关闭通讯通道。因此你在服务执行回调之前你应该检查通道回调通道是否已经被关闭,如下面***方块中的代码所示:



所有的WCF通道都实现了ICommunicationObject接口。该接口提供了State属性,你可以使用该属性确定通道是否仍然处于打开的状态。如果State属性的值不为CommunicationState.Openned,那么此时服务不应该试图去使用回调。


回调操作与线程

如果服务调用回调合约中的一个操作,那么很可能客户端的代码也实现了该回调合约以使其他的操作能在服务中被回调。默认情况下,服务端的WCF运行时使用单线程处理回调的执行,因此回调至服务可能导致服务阻塞处理初始化请求的那个线程。在这种情况下,WCF运行时探测当前的情形,然后抛出一个InvalidOperationException异常,该异常的消息为"This operation would deadlock because the reply cannot be received until the current
Message completes processing"。为了避免出现这种情形的发生,你可以在客户端程序中的回调实现类中设置并发模式:那么启动多线程(如果客户端程序的代码是线程安全的),要么启动重入(如果客户端程序的代码不是线程安全的,但是客户端使用的数据在多次调用之前是一致的)。为了实现这点,你需要在客户端程序中的回调实现类上应用CallbackBehavior特性,设置该特性类的ConcurrencyMode属性的值为ConcurrenyMode.Muitiple或者ConcurrencyMode.Reentrant。





绑定和双向通道

并不是所有的绑定都支持客户端回调。为了支持客户端回调,你必须使用支持支持双向通讯的绑定;连接的一端必须能初始化通讯,而另一端必须能接受通讯。TCP传输协议和NamedPipes协议本身就是双向的,因此在实现客户端回调时,你可以使用NetTcpBinding绑定和NetNamePipeBinding绑定。HTTP协议实现的模型不支持双向操作,因此你不能使用BasicHttpBinding绑定,WSHttpBinding绑定,或者WS2007HttpBinding绑定。这看起来在基于HTTP传输协议的在内网系统中有很大的缺陷。然而,为此WCF提供了WSDualHttpBinding绑定以应对这种缺陷。该绑定建立两个HTTP通道(一个用于客户端向服务发送请求;另外一个用于服务向客户端发送那个请求),当然该绑定隐藏了实现的细节,因此不只需要把它当作一个普通的双向绑定即可。

在WSDualHttpBinding绑定和WSHttpBinding绑定与WS2007HttpBinding绑定之间有一些非常重要的差别。尤其是WSDualHttpBinding绑定不支持传输级安全,但是它还是实现了可靠的会话(而且你不能禁用该特性)


使用回调合约通知客户端单向操作的结果

使用回调合约的原则是,提供一个服务,该服务采用单向操作—不返回任何信息—的方式通知客户端程序。本小节的例子基于之前描述过的更改产品价格场景。当客户端程序调用ProductsService服务的ChangePrice操作时,当数据库中产品的价格更新后,服务将回调客户端并通知客户端价格已经发生变化。



练习:为ProductsService服务添加回调合约以及添加调用回调的操作

1. 使用Visual Studio,打开ProductsServiceV3.sln,该解决方案位于WCF\Step.by.Step\Solutions\Chapter16\ProductsServiceV3文件夹下。

该服务实现了ListProducts,GetProduct,CurrentStockLevel和ChangeStockLevelcaozuo .此外,它还包含了一个新的操作ChangePrice.客户端程序可以调用该操作以更改产品的价格。此外,该解决方案包含了一个WPF程序寄宿该服务,一个客户端程序用以测试ProdutsService服务。

2. 生成并运行该解决方案。在Products Service宿主窗口,点击start按钮。当服务启动后,在客户端控制台窗口中按下ENTER键。

客户端程序连接到服务,首先显示一个产品列表,然后显示产品FR-M21S-40的详细信息,并更改该产品的价格,最后显示更新后的价格。

当客户端程序结束后,关闭客户端控制台窗口。在Products Service宿主窗口点击Stop按钮以停止服务,然后关闭宿主程序,最后返回到Visual Studio

3. 在解决方案窗口中,打开ProductsClient项目下的Programm.cs文件。

查看Main方法的代码。该方法创建了Client类的一个实例,然后运行该实例的TestProductsService方法。TestProductsService方法创建代理对象,并使用该对象连接到ProductsService服务,然后执行ListPoructs操作和GetProduct操作。TestProductsService方法还调用PriceChange方法以更新指定产品的价格。ChangePrice操作在数据库中产品的价格更新前会一直阻塞客户端调用它,当产品价格更新后,该操作向调用者返回一个Boolean值以标明产品价格是否更新。由于该操作需要耗费一定的时间去更新数据库,你将修改其为单向操作,并为ProductsService服务创建一个回调合约,使用该回调合约通知客户端操作的结果。该策略可以使客户端继续运行而不需等待PriceChange操作的执行完成。

4. 在解决方案浏览器窗口,打开ProductsService服务下的IProductsService.cs文件。添加下面的回调合约:



该回调合约仅仅包含了一个操作OnPriceChanged。你将在后续步骤中更改ProductsService服务中的ChangePrice操作。该操作的目的是通知客户端作为参数所传入的产品的价格已经更改。请注意该操作指定为单向操作;它仅仅通知客户端而且不返回其他任何响应。

5. 修改IProductsServiceV3接口的ServiceContract特性,使其引用第4步定义的回调合约



因为CallbackContract属性必须为类型,所以上述代码使用typeof操作返回IProductsServiceV3Callback接口的类型。

6. 在IProductsServiceV3接口中,修改ChangePrice操作的定义并标记该操作为单向操作。单向操作不能有返回值,所以更改返回类型为void



7. 打开ProductsService.cs文件,找到ChangePrice方法。该方法使用新的产品价格更新AdventureWorks数据库,如果更新成功返回true,否则返回false。

更改该方法的返回类型为void。并修改return false或return true为return。



8. 生成ProductService项目



下面的步骤是在客户都程序中实现回调合约,但首先你需要为客户端生成代理类。

练习:生成客户端代理对象并实现回调合约

1. 使用下面的步骤为客户端程序生成代理类

1).打开Visual Studio命令行工具,并且切换路径到Chapter16\ProductsServiceV3\ProductsService\bin\Debug目录下

2).在Visual Studio命令行工具中,输入下面的命令svcutil ProductsService.dll

3).然后执行下面的命令

Svcutil /namespace:*.ProductsClient.ProductsService *.wsdl *.xsd /out:ProductsServiceProxy.cs

2. 使Visual Studio命令行处于打开的状态;然后回到Visual Studio。在ProductsClient项目中,删除现存的ProductsServiceProxy.cs文件,然后添加刚生成的ProductsServiceProxy.cs文件到ProductsClient项目中。

3. 编辑ProductsClient项目下的Program.cs文件。修改Client类使其实现ProductsServiceCallback和IDisposable接口。



ProductsServiceCallback是代理对象中定义了回调合约的接口。所以Client类需要实现该接口。

4. 在Client列的TestProductsService方法后面添加OnPriceChanged方法



5. 在OnPriceChanged方法后,添加Dispose方法



6. 在TestProductsService方法中,修改创建Proxy对象的声明:



上述代码常见一个InstanceContext对象,该对象引用Client对象,并作为参数传入到连接对象。请注意该连接所使用端点的名字发生了变化(现在使用WS2007HttpBinding_IProductsServiceV3);在后面的步骤中你将会在客户端的配置文件中添加WS2007HttpBinding_IProductsServiceV3端点的定义。

7. 在TestProductsService方法中,找到if/else中调用proxy对象的ChangePrice方法的代码片段。由于新的ChangePrice操作是单向操作,它不会返回任何值。更改这段代码,并移除if/else过程。



8. 在catch代码后,删除关闭proxy对象的声明;因为现在关闭proxy对象由Dispose方法来处理。

9. 在Programm.cs类的Main方法中,重构创建Client对象的生命,然后调用TestProductsService方法,并操作结束后等待用户按下ENTER键结束程序。



10. 重新生成解决方案。



练习:配置WCF服务和客户端程序使用WSDualHttpBinding绑定

1. 在ProductsServiceHost项目中,使用服务配置编辑器编辑App.config

2. 在配置面板,展开服务文件夹,展开Products.ProductsServiceImpl服务,展开端点文件夹,在WS2007HttpBinding_IProductsServcie端点上点击右键,然后点击删除端点。在服务配置编辑器对话框中,点击确认删除。

3. 在配置面板,在端点文件夹上点击右键,然后点击创建新的服务端点。按照下表内容创建一个新的端点

属性

名字WSDualHttpEndpoint_IProductsService
地址http://localhost:8010/ProductsService/Service.svc
绑定wsDualHttpBinding
合约Products.IProductsServiceV3
4. 保存配置文件,并退出WCF服务配置编辑器

5. 在ProductsServiceClient项目中,使用服务配置编辑器编辑App.config

6. 在配置面板,展开客户端文件夹,右键点击端点文件夹,然后点击创建新端点以创建一个新的端点。按照下表内容设置新端点

属性

名字
WSDualHttpEndpoint_IProductsService
地址
http://localhost:8010/ProductsService/Service.svc
绑定
wsDualHttpBinding
合约
ProductsClient.Products.IProductsServiceV3
与服务宿主程序不一样,在客户端你可以保留现有的客户端端点。

7. 保存位置文件,并退出WCF服务配置编辑器。

8. 在非调适模式下启动项目。在产品服务宿主窗口,点击开始。然后再客户端控制台窗口中,按下ENTER键。

客户端程序首先显示一份产品列表,然后电视FR-M21S-40产品的详细信息,然后调用ChangePrice操作使该款产品的价格增加10美元。请注意在Test 3开始后,将显示消息"Callback from service: Price of LL Mountain Frame – Sliver, 40 changed to $274.05"。该消息是由于服务调用OnPriceChanged操作而显示。









使用回调合约实现事件机制

回调合约允许服务确认客户端程序已经更改了产品的价格,但是接受确认消息的客户端实例可能已经知道这点了,因为客户端激发了更改价格的操作。无疑地,通知其他并发的客户端程序价格已经改变是非常有用的。

你可以使用回调实现一个事件机制;服务发布事件并提供操作以供客户端程序订阅这些事件或取消订阅。当时事件发生时,服务使用一个回调合约发送消息至每个订阅了该事件的客户端。为了实现这点,服务不惜必须引用每个客户端实例。在下面的联系中,你将修改ProdutsService服务以激活客户端程序通过订阅操作以关注感兴趣产品的价格变化。该操作的目的是简化缓存客户端实例的引用,当服务调用OnPriceChanged操作之后将使用的这些客户但实例。你还将添加一个取消订阅操作以使客户端程序可以取消价格更该订阅。

练习:添加订阅和却笑订阅操作至ProductsService服务

1. 在Visual Studio中,打开ProductsService项目下的IProductsService.cs文件。

2. 添加SubscribeToPriceChangedEvent时间和UnsubscribeToPriceChangedEvent事件到IProductsServiceV3服务合约中



客户端程序将使用SubscribeToPriceChangedEvent操作关注感兴趣产品的价格变化,使用UnsubscribeToPriceChangeEvent操作指明不再对产品价格变化感兴趣。

3. 打开ProductsService.cs文件,添加下面的私有变量:



ProductsServiceImpl类针对每个对产品感兴趣的客户端实例,都把服务对其的客户端回调引用添加到该列表中。

4. 添加SubscribeToPriceChangedEvent方法的具体实现



上述方法获取调用ChangePrice操作并保存在订阅列表中的的客户端实例对应回调合约的引用。如果回调合约的引用已经在列表中存在,那么将不会把回调毁约的引用添加到该列表中。

5. 添加UnsubscribeToPriceChangedEvent方法的具体实现



上述方法从列表中移除调用ChangePrice操作的客户端实例对于的回调引用。

6. 添加下面私有方法到ProductsServiceImple类中



该方法遍历所有的订阅列表中的所有回调引用。如果发现一个引用,并且该引用是有效的(客户端程序实例仍然处于运行的状态),该方法将调用OnPriceChanged操作,并将对应的产品作为参数传递给OnPriceChanged操作。如果引用是无效的,放方法则从订阅列表中删除该引用。

7. 在ChangePrice方法中,删除获取客户端程序对应的回调引用的声明,并删除调用OnPriceChanged方法的代码。取而代之的是,创建ProductData对象以保存修改产品的详细信息,然后调用raisePriceChangeEvnet方法。



当客户端程序实例更改了一个产品的价格,所有订阅了价格更改时间的客户端程序实例都会由于OnPriceChanged方法的执行而得到通知。

8. 重新生成ProductsService项目



练习:更新WCF客户端程序以订阅价格更改事件

1. 重新生成客户端代理类

2. 关闭Visual Studio命令行工具窗口,然后返回到Visual Studio中。从ProductsClient项目中删除ProductsServiceProxy.cs文件,并添加上一步生成的新版本的ProductsServiceProxy.cs文件

3. 打开ProductsClient项目的Programm.cs文件,添加调用SubscribeToPriceChangeEvent操作的代码



无论何时,当客户端程序实例更新一个产品的价格,服务将调用该客户端实例的OnPriceChanged方法。

4. 重新生成ProductsClient项目。



练习:测试ProductsService服务的价格变更事件

1. 在解决方案浏览器窗口中,在ProductsServiceHost项目上点击右键,选择调适,然后点击创建新的实例。在ProductsService寄宿窗口,点击开始按钮启动服务。

2. 在Windows浏览器中,转到\WCF\Step.by.Step\Solutions\Chapter16\ProductsServiceV3WithEvents\ProductsClient 文件夹下。

在该文件夹中,有一个名为RunClients的命令行文件。该命令文件简单地实现了同时运行三次ProductsClient程序,每次都打开一个新的窗口;该文件的内容如下:

start bin\Debug\ProductsClient

start bin\Debug\ProductsClient

start bin\Debug\ProductsClient

3. 双击RunClients.cmd文件,将会出现三个控制台窗口,每个窗口都创建一个客户端程序实例。在某一个控制台窗口中,按下ENTER键,等待出现产品列表,然后显示FR-M21S-40产品的详细信息,然后更改该款产品的价格。确认回调操作的消息出现。保持当前控制大窗口处于打开的状态。

4. 在另外两个控制台窗口中,按下ENTER键。等待出现产品列表,显示FR-M21S-40产品的详细信息,以及更改产品的价格。确认回调操作的消息出现。请注意,第二个回调操作的消息也出现在第一个控制台窗口中,不仅如此,第一个窗口中还显示了由第二控制台窗口更新后的产品价格。

5. 在最后一个控制台窗口中,按下ENTER键。确认该客户端实例更新了产品的价格并返回了回调消息。然后你可以发现第一个和第二个控制台窗口也输出了回调消息。到现在为止,第一个控制台窗口显示了三个回调消息。



6. 在每个控制台窗口中按下ENTER键以关闭客户端程序。在ProductsService寄宿窗口中,点击停止按钮关闭服务,最后关闭ProductsService窗口。



发布和订阅的传送模型

使用回调合约可以非常容易地实现基于WCF的发布和订阅服务。你应当意识到在前面的练习中一定程度上你所配置的环境是一个精心设置并且相对完美的运行环境。如果你在大型企业中,或者在Internetzhong实现这样的一个系统,那么你应当考虑安全性和稳定性,此外还应考虑所选择的模型对WCF服务回调客户端程序所产生的影响。

一般地,实现发布和订阅系统采用三种常见的模型,你可以基于任何一种模型构建WCF创建系统。每个模型都有其优缺点。

推送模型

该模型就是本章前面练习中你所使用的模型。在该模型中,发布者(WCF服务)通过回调合约中的操作直接向每个订阅者(WCF客户端程序)发送消息。服务必须有足够的资源同时回调大量的订阅者。在回调操作返回数据时服务需要对每一个订阅者生成一个新线程、或者在回调操作不返回数据时使用单向操作方式进行回调。 这种方式的主要弱点在于安全性。服务调用客户端的的回调操作可能被客户端防火墙阻塞。

拖拉模型

使用这种模型,发布者在事件发生时更新唯一的且可信任的第三方服务的信息。 每个订阅者周期地查询第三方服务的信息是否已经更新。该模型减少了防火墙的问题,但是在订阅者部分的要求更复杂。使用这个模型还可能会面临扩展性方面的问题—当大量的订阅者频繁地查询第三方服务。另外一个方面,入伙订阅者不是经常地查询第三方服务,那么客户端可能错过事件的发生。

中间人模型

该模型是前面两种模型的结合。发布者在时间发生时更新唯一的且可信任的第三方服务。第三方服务放置于一个特定的位置,比如发不事件的服务和订阅事件的客户顿均信任的网络中。订阅者在第三方服务注册事件,而不是直接在原始服务处订阅事件。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐