1.3 [2006-11-12]
本教程在《NBearV3 Step by Step教程——IoC篇》的基础上,演示如何基于NBearV3的IoC模块开发一个分布式Web应用程序的过程。您将看到,基于NBear的IoC组件,开发分布式系统就和开发单服务器系统一样容易。本教程同时将引导您注意分布式开发和非分布式开发,在实体定义中的注意事项。
注1:NBearV3提供的分布式支持,从用户视角来说,只要按照《NBearV3 Step by Step教程——IoC篇》的方式,以定义本地服务接口和实现相同的方法定义和实现服务接口,再进行一定的配置和部署,就能在不修改代码,甚至不需重新编译的情况下,使应用程序轻松具有分布式能力,并可以以Service为单位进行多服务器分布部署,且能够由ServiceMQ Server控制,自动实现负载均衡。在NBear封装的逻辑内部,是以ServiceMQ Server为消息中心,基于.Net Remoting进行消息传递,并使用Castle作为IoC容器实现的。
注2:在阅读本文之前,建议读者先阅读《NBearV3 Step by Step教程——IoC篇》以掌握NBearV3中有关ORM和IoC的基本知识。
通过本教程,读者应能够全面掌握使用NBearV3的IoC模块开发单服务器/分布式应用程序的全过程。
本教程演示创建的所有工程和代码,包含于可以从sf.net下载的NBearV3最新源码zip包中的tutorials\IoC_Adv_Tutorial目录中。因此,在使用本教程的过程中如有任何疑问,可以直接参考这些代码。
<30分钟。
1.1访问http://sf.net/projects/nbear,下载NBearV3的最新版本到本地目录。
1.2 将下载的zip文件解压至C:\,您将看到,加压后的NBearV3目录中包括:dist、doc、cases、src、tutorials等目录。其中,在本教程中将会使用的是dist目录中的所有release编译版本的dll和exe和tutorials目录中之前的IoC基础教程。
1.3 将tutorials目录中的整个IoC_Tutorial目录复制到任意其它位置,并命名为IoC_Adv_Tutorial,我们将以IoC_Tutorial为基础,演示NBearV3中基于IoC的分布式开发的知识。
2.1 将IoC_Adv_Tutorial中的IoC_Tutorial.sln重命名为IoC_Adv_Tutorial.sln,并在VS2005开发环境中打开。
2.2在本教程中,对于从IoC_Tutorial继承过来的这些工程,我们会做很小的一些修改,您将注意到,我们做这些修改的原因,并不意味着,一个非分布式系统必须做经过修改才能以分布方式部署。而是,我们将引导您注意,在基于NBear的分布式系统中,实体定义和Service接口设计的重要注意事项。
2.3 首先,需要注意一个在分布系统中的实体设计规范:两个实体或者多个实体间,要避免双向/循环可读写、可序列化的引用。
具体举例来说,如果您打开EntityDesigns中的EntityDesigns.cs文件,您将注意到,Category和Product,互相包含了可读写的引用。这会有什么问题呢?在非分布式系统中,只要两个引用不同时是LazyLoad=false,这就完全没问题,您在IoC Tutorial中已经看到了,程序运行得很正常。但是,在分布式情况下,因为,Service的中的方法的参数和返回值,会被序列化后,以消息的形式进行传递。所以,以这里的Category和Product为例,假如我有一个Product的实例,现在我把它序列化,此时会发生什么呢?他的属性Category也会被序列化,序列化这个Category属性时,又会发生什么呢?他的Products属性也要被序列化!!问题来了,我们最初的Product实例,肯定也包含在他的Category属性的Products中,所有又会被序列化。。。这样就死循环了。
怎么办呢?办法很简单,至少将一个引用设为只读(设为只有get没有set)或不可序列化(为属性标注SerializationIgnoreAttribute)。在这个Category和Product的关系中,比较合理的是将Category.Products属性设为只读,代码如下:
此时,序列化Category时,就不会序列化他的Products,从而就能避免序列化的死循环。也就能正常用于分布式系统了。
3.1 至此,所有的实体的设计就修改就完毕了。编译EntityDesigns工程。
3.2 运行dist目录中的NBear.Tools.EntityDesignToEntity.exe工具,载入EntityDesigns工程编译生成的EntityDesigns.dll。
3.3 点击Generate Entities按钮,将生成的代码保存到Entities工程中的一个名叫Entities.cs的代码文件。
3.4 点击Generate Configuration按钮,将生成的代码保存到website工程下的名为EntityConfig.xml的文件中。
4.1 在将测试程序部署为分布式系统之前,我们先验证一下程序运行正常。将website设为启动工程,并设置Default.aspx为启动页。运行website,看看,Default.aspx是否正常显示了和IoC_Tutorial中完全相同的运行结果。
4.2 为了更方便测试,我们在IoC_Adv_Tutorial目录中建一个Bin目录,Bin目录中建立ServiceMQServer目录和ServiceHost目录。新建如下的脚本文UpdateAssemblies.bat,用来更新所有需要的dll和exe到两个目录下:
@echo off
copy ..\website\EntityConfig.xml .\ServiceHost\ /Y
copy ..\Entities\bin\Debug\*.* .\ServiceHost\ /Y
copy ..\ServiceImpls\bin\Debug\*.* .\ServiceHost\ /Y
copy ..\ServiceInterfaces\bin\Debug\*.* .\ServiceHost\ /Y
copy ..\..\..\dist\NBear.IoC.Servers.ServiceMQServer.exe .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.Common.dll .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.IoC.dll .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.Net.dll .\ServiceMQServer\ /Y
copy ..\..\..\dist\NBear.IoC.Hosts.ServiceHost.exe .\ServiceHost\ /Y
4.3 执行4.2所见的脚本,复制相关程序集到这两个目录。
4.4 在ServiceMQServer目录中,我们看到,除了NBear.*.dll之外,只有一个文件NBear.IoC.Servers.ServiceMQServer.exe。这个文件是NBear提供的,从dist目录复制过来的。我们需要为它创建如下的NBear.IoC.Servers.ServiceMQServer.exe.config文件:
以上的配置,指定了允许连接到该Server的ServiceFactory的配置信息。其中参数含义分别为:
· type - ServiceFactory的类型是Remoting,默认情况下,ServiceFactory的类型总是Local的,所以不能连接远程ServiceMQServer。
· name – 用于连接ServiceMQServer的唯一名称,该名称不能包含空格。
· protocol - ServiceFactory连接ServiceMQServer的协议,可选值为HTTP或TCP。
· server和port – ServiceMQServer监听的服务器地址和端口。
· debug - 是否在ServiceMQServer中显示调试日置信息。
· maxTry - 对于同一个消息的等待读取的最大次数。
4.5 我们再切换到ServiceHost目录。该目录下包含了用于部署Service的程序集。我们可以看到,有ServiceInterfaces.dll,ServiceImpls.dll,Entities.dll,相关的NBear和Castke程序集,和NBear.IoC.Hosts.ServiceHost.exe。最后这个程序也是有NBear提供,从dist复制过来的。我们需要为它创建如下的NBear.IoC.Hosts.ServiceHost.exe.config文件:
我们可以注意到,配置文件中除了包含对ServiceFactory的配置,参数的含义和4.4中的config含义完全一样。另外,这里也包含了我们从IoC_Tutorial中复制过来的website中的Web.config中类似的entityConfig、ConnectionString和castke配置节。之所以要配置这些信息,是因为,我们的ServiceHost将作为Service的宿主,接受对他支持的service的访问请求,要读取实体信息,也需要访问数据库。
4.6 接着,为了让website能够访问远程Service,我们需要为website的Web.config添加serviceFactory配置节,同时为,为了演示同时存在本地Service和远程Service的情形,我们保留castle配置节中的category service。修改完的Web.config内容如下:
4.6 如果您想在多个服务器上测试本程序,您可以分别将ServiceMQServer和ServiceHost目录中的内容复制到不同的服务器。但是,需要注意修改所有的config中的server地址修改为ServiceMQServer所在的服务器地址。
当然,如果只是想先看看运行效果,你也可以直接在本机运行。
5.1 现在我们就可以运行整个程序了。我们首先必须先运行ServiceMQServer.exe。
5.2 接着,我们运行两个ServiceHost.exe实例(如果您愿意,也可以运行更多)。您将能看到,在ServiceMQServer.exe的窗口中,会显示,分别由两个ICategoryService和IProductService的订阅者。他们自然是我们的ServiceHost向ServiceMQServer订阅的。
5.3 运行website,并访问Default.aspx,你将能看到website的运行结果应该和没有部署为分布式程序之前的结果实完全一样的。您可以刷新几次页面,并注意ServiceMQServer和ServiceHost的窗口。
您将能看到,对IProductService的请求,会被自动发送给两个ServiceHost中的一个来处理并返回,但是,你看不到ICategoryService被处理的日志。为什么呢?因为,我们在website的Web.config中的castle块中保留了本地的category service组件定义。在Default.aspx请求某个Service时,如果,ServiceFactory发现有本地实现,则会直接返回本地Service实现的实例,如果找不到本地实现,则会向ServiceMQServer发送Service调用请求,ServiceMQServer,则将把对Service的调用请求负载均衡地,转发给注册到它的ServiceHost。所以,在多刷新几次页面的时候,您将注意到,有时,请求是被一个ServiceHost处理的,有时,请求被另一个处理。
如果你将Web.config中的category service那个component注释掉,再次刷新Default.aspx页面,则您将能看到,对category service调用,也会被发送给ServiceHost处理。
但是,注意,此时,Category.Products总是返回null。为什么呢?因为,我们在2.3中将Category.Products属性设为只读了。只读属性是不会被序列化的,所以Products不会被传递到远程。
正文结束。
有朋友问,实体被分布式的传递到远程后,LazyLoad=true的属性被访问时,会是什么行为呢?
实际上,请注意一个事实——那就是实体或实体数组总是在序列化后,才被发送到远程的,所以,至少,serializer会访问一次被序列化的实体的可读写属性,也因此,被接收到的远程实体的属性,即使是LazyLoad=true的属性,它们的内容其实已经被Load过了,如果在访问这样的属性,只是简单的返回已载入的数据。
相信更多朋友对ServiceFactory如何返回远程Service访问代理,以及访问代理的内部原理非常感兴趣。
限于篇幅,我这里只是简单介绍一下一个Service调用处理过程——从调用端发出调用请求,到请求端收到处理结果的过程。
当Default发出一个Service调用请求,如IProductService.GetAllProducts()之前,它首先要从ServiceFactory.GetService<>()得到一个IProductService的实现类。ServiceFactory首先判断是否有一个本地Service实现注册在自己的Web.config中,如果有,对于本地Service实现组件而言,ServiceFactory简单的返回一个新建的实现类的实例。
当ServiceFactory找不到本地Service实现时,他将在内存中,使用System.Reflection.Emit技术,动态创建一个实现了IProductService的代理类(第一次创建后会缓存起来),这个代理类封装了对ServiceMQServer的访问功能。调用一个代理类的方法的过程为:代理类奖输入参数序列化,并封装为一个RequestMessage,发送给ServiceMQServer,并定时查询ServiceMQServer是否已经处理完毕;ServiceMQServer接到RequestMessage,则负载均衡地将RequestMessage转发给注册到它的某一个能够处理IProductService的ServiceHost;ServiceHost接到ServiceMQServer的通知时,执行Service逻辑,并将结果返还给ServiceMQServer;此时,代理类发现Service请求已经处理完毕;它就将结果取回来,返回给调用者。
默认情况下,在分布式Service的方法的参数或返回值都会被序列化为XML。有一些情形下,我们需要自定义序列化方式,或者,还有一些情况下,某些参数会返回值类型默认不能被序列化,比如接口类型,那么,这些情况下,我们都需要为这些类型定义自定义序列化/反序列化逻辑。
我们可以使用NBear.Common.SerializationManager类来自定义特定类型的序列化方式。一般,我们可以在应用程序启动的时候,如Web应用程序的Application_Start中,调用SerializationManager.RegisterSerializeHandler()/UnregisterSerializeHandler()方法注册和注销对特定类型的自定义序列化和反序列化方法。
NBear默认提供的ServiceMQServer和ServiceHost都是非常简单的控制台程序实现,它们的有效源码分别都不到10行。它们对于日志也只是简单的显示出来。在现实的开发中,这往往会不能满足我们的需求,此时,我们可以参照这两个程序的源码,实现您自己的ServiceMQServer和ServiceHost。比如,我们可以将它们写成Windows Service,或者Windows Form程序。
//本文结束