WCF4.0进阶系列--第七章 维持会话状态和设置服务操作的顺序
在之前章节所完成的练习中,客户端调用WCF服务的一系列操作,但是这些操作的顺序并不重要;因此先调用一个操作然后再调用另外一个操作,均不会对彼此产生影响,因为这些操作是相互独立地。但在实际应用中,服务的操作可能需按照一定的顺序调用。比如,如果你在服务中实现了购物车功能,那么在没有将任何商品放进购物车之前,就执行结算和支付操作显然是没有意义的。
按照一定的顺序调用操作会使你考虑在如何两次操作之间维持会话状态信息。以购物车为例,在何处存储购物车中商品的描述信息? 你至少有两个选择:
在客户端维持购物车。使用该方法,你将描述购物车内容的信息作为参数传递给服务端的操作,并将更新后的内容返回给客户端。传统的Web应用程序(包括ASP.NET Web程序)使用用户计算机上的Cookie存储信息就是一个该方案的变种。这种方案消除了web程序的维持客户端调用之间状态信息的边界;但是该方案不能阻止客户端程序直接修改存储在Cookie中的内容或者通过其他方式篡改Cookie数据;此外,Cookie还可能带来安全风险。因此,许多Web浏览器允许用户禁止使用Cookie功能;其结果增加了在用户计算机上存贮信息的难度。在Web服务环境下,客户端程序可以通过自己的代码而非依赖Cookie去维持状态信息;但是该方案导致客户端程序与Web服务之间紧密耦合,并导致客户端和服务端非常脆弱从而增加维持方面的问题。
在服务端维持购车车。客户端程序第一次运行时,尝试添加一些商品到购物车,服务创建一个数据结构去呈现添加后的商品。如果用户继续添加商品到购物车,服务可以计算商品总数,通过客户端程序建立支付方法使用户进行结算,然后安排这些商品的运送。在WCF环境下,通过执行在服务契约中事先定义所有客户端与服务操作;此外,客户端程序不需要知道服务是实现如何购物车的细节。
第二种方式看起来似乎更理想,但当在上述场景下构建Web服务时,你需要处理几个问题。在本章中,你将调查并解决这些问题。
管理WCF服务的状态
首先学习如何管理和维持WCF服务的状态,然后再返回到如何设置服务操作的顺序。
在前面章节中的练习都是状态无关的操作。在ProductsService服务中,所有用于执行操作的信息都是做为参数通过客户端传递给服务。当操作完成,服务随即"忘记"客户端曾经调用过该服务的方法。然后在购物车场景中,情况发生了改变;你必须在服务操作之间维持购物车的状态。在本节的练习中,你将了解到维持购物车的方式;尽管该方式相对比较简单,并且未过多考虑其可靠性和扩展性。
创建ShoppingCartService服务
1. 按照如下要求创建ShoppingCartService项目:
方案名称 |
ShoppingCart |
项目名称 |
ShoppingCartService |
位置 |
*\Step.By.Step\Chapter7 |
项目类型 |
WCF Service Library |
2. 创建好项目之后,重命名IService.cs为IShoppingCartService.cs;并且在出现的提示框中点击"允许"按钮,允许Visual Studio重命名所有的引用
3. 重命名Service.cs为ShoppingCartService.cs;并且在出现的提示框中点击"允许"按钮,允许Visual Studio重命名所有的引用
4. 添加引用ProductsEntityModel.dll
5. 添加引用System.Data.Entity; 因为ProductsEntityModel组件需要使用该组件
6. 打开IShoppingCartService.cs文件;删除所有的代码和注释,只保留using语言和命名空间
7. 添加ShoppingCartItem类到命名空间ShoppingCartService
8. 添加服务合约IShoppingCartService到命名空间ShoppingCartService下
9. 打开ShoppingCartService.cs文件,删除所有的注释和代码,仅保留using语句和命名空间
10. 添加服务实现类ShoppingCartService
11. 生成项目,并确保没有错误。
为ShoppingCartService服务创建宿主程序
1. 按照下列要求添加一个控制台应用程序
项目名称 |
ShoppingCartHost |
位置 |
*\Step.By.Step\Chapter7 |
项目类型 |
Console application |
2. 添加引用System.ServiceModel和System.Data.Entity
3. 添加app.config,并复制之前章节使用的数据库连接字符串到该app.config
4. 使用WCF服务配置工具,按照下列要求创建服务端点
服务类型 |
ShoppingCartService.ShoppingCartService |
服务合约 |
ShoppingCartService.IShoppingCartService |
通讯模式 |
HTTP |
互操作方法 |
Advanced Web Service interoperability (Simplex communication) |
端点地址 |
http://localhost:9000/ShoppingCartService/ShoppingCartService.svc |
创建服务向导的最后页面如下图所示:
5. 保存配置文件,并退出WCF服务配置管理工具
6. 打开app.config,确认system.serviceModel片段如下图所示:
7. 打开Program.cs文件,添加下面using语句
8. 修改Main方法
9. 生成项目,确认没有错误
创建客户端程序以测试ShoppingCartService服务
1. 按照下列要求创建一个控制台程序项目
项目名称 |
ShoppingCartClient |
位置 |
*\Step.By.Step\Chapter7 |
项目类型 |
Console application |
2. 添加引用System.ServiceModel
3. 使用管理员身份运行Visual Studio Command Prompt运行下列命令,使用svcutil使用工具生成ShoppingService服务的客户端代理类
4. 关闭Visual Studio Command Prompt,回到Visual Studio,然后添加上一步生成的文件ShoppingCartServcieProxy.cs到项目ShoppingCartClient
5. 添加配置文件app.config到项目ShoppingCartClient
6. 使用WCF服务配置管理工具,配置app.config;选择"Client"文件夹,然后点击右边面板中的链接"创建新的客户端"启动创建客户端元素向导。然后选择从ShoppingCartHost的配置文件中生成客户端。
然后点击"下一步"按钮
继续点击"下一步",在名字后面的文本框里,输入"WS2007HttpBinding_IShoppingCartService"
继续点击下一步,你将得到如下结果:
确认无误后,点击"完成"按钮完成客户端端点的创建;
7. 完成端点创建后,选择端点,然后更新Contract为ShoppingCartClient.ShoppingCartService.ShoppingCartService.因为根据服务的配置文件创建的客户端的端点是使用的ShoppingCartService.ShoppingCartService,这与代理类中的服务合约不匹配
8. 保存配置文件,然后退出WCF服务配置工具
9. 打开ShoppingCartClient下的app.config,确认servcie.serviceModel片段如下图所示
所以删除上述配置文件中<identity>片段内的内容,因为本章将不会配置客户端使用证书
10. 打开ShoppingCartClient项目下的Programm.cs文件,添加如下using语句
11. 完整的Programm.cs代码如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; using ShoppingCartClient.ShoppingCartService; namespace ShoppingCartClient { class Program { static void Main(string[] args) { Console.WriteLine("Press ENTER when the service has started"); Console.ReadLine(); try { ShoppingCartServiceClient proxy = new ShoppingCartServiceClient("WS2007HttpBinding_IShoppingCartService"); proxy.AddItemToCart("WB-H098"); proxy.AddItemToCart("WB-H098"); proxy.AddItemToCart("SA-M198"); string cartContents = proxy.GetShoppingCart(); Console.WriteLine(cartContents); proxy.Close(); } catch (Exception e) { Console.WriteLine("Exception: {0}", e.Message); } Console.WriteLine("Press ENTER to finish"); Console.ReadLine(); } } } |
12. 使用管理员身份执行Visual Studio Command Prompt运行下列命令:
上述命令用于确保端口9000已经被当年用户占用
13. 关闭Visual Studio Command Prompt,回到Visual Studio,设置ShoppingCartClient和ShoppingCartHost为ShoppingCart方案的启动项目(非调试)
14. 运行项目;在客户端控制台窗口中按ENTER键后,你将得到如下结果:
15. 按ENTER键退出客户端控制台应用程序;按ENTER键退出宿主控制台应用程序
从上面的练习,你可以看到,ShoppingCartServcie服务维持了购物车的信息,以供客户端在两次调用服务之间可以访问该信息;看起来,服务所维持的购物车信息正常工作。但是,上述的练习简单到令人生疑。确实,所有事物刚出现的时候都非常简单。
服务实例模式
当宿主程序创建服务实例时,服务实例创建购物车实例。 shoppingCart变量是ShoppingCartService类的一个私有实例变量。如果两个客户端同时访问该shoppingCart时,将会发生什么? 结果是每个客户端各获取一个服务实例,并且每个实例都实例化一个shoppingCart变量。 这是关键的地方。默认情况下,每个客户端第一次调用服务的操作时,宿主程序创建为请求的客户端创建一个新实例。那么该实例持续多长时间?
可以从购物车练习中看到,购物车实例在两次调用之间存在;否则,不可能在服务实例中维持该购物车变量实例的状态。客户端关闭与宿主程序的连接后,服务实例被销毁。现在考虑假如有10个并发客户端,那么将产生10个服务实例。如果你有10000个客户端,那么将产生10000个实例。如果客户端程序与服务交互,并且运行时间不确定,在该段时间内用户浏览产品目录并决定购买哪个产品。那么宿主服务的计算机需要配置大容量内存。
WCF服务实例创建后,它将负责处理来自特点客户端程序的请求,并在该客户端两次调用之间维持状态信息; 这就是一个会话。 更准确地讲,当客户端程序使用代理类连接到服务后,寄宿服务的WCF运行时创建一个会话,该会话包含服务实例和服务实例所需的状态数据。 客户端程序关闭代理类后,该会话将终止。
注意:如果你使用TCP,命名管道通信协议,你可以通过MaxConnections来指定同时连接到服务的最大连接数。如果你使用IIS使用HTTP或者HTTPS协议,那么你可以通过设置IIS的最大连接数来限制连接到服务的最大连接数。具体信息请查阅MSDN
你可以通过ServiceBehavior特性类的InstanceContextMode属性来控制客户端程序和服务实例之间的关系。你需要在服务实现类上指定该属性的值,如下图所示:
关于InstanceContextMode属性值及简单说明,请参考WCF4.0进阶系列--第二章 寄宿WCF服务;而关于ConcurrentcyMode属性值及简单说明,请看下表的内容:
OK,让我们进一步了解三种服务实例模式的细节及服务实例与并发模式之间的关系。
PerSession实例模式
PerSession模式指明当客户端程序首次调用服务的操作时,创建服务实例;一旦服务实例创建后,服务实例将保持活动状态;并响应该客户端的后续所有请求直到该客户端与服务间的连接被关闭。每当客户端程序创建一个新的会话时,该会话获取一个新的服务实例。当使用PerSession模式时,两个会话之间不能共享一个服务实例。即便这两个会话都由同一个客户端实例而创建。
客户端程序可以创建多个线程,然后尝试同时调用同一个会话中的操作。默认情况下,一个服务是单线程的因此不能处理多个并发的请求。当新的请求到达时,如果服务实例仍在处理上一个请求,WCF运行时使新请求处于等待状态直到上一个请求完成。新的请求可能会在等待处理的过程中发生超时。你可以通过ServcieBehavior特性的ConcurrentcyMode属性来指定在同一个会话内如何处理并发请求。
ConcurrentcyMode属性的默认值为ConsurrencyMode.Single;它将使服务按照上述方式运行。你还可以设置该属性的值为ConcurrentcyMode.Multiple,在这种情况下,服务实例是多线程的,可以处理并发请求。但是,设置并发模式为多线程后并不能保证自动同步。你必须确保服务端的代码是线程安全的。(即开发人员需手动同步对共享数据的所有访问)
还有一种同步模式为ConcurrentcyMode.Reentrant;这种模式下服务实例也是单线程的,但是它允许服务中的代码去调用其他服务或程序,因此你可以在服务中实现回调。但是,这种模式不能确保服务实例的状态数据。开发人员必须负责确保服务实例状态保持一致性,并且确保服务不会自我锁死。
PerCall实例模式
PerCall模式下,客户端程序每调用一个操作,都创建一个服务实例。当操作完成,该服务实例自动销毁。该模式的优点是宿主的资源在客户端调用服务的操作之间被释放,极大地提高了服务的扩展性。回想在PerSession模式下,当10000个同步用户连接至服务时,主要的问题是服务端的宿主程序将容纳10000个服务实例,即使9999个实例当前并没有执行任何操作。 如果你使用PerCall模式,此时宿主程序仅仅需要针对活动用户创建并维持一个实例。
该模式的缺点是在操作之间维持状态将面临更大的挑战。你不能在服务实例变量中维持信息,因此你必须在持久化存贮设备(硬盘或数据库)中保存任何需要的状态信息。设计服务的操作同样也是一个复杂的工作,因为客户端程序必须识别自己以使服务能从存贮设置中取回相应的状态(详细信息,请参考第八章 使用工作流实现服务)。
你可以看到服务实例的生命周期取决于服务执行客户端请求的操作所耗费的时间,因此你需要尽量保持操作的简洁性。如果一个操作创建额外的线程时,你需要格外小心;因为在这些额外的线程完成之前,服务实例将一直存在;即使主线程在这些额外线程完成之前已经将数据回传至客户端。这种情况将严重影响扩展性。你应该避免在服务中注册回调。注册回调不会阻碍服务完成,但回调的对象可能发现服务的实例已经被回收;.NET Framework CLR是导致这个问题的根本原因,这不是一个安全风险,但是这对于回调对象并不方便,因为回调对象此时将接收到一个异常。
Single实例模式
Single模式下,当客户端程序调用操作时,创建一个新的服务实例;然后使用该实例去处理来自该客户端或连接至该服务的其他客户端的所有后续请求。只有当宿主程序关闭服务时,该服务实例才被销毁。
该模式的优点是,除了减少资源的需求,所有用户还可以共享数据。但是,它同时也是该模式最重要的缺点。
Single模式使用单个相同的实例去处理所有请求,减少了服务所使用的资源。如果你有10000个并发用户,那么将会产生很多请求。同时,如果服务是单线程的,并且操作不能快速完成,那么你遇到许多超时问题。所以,你应该设置并发模式为多线程模式,并且实现同步机制以确保所有的操作时线程安全的。
实例模式和并发模式的性能影响 http://msdn.microsoft.com/zh-cn/library/cc681240.aspx
Discover Mighty Instance Management Techniques For Developing WCF Apps http://msdn.microsoft.com/en-us/magazine/cc163590.aspx
在下面的练习中,你将调查如何使用服务实例的PerCall和Single模式
调查服务行为的实例模式属性
1. 在Visual Studio中,打开ShoppingCartServcie项目下的ShoppingCartServcie.cs文件。
2. 设置InstanceContextMode属性为PerCall
3. 运行项目;在客户端窗口中按ENTER键后,你将得到如下结果:
每次客户端程序调用服务时,服务端的WCF运行时都创建一个新的服务实例。购物车对象在每次调用结束后都被销毁,因此GetShoppingCart操作返回的结果为一个空的购物车对象。
4. 按ENTER键退出客户端控制台应用程序;按ENTER键退出宿主控制台应用程序
5. 设置InstanceContextMode属性为Single
6. 再次运行项目;在客户端窗口中按ENTER键后,你将得到如下结果:
此时,看到有两个Water Bottle商品和一个Mountain seat assembly。此时,返回结果是正常的;但是真的就没问题了吗? 让我们继续实验。
7. 按ENTER键退出客户端控制台应用程序;但是保留宿主程序继续运行。
8. 转到*\Chapter7\ShoppingCart\ShoppingCartClient\bin\Debug,双击ShoppingCartClient.exe运行ShoppingCartClient
9. 启动程序后,俺ENTER键,你将得到如下结果:
第二次运行ShoppingCartClient时,仍然使用第一次运行ShoppingCartClient时所创建的服务实例,因此商品添加到同一个服务实例的购物车上。
10. ENTER键退出客户端控制台应用程序;按ENTER键退出宿主控制台应用程序
在PerCall实例模式下维持会话状态
到目前为止,本章的练习侧重点在于当你改变实例模式后,服务将发生什么。 在ShoppingCartService服务中,到底该使用哪个实例模式? 在现实环境中,你使用的不是测试代码构建的客户端,而是真正的客户端;用户在将商品放入购物车之前,花费了大量时间来浏览商品。 在这种情况下,应使用PerCall实例模式。但是,你必须提供一种机制在每次客户端程序调用一个操作时存储和重建购物车对象。有多种方法可以来完成此目标,比如当服务创建一个购物车时,为该购物车赋予一个标识;并将该标识返回给客户端;然后强制客户端程序在后续调用服务时,将该标识符作为参数传递至服务。这项技术,以及该技术的变种,被广泛采用;但使用该技术需承受服务所关注安全方面的许多缺点,比如Cookies;因为客户端可能伪造一个标识符然后劫持其他用户的购物车。
另外一种可选方案采用用户自身的标识作为保存和获取状态信息的键(值)。在一个安全环境中,该信息与客户端的请求一起传送至服务,同样该信息与服务的响应一起回传至客户端。比如,ws2007HttpBinding绑定使用Windows集成身份验证,默认情况下,将传送用户凭据至服务。在下面的练习中,我们将学习如何利用该信息。
在ShoppingCartService服务中维持会话状态
1. 打开路径*\Step.by.Step\Solutions\Chapter7目录下的解决方案ShoppingCartPerCall
2. 修改IShoppingCartService.cs;添加下图中的黄色部分
3. 修改ShoppingCartService.cs,添加下列using语句
4. 添加下面的方法SaveShoppingCart到 ShoppingCartService.cs
该私有方法获取运行客户端程序的用户名,然后基于该用户名创建一个xml文件。该用户名可能包含域名及域名和用户名的分隔符"\"。由于"\"符号不允许出现在文件名中,因此将该符号替换成"!",当然你也可以替换成其他允许的符号。
注意:如果在Internaet环境中,你使用证书而非Windows用户识别客户端用户,上述文件的文件名将依然有效。只不过文件名看起来有点不同,因为用户标识符采用了如下的形式:
CN=Bert; 64106fcaa45093f01739c82d8280c39153b5559b
在文件关闭前,使用XmlSerializer对象将用户购物车对象序列化到该文件中。
5. 添加下面的方法RestoreShoppingCart到 ShoppingCartService.cs
上述方法使用与SaveShoppingCart相同的方式来生成文件名;如果该文件存在,那么上述方法打开该文件,然后将内容反序列化为购物车对象;然后关闭该文件。如果该文件不存在,返回空对象(不能抛出异常,因为为空时允许的,比如客户端第一次访问)。
6. 修改方法AddItemToCart; 在客户端程序访问AddItemToCart时,先根据客户端用户名查找文件,然后反序列化文件内容以获取shoppingCart对象;随后再从该对象中查找详细的商品;如果添加的商品在购物车中存在,那么更新该商品的数量,然后序列化购物车对象并保存到文件中;如果添加的商品的在购物车中不存在,那么先添加该商品到购物车,然后在序列化购物车对象并保存到文件中
7. 同样,修改方法RemoveItemFromCart
8. 然后,修改方法GetShoppingCart
9. 最后,我们修改服务实现了ShoppingCartService,设置实例模式为PerCall
测试ShoppingCartService服务管理会话状态的能力
1. 运行方案;在客户端窗口中按ENTER键后,你将得到如下结果:
2. 退出ShoppingCartClient和ShoppingCartHost程序
3. 再次运行方案;你会得到如下结果:
因为状态信息已经存储在外部文件中,并且在服务重新启动和关闭间隔间持久化到文件中。
4. 按ENTER键退出ShoppingCartClient控制台程序和ShoppingCartHost控制台程序
5. 转到目录*\Step.by.Step\Solutions\Chapter7\ShoppingCartPerCall\ShoppingCartHost\bin\Debug下,你会发现名为"域名!用户名.xml"的文件
6. 打开该文件,其内容如下所示:
7. 关闭该文件,并回到Visual Studio中,修改ShoppingCartClient下的program.cs文件,使其使用Fred用户去访问ShoppingCartService服务
8. 再次运行项目,你将得到如下结果:
9. 按ENTER键退出ShoppingCartClient控制台程序和ShoppingCartHost控制台程序
10. 转到目录*\Step.by.Step\Solutions\Chapter7\ShoppingCartPerCall\ShoppingCartHost\bin\Debug下,确认"域名!Fred.xml"的文件已经生成。
上述方案在资源占用和响应方面实现平衡 。尽管每一个服务的操作都会创建新的服务实例,并且占用时间去获取和保存会话状态,但是你不需要在内存中为每个活动的客户端保留一份服务实例;因此当访问服务的用户越来越多时,该方案可以有效地提升服务的扩展性。
关于上述示例代码,有三个关键点:
- RestoreShoppingCart和SaveShoppingCart方法目前不是线程安全的。这看起来似乎不重要,因为ShoppingCartService服务使用PerCall实例模式和单线程并发模式。但是,如果同一个用户运行两个并发客户端实例,那么将产生两个并发服务实例,这两个实例都试图读和写同一个文件。NET Framework所定义的文件读取类库将阻止两个服务实例在同一个时间同时写入同一个文件,但是允许两个服饰实例相互交互。特别地,SaveShoppingCart方法可重写XML文件,因此一个服务实例可能抹去另外一个服务实例存贮的任何数据。在生产环境中,你应该采取一些步骤去阻止上述情况发生,比如使用锁或使用数据库,而不要采用XML文件。
- SaveShoppingCart方法创建了容易读懂的XML文件。在生产环境中,你应该安排将这些文件存储到一个安全位置,而不是寄宿服务程序所在问文件夹。由于保护需要隐私性,你不希望其他用户可以读取或修改这些XML文件
- 该方案依赖于经过验证的用户,而且这些用户还拥有唯一标识;用户不可以是匿名的。如果没有验证或标识,那么对于客户端用户将没有标识,其结果将导致ShoppingCartService不能为该用户生成具有唯一名字的、存储用户会话状态信息的XML文件。
你将在本章后续内容中回到上述问题,并且学习使用持续性服务和持持续性操作来解决这些问题。但是在这之前,我们需要先了解服务的一些其他特性,以及如果控制客户端执行服务操作的顺序。它们也关系到你维持状态信息方式。
选择性控制服务实例停用(失效)
服务实例模式确定服务实例的生命周期。该属性是服务的一个全局变量;你在服务实现类中设置一次,WCF运行时处理客户端请求,分配这些请求到服务的实例(也可能创建一个新的服务实例),无论客户端程序是否调用操作。
当服务实例处于非激活状态时,基于客户端所调用的操作,通过WCF运行时你可以选择性地控制服务的实例。 你可以在实现服务的每个操作上标记OperationBehavior特性。你可以使用该特性的属性ReleaseInstanceMode修改服务实例模式的行为。你可以按照下面的方式来使用OperationBehavior特性类
ReleaseInstanceMode属性还有下列值:
值 |
描述 |
AfterCall |
当操作完成时,WCF运行时将释放服务实例以回收资源。如果客户端调用另一个操作,WCF运行时将创建一个新的实例来处理该请求 |
BeforeCall |
如果当前客户端对应的服务实例已经存在,WCF运行时将释放该服务实例以回收资源,并创建一个新的服务实例处理客户端请求 |
BeforeAndAfterCall |
上述两种情况的集合;WCF运行时创建一个新的服务实例处理操作并在操作完成时释放服务实例以回收资源 |
None |
默认值。服务实例由服务实例模式管理 |
你应注意到你仅仅可以使用ReleaseInstanceMode属性减少服务实例的生命周期,并且你应理解ServiceBehavior特性类的InstanceContextMode属性和OperationBehavior特性类的ReleaseInstanceMode属性两者之间的相互作用。比如,如果你InstanceContextMode属性为PerCall,并且某一个操作的ReleaseInstanceMode属性为BeforeCall,那么WCF运行时将仍旧在操作完成后释放服务实例。 从语义上来看,InstanceContextMode.PerCall使服务实例在操作调用结束时被释放,而ReleaseInstanceMode属性不能强制WCF运行时使服务实例保持激活状态。 另一方面, 如果你指定InstanceContextMode属性为Single,并且某一个操作的leaseInstanceMode属性的值为AfterCall,那么WCF运行时将在操作结束时释放服务实例,并销毁进程中共享的资源。
OperationBehavior特性类的ReleaseInstanceMode属性经常和服务实例模式的PerSession一起使用。如果你需要创建一个服务使用Persession实例化,你应该仔细的评估是否需要在整个会话期间内一直保持一个服务实例。比如,如果你知道一个客户端程序经常在一个逻辑片段的工作结束时调用一个特定操作一个一组特定的操作,你可以考虑设置OperationBehavior特性类的ReleaseInstanceMode属性值为AfterCall
另外一个可选技术是使用用一些操作属性,使用这些属性你可以控制会话中操作的顺序。这就是下面要介绍的内容
设置WCF服务操作的顺序
使用PerSession实例模式可以方便地控制客户端程序调用服务操作的顺序。回顾ShoppingCartService服务,假设你使用Persession实例模式,那么在这种模式下,如果用户实际上并没有添加任何商品到购物车,那么允许客户端程序从购物车中移除一个商品、查询购物车中的商品、或结算操作都可能变得没有意义。实际上,ShoppingCartService服务中的操作有顺序的,而且需要按照这个顺序来执行。这个顺序就是:
- 添加一个商品到购物车
- 添加另外一个商品、移除已经添加的商品、或者查询购物车中的商品信息
- 结算并清空购物车
当你在服务合约中定义操作时,你可以使用OperationContract特性类提供的两个属性来控制操作的顺序以及操作在服务中的生命周期
- IsInitiating 如果设置该属性的值为True,客户端程序调用该操作以实例化一个新的会话并且创建一个新的服务实例。 如果会话已经存在,该属性将没有进一步的影响。默认情况下,该属性的值为True。 如果你设置该值为False,那么客户端程序不能调用该操作,直到另外一个操作已经实例化会话并且创建了服务的实例。在一个服务合约中,至少有一个操作的该属性值必须设置为True
- IsTerminating 如果你设置该值为False,WCF运行时在操作调用结束时,将中止会话并释放服务的实例。在调用该服务的另外一个操作前,客户端程序必须创建一个新的连接,而且该操作的IsInitiating属性的值必须为True。 该属性的默认值为False。如果服务中没有一个操作的IsTerminating属性值设置为True,那么会话将持续直到客户端程序关闭与该服务的连接。
WCF运行时检查这些属性的值,以确保运行时和另外一个服务合约属性SessionMode的一致性 。SessionMode用来指定服务服务是否实现Session。该属性可能含有下列值:
- Required 如果客户端对应的Session不存在,服务将创建一个Session以处理客户端的请求;否则,将该客户端将使用现存的Session。 此时,要求服务所使用的绑定必须支持Session,比如,ws2007HttpBinding绑定支持session,但是basicHttpBinding不支持。
- Allowed 服务将创建或者使用现存的Session,当然服务的绑定需要支持Session。否则,服务将不会实现Session
- NotAllowed 服务将不是使用Session,即使服务的绑定支持Session。
如果你指定任何操作的IsInitiating属性值为false,那么你必须设置Session属性的值为Required;如果你不这么做,WCF运行时将抛出一个异常,同样地,如果你设置Session模式为Required,那么你必须设置IsTerminate属性的值为True
在下面的练习中,你将学习到如何使用IsInitiating和IsTernminating属性
控制ShoppingCartService服务操作的顺序
1. 打开空白解决方案ShoppingCartWithSequence;(该方案复制了上一个方案ShoppingCartPerCall),然后打开ShoppingCartService项目下的IShoppingCartService.cs文件
2. 指定IShoppingCartService服务SessionMode值为SessionMode.Required; 并按照下列所示指定该服务操作的IsInitiating属性和IsTerminating属性
3. 打开ShoppingCartService.cs文件,并指定服务实现类的InstranceContextMode值为InstanceContextMode.PerSession
4. 注释掉ShoppingCartService.cs文件中AddItemToCart方法里的RestoreShoppingCart方法和SaveShoppingCart方法
5. 注释掉ShoppingCartService.cs文件中RemoveItemFromCart方法里的RestoreShoppingCart方法和SaveShoppingCart方法
7. 注释掉ShoppingCartService.cs文件中GetShoppingCart方法里的RestoreShoppingCart
现在你需要修改客户端以测试上述修改产出的结果。
测试ShoppingCartService服务中操作的顺序
1. 打开ShoppingCartClient项目下的ShoppingCartServiceProxy.cs。该文件由之前版本的服务生成。 由于你已经修改了服务合约,因此你必须更新客户端代理类以反映服务合约的更新。你可以继续使用svcutil使用工具重新生成客户端代理类;但是由于服务合约的修改比较少,因为我们完全可以通过手动方式修改服务代理类。所需要做的更新是为服务代理类指定SessionMode,以及为操作添加IsInitiating和IsTerminating属性;
2. 修改ShoppingCartClient项目下的Program.cs文件;添加下面图片中红色矩形内的内容:
3. 在非调适模式下运行方案。在ShoppingCartClient控制台窗口中,按ENTER键。 客户端程序添加三个商品到购物车然后输出购物车的内容。然后将输出错误消息
上述结果证实了第一次调用Checkout操作后,终止了客户端与服务的会话。此外,当会话结束时,服务关闭了与客户端程序的连接。 因此,客户端程序与服务再次通信之前必须重新打开一个新的连接并创建一个新的会话。
4. 在ShoppingCartClient控制台窗口中,按ENTER键以退出控制台程序;在ShoppingCartHost控制台窗口中按ENTER键退出宿主控制台程序。
5. 在步骤二的方法前重新创建ShoppingCartServiceClient对象;
proxy = new
ShoppingCartServiceClient("WS2007HttpBinding_IShoppingCartService");
6. 再次运行方案,你将得到如下的结果:
7. 在ShoppingCartClient控制台窗口中,按ENTER键以退出控制台程序;在ShoppingCartHost控制台窗口中按ENTER键退出宿主控制台程序。
你还可以自己尝试在调用AddItemToCart之前,调用RemoveItemFromCart操作、GetShoppingCart或Checkout操作。 由于后面三个操作都不创建一个新的会话,因为客户端程序都将失败,并抛出异常。如下图所示
绑定和Sessions之间的关系
你应该意识到sessions需要连接至服务所采用的协议支持。并非所有协议都提供了session的支持;相应地并非所有的标准绑定都支持session。 你如你尝试在不支持session的绑定上配置服务支持session,服务启动将会失败。 下面的列表列出了标准绑定对session的支持情况
绑定 |
支持Sessions |
BasicHttpBinding |
No |
BasicHttpContextBinding |
No |
WSHttpBinding |
Yes |
WSHttpContextBinding |
Yes |
WS2007Binding |
Yes |
WSDualHttpBinding |
Yes |
WebHttpBinding |
No |
WSFederationHttpBinding |
Yes |
WS2007FederationHttpBinding |
Yes |
NetTcpBinding |
Yes |
NetTcpContextBinding |
Yes |
NetPeerTcpBinding |
No |
NetNamePipeBinding |
No |
NetMsmqBinding |
No |
MsmqIntegrationBinding |
No |
使用持续性服务维持会话状态
在本章的前部分"PerCall实例模式中维持状态",你已经了解到在如何通过使用代码序列化和反序列化状态信息的方式提供一个session-less服务。你还了解到如果采用这种实现方式提供更专业的程序所面临的问题。这些都并非小问题。幸运的是,WCF提供持续性服务和持续性操作可以帮助你解决上述问题。
WCF的持续性服务指使用会话维持状态。你可以中断、甚至停止一个会话, 在保存会话状态的同时释放服务实例所使用的资源。然后,你可以启动一个新的服务实例,恢复一个会话,并且重新装载会话的状态到该新服务实例。持续性服务对于长时间运行的会话是完美地,客户端程序激活一段时间后可以停用;非激活的客户端所使用会话和资源可以替换出去、当被激活时再重新装载回来。用于构建可扩展性WCF服务的工作流模型就是基于持续性服务(你将在第八章看到相关内容);如果你在企业环境中寄宿WCF服务,那么Windows Server AppFabric也依赖于持续性服务维持状态。当然,你也可以在上述场景之外使用持续性服务。在后面的练习中你将研究如何使用。
你通过DurableService特性来指定一个服务是持续性的。 持续性服务要求一个数据存储来持久化该服务所有会话的状态。WCF和WF提供SQL持久化程序在SQL数据库中存储会话状态。如果你想使用一些其他的机制你也可以构建自己的持久化程序。你配置一个持续性服务引用一个持久化程序,并提供连接到所采用持久化程序存储的详细信息。
启动一个服务实例时将创建一个新的会员,WCF运行时为该会话生成一个唯一实例ID。 该实例ID存贮在SOAP消息的头部,并在客户端和服务端之间来回传输,WCF运行时使用该实例ID关联客户端请求和对应的服务实例。当客户端关闭与服务的连接后,WCF运行时存储会话的状态和实例ID到持久化存储。 然后,如果客户端程序重新连接到服务,将早期会话对应的实例ID填充到请求消息的头部,然后传送至WCF服务。服务端的WCF运行时创建一个新的会话,从持久存储中查询会话状态,然后将这些信息填充到新的会话中。 对于客户端程序,新会话与旧会话完全一致。
与持续性服务一样,你可以指定持续性操作以使服务保存和恢复会话状态。 你通过DurableOperation特性来实现持续性操作。该特性类提供与OperationBehavios特性类中IsInitiating和IsTerminating属性相似的功能。你可以设置属性CanCreateInstance的值为true,以指定WCF运行时在操作被调用时创建一个新的持续性服务实例。 你也可是这是该属性值为false以标明服务仅仅通过已存的实例运行。 此外,你可以设置CompleteInstance实行为true以标明操作结束当前的session,任何保存在持久存储的状态当操作完成时应该被移除。
在下面的练习中,你将修改ShoppingCartService为持续性服务,并检查如何通过一个简单的图形化客户端与该持续性服务交互
检查ShoppingCartGUIClient程序
1. 打开*\Step.by.Step\Chapter7\DurableShoppingCartService文件夹下的DurableShoppingCartService方案
2. 设置ShoppingCartHost和ShoppingCartGUIClient为该方案的启动项目
3. 在非调适模式下启动方案,然后切换ShoppingCartGUIClient客户端程序的窗口,在产品编码处输入WB-H098;然后点击"添加商品"按钮,你将看到这项目将被添加到购物车并显示在窗口的下方
4. 再次点击击"添加商品"按钮,确保购物车中商品数量发生变化。
5. 在产品编码出输入"SA-M198";然后点击"添加商品";确认该商品添加到购物车中并正确地显示
6. 点击"删除商品",确认商品从购物车中移除。
7. 点击"结算",确认购物车中的商品被清空。
8. 关闭ShoppingCartGUIClient;并推出ShoppingCartHost程序
9. 打开MainWindow.xaml.cs,你可以看到ShoppingCartGUIClient程序的逻辑
public partial class MainWindow : Window { private ShoppingCartServiceClient proxy = null; public MainWindow() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { proxy = new ShoppingCartServiceClient("WS2007HttpBinding_IShoppingCartService"); } private void Window_Unloaded(object sender, RoutedEventArgs e) { proxy.Close(); } private void btnAddItem_Click(object sender, RoutedEventArgs e) { try { proxy.AddItemToCart(txtProductNumber.Text); txtShoppingCartContents.Text = proxy.GetShoppingCart(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Error adding item to cart", MessageBoxButton.OK, MessageBoxImage.Error); } } private void btmRemoveItem_Click(object sender, RoutedEventArgs e) { try { proxy.RemoveItemFromCart(txtProductNumber.Text); txtShoppingCartContents.Text = proxy.GetShoppingCart(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Error removing item to cart", MessageBoxButton.OK, MessageBoxImage.Error); } } private void btnCheckout_Click(object sender, RoutedEventArgs e) { try { proxy.CheckOut(); txtShoppingCartContents.Clear(); } catch (Exception ex) { MessageBox.Show(ex.Message, "Error checking out", MessageBoxButton.OK, MessageBoxImage.Error); } } } |
该客户端程序非常简单,但是该程序的实用性很差;因为借助该程序并不能构建专业的、可扩展的系统。问题在于当用户启动程序并打开窗口时,建立到服务的连接然后创建会话;该连接和绘画将一直驻留在宿主计算机的内存中,直到用户关闭窗口停止客户端。如果用户忘记关闭客户端程序,然后离开办公室去度两个星期假;那么会话所使用的资源在这段时间内将一直保留。此外,如果服务在这段时间内关闭,那么会话的状态信息将丢失。在下面的练习者中,你将为持续性服务创建持久存储。
使用SQL持久化程序为WCF服务创建持久存储
1. 启动SQL Server管理工具;连接到本机SQL Server实例
2. 创建一个新的数据库WCFPersistence
3. 然后连接到该数据库,打开文件C:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en\SqlPersistenceProviderSchema.sql;切换到数据库WCFPersistence,然后执行该文件。
4. 确认文件执行成功
5. 重复步骤3和步骤4,执行文件C:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en\SqlPersistenceProviderLogic.sql
重新配置ShoppingCartService服务为持续性服务
1.在解决方案中,在ShoppingCartService项目上点击右键,选择ShoppingCartService属性
2. 在属性页中,选择"Application"标签,然后设置Target framework为.NET Framework 4
3. 添加System.WorkflowServices引用到ShoppingCartService项目。该组件包含DurableService和DurableOperation特性类
4. 打开IShoppingCartService.cs文件,确认ShoppingCartItem类标记了Serializable特性
5. 打开ShoppingCartService.cs文件,添加using语句
6. 确认ShoppingCartService类的InstanceContextModel属性为PerSession。
7. 添加Serializable和DurableService特性到ShoppingCartService类
8. 添加DurableOperaton特性到ShopingCartService服务的操作上
9. 在ShoppingCartHost项目上点击右键,选择ShoppingCartService属性;在属性页中,选择"Application"标签,然后设置Target framework为.NET Framework 4
10. 添加连接到WCFPersistence的数据库连接字符串
<add name="DurableServiceConnectionService" connectionString="metadata=res://*/ProductsModel.csdl|res://*/ProductsModel.ssdl|res://*/ProductsModel.msl;provider=System.Data.SqlClient;provider connection string="data source=localhost;initial catalog=WCFPersistence;integrated security=True; multipleactiveresultsets=True;App=EntityFramework"" providerName="System.Data.EntityClient"/> |
11. 保存app.config;然后使用WCF服务配置工具打开该app.config
12. 在WCF服务配置工具左边面板中,选择高级—服务行为,然后右键,选择"创建新的服务行为配置"
13. 上述操作将会创建一个为命名的服务行为,你更新其名字为"DurableServiceBehavior",然后在右下面板中点击"添加"按钮,添加服务行为元素"persistenceProvider"
14. 在WCF服务配置左边面板中,选择"高级—服务行为—DurableServiceBahavior--persistenceProvider",然后切换到右边面板。设置persistentenceProvider的类型为System.ServiceModel.Persistence.SqlPersistenceProvider
15. 完成上述步骤后,选择选择"高级—服务行为—DurableServiceBahavior—persistenceProvider-- persistenceProviderArgument",然后在WCF服务配置工具的右下角,点击"创建"按钮,在弹出的对话框中的名字出输入"connectionStringName";值处输入"DurableServiceConnectionService"。然后点击"确定"按钮
16. 选择"服务—ShoppingCartService.ShoppingCartService",在右边面板中选择该服务的行为配置:
17. 在WCF服务配置工具中,选择"服务—端点—空名",在右边面板中更改绑定为wsHttpContextBinding
如前文所述,持续性服务生成的实例ID在包含SOAP消息的头部中并在客户端和服务之间交换。服务所使用的协议必须自动地填充和检查该信息;wsHttpContextBinding绑定提供了该功能。
18. 保存配置文件,并退出WCF服务配置工具。
更新ShoppingCartGUIClient程序
1. 打开ShoppingCartGUIClient程序的app.config文件,修改binding为"wsHttpContext"
2. 打开MainWindow.xaml.cs文件,注释掉Window_Loaded和Window_Unloaded方法内的内容
3. 添加using语句
4. 在MainWindow类中,添加一个如下的私有变量
private IDictionary<string, string> context = null;
5. 修改btnAddItem_Click的逻辑
private void btnAddItem_Click(object sender, RoutedEventArgs e) { try { using (proxy = new ShoppingCartServiceClient("WS2007HttpBinding_IShoppingCartService")) { IContextManager contextManager = proxy.InnerChannel.GetProperty<IContextManager>(); if (context != null) contextManager.SetContext(context); proxy.AddItemToCart(txtProductNumber.Text); if (context == null) { context = contextManager.GetContext(); MessageBox.Show(context["instanceId"], "New context created", MessageBoxButton.OK, MessageBoxImage.Information); } txtShoppingCartContents.Text = proxy.GetShoppingCart(); } } catch (Exception ex) { MessageBox.Show(ex.Message, "Error adding item to cart", MessageBoxButton.OK, MessageBoxImage.Error); } } |
上述代码有如下需要注意的几点:
- 与服务通信代理对象的创建方法
- 创建代理对象所使用端点的名字
- 用于读取代理对象收发SOAP的头部信息的IContextManager对象的创建方法
- 检查context变量的方法
- 在调用AddItemToCarth操作后如果context变量为空,如果存储头部信息到SQL持久化存储中,从context中获取服务返回的SOAP消息头的代码
6. 修改btmRemoveItem_Click与btnCheckout_Click逻辑;
private void btmRemoveItem_Click(object sender, RoutedEventArgs e) { try { using (proxy = new ShoppingCartServiceClient("WS2007HttpBinding_IShoppingCartService")) { IContextManager contextManager = proxy.InnerChannel.GetProperty<IContextManager>(); contextManager.SetContext(context); proxy.RemoveItemFromCart(txtProductNumber.Text); txtShoppingCartContents.Text = proxy.GetShoppingCart(); } } catch (Exception ex) { MessageBox.Show(ex.Message, "Error removing item to cart", MessageBoxButton.OK, MessageBoxImage.Error); } }
|
private void btnCheckout_Click(object sender, RoutedEventArgs e) { try { using (proxy = new ShoppingCartServiceClient("WS2007HttpBinding_IShoppingCartService")) { IContextManager contextManager = proxy.InnerChannel.GetProperty<IContextManager>(); contextManager.SetContext(context); proxy.CheckOut(); txtShoppingCartContents.Clear(); } } catch (Exception ex) { MessageBox.Show(ex.Message, "Error checking out", MessageBoxButton.OK, MessageBoxImage.Error); } } |
测试持续性服务
1. 运行方案DurableShoppingCartService;在ShoppingCartGUIClient窗口中,在产品编码处输入WB-H098;然后点击添加商品按钮。下图所示的消息框窗出现。该消息框显示了持续性服务创建的会话的实例的ID。
2. 点击OK键后,确认产品已经添加到购物车中
3. 保持ShoppingCartGUIClient和ShoppingCartHost程序处于运行状态;然后转到SQL客户端管理工具;执行下面的T-SQL语句,你将会得到一条纪录。该记录包含一个id的列,其值为步骤一里消息提示框的实例ID的值。
4. 返回到ShoppingCartGUIClient窗口中,在产品编码处输入SA-M198;然后点击"添加商品"按钮。此时,将不会出现消息提示框。因为btnAddItem_Click确认context变量已经有值存在,所以一个会话必定已经创建。Context被传送至ShoppingCartService服务。服务端的WCF运行时创建一个新的服务实例,并从context中提取实例ID,从WCFPersistence数据库的InstanceData表中获取该实例ID对应的会话数据,并且将这些数据自动填充到新创建的服务实例中。 当btnAddItem_Click方法结束, 服务端的WCF运行时保存会话数据到InstanceData表中,然后再销毁服务实例。
4. 保持ShoppingCartGUIClient处于运行状态,关闭ShoppingCartHost控制台程序。
5. 重新启动ShoppingCartHost控制台程序
6. 切换回ShoppingCartGUIClient窗口,在产品编码处然后输入PU-M044,然后点击"添加商品"按钮。Mountain Pump商品添加到购物车中。请注意WCF运行时恢复了之前购物车中已经存在的商品,尽管ShoppingCartService宿主程序已经关闭并重新启动。
7. 点击"结算"按钮。该操作结束会话,购物车也被情况
8. 保持ShoppingCartGUIClient和ShoppingCartHost程序处于运行状态;然后转到SQL客户端管理工具;执行下面的T-SQL语句。 由于结算操作已经完成,会话终止,保存在持久化存储中的数据也已经被移除。
9. 关闭ShoppingCartGUIClient程序和ShoppingCartHost控制台应用程序
总结
在本章,你了解到WCF运行时为创建服务实例提供了不同的选择。在单个操作或者整个会话期间内,服务实例可以一直存在,直到客户端程序关闭与服务之间的连接。在很多情况下,服务实例为某一个特定的客户端专有,但是WCF也提供供多个客户端实例共享单个服务实例。 你还了解到如何选择性地控制哪一个操作创建会话,哪一个操作关闭会话。 最后,你了解到如何创建持续性服务,使用该服务,你可以维持会话状态,而不需要对应的服务实例一直处于活动状态。 持续性服务是构建会话时间长、 可以从关闭的服务中恢复会话信息、或者重启服务并恢复会话的完美解决方案。