版本
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分鐘。
正文
Step 1 下載NBearV3最新版本及准備
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的分布式開發的知識。
Step 2 擴展設計實體及元數據
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屬性設為只讀,代碼如下:
[MappingName("Categories")]
public interface Category : Entity
{
[PrimaryKey]
int CategoryID { get; }
[SqlType("nvarchar(15)")]
string CategoryName { get; set; }
[SqlType("ntext")]
string Description { get; set; }
byte[] Picture { get; set; }
[FkQuery("Category", OrderBy = "{ProductName}", Contained = true, LazyLoad = true)]
[SerializationIgnore]
Product[] Products
{
get;
set;
}
}
此時,序列化Category時,就不會序列化他的Products,從而就能避免序列化的死循環。也就能正常用於分布式系統了。
Step 3 從實體設計代碼生成實體代碼、實體配置文件
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的文件中。
Step 4 使用ServiceMQServer.exe和ServiceHost.exe,部署程序為分布式系統
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文件:
<?xml version="1.0" encoding="utf-8" ?>
以上的配置,指定了允許連接到該Server的ServiceFactory的配置信息。其中參數含義分別為:
<configuration>
<configSections>
<section name="serviceFactory" type="NBear.IoC.Service.Configuration.ServiceFactoryConfigurationSection, NBear.IoC" />
</configSections>
<serviceFactory type="Remoting" name="testServiceFactory" protocol="HTTP" server="127.0.0.1" port="8888" debug="true" maxTry="30" />
</configuration>
· 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文件:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
<section name="serviceFactory" type="NBear.IoC.Service.Configuration.ServiceFactoryConfigurationSection, NBear.IoC" />
<section name="entityConfig" type="NBear.Common.EntityConfigurationSection, NBear.Common"/>
</configSections>
<entityConfig>
<includes>
<add key="Sample Entity Config" value="~/EntityConfig.xml"/>
</includes>
</entityConfig>
<castle>
<components>
<!--You can use standard castle component decleration schema to define service interface impls here-->
<component id="category service" service="ServiceInterfaces.ICategoryService, ServiceInterfaces" type="ServiceImpls.CategoryService, ServiceImpls"/>
<component id="product service" service="ServiceInterfaces.IProductService, ServiceInterfaces" type="ServiceImpls.ProductService, ServiceImpls"/>
</components>
</castle>
<serviceFactory type="Remoting" name="testServiceFactory" protocol="HTTP" server="127.0.0.1" port="8888" debug="true" maxTry="30" />
<connectionStrings>
<add name="Northwind" connectionString="Server=(local);Database=Northwind;Uid=sa;Pwd=sa" providerName="NBear.Data.SqlServer.SqlDbProvider"/>
</connectionStrings>
</configuration>
我們可以注意到,配置文件中除了包含對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內容如下:
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="serviceFactory" type="NBear.IoC.Service.Configuration.ServiceFactoryConfigurationSection, NBear.IoC" />
<section name="entityConfig" type="NBear.Common.EntityConfigurationSection, NBear.Common"/>
<section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
</configSections>
<entityConfig>
<includes>
<add key="Sample Entity Config" value="~/EntityConfig.xml"/>
</includes>
</entityConfig>
<castle>
<components>
<!--You can use standard castle component decleration schema to define service interface impls here-->
<component id="category service" service="ServiceInterfaces.ICategoryService, ServiceInterfaces" type="ServiceImpls.CategoryService, ServiceImpls"/>
</components>
</castle>
<serviceFactory type="Remoting" name="testServiceFactory" protocol="HTTP" server="127.0.0.1" port="8888" debug="true" maxTry="30" />
<appSettings/>
<connectionStrings>
<add name="Northwind" connectionString="Server=(local);Database=Northwind;Uid=sa;Pwd=sa" providerName="NBear.Data.SqlServer.SqlDbProvider"/>
</connectionStrings>
<system.web>
<compilation debug="true">
<assemblies>
<add assembly="System.Transactions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Runtime.Remoting, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/></assemblies></compilation>
<authentication mode="Windows"/>
</system.web>
</configuration>
4.6 如果您想在多個服務器上測試本程序,您可以分別將ServiceMQServer和ServiceHost目錄中的內容復制到不同的服務器。但是,需要注意修改所有的config中的server地址修改為ServiceMQServer所在的服務器地址。
當然,如果只是想先看看運行效果,你也可以直接在本機運行。
Step 5 運行分布式程序
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不會被傳遞到遠程。
正文結束。
附錄
1 關於分布式系統中LazyLoad=true的屬性
有朋友問,實體被分布式的傳遞到遠程後,LazyLoad=true的屬性被訪問時,會是什麼行為呢?
實際上,請注意一個事實——那就是實體或實體數組總是在序列化後,才被發送到遠程的,所以,至少,serializer會訪問一次被序列化的實體的可讀寫屬性,也因此,被接收到的遠程實體的屬性,即使是LazyLoad=true的屬性,它們的內容其實已經被Load過了,如果在訪問這樣的屬性,只是簡單的返回已載入的數據。
2 關於ServiceFactory返回遠程Service訪問代理的內部原理
相信更多朋友對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請求已經處理完畢;它就將結果取回來,返回給調用者。
3 關於自定義序列化邏輯
默認情況下,在分布式Service的方法的參數或返回值都會被序列化為XML。有一些情形下,我們需要自定義序列化方式,或者,還有一些情況下,某些參數會返回值類型默認不能被序列化,比如接口類型,那麼,這些情況下,我們都需要為這些類型定義自定義序列化/反序列化邏輯。
我們可以使用NBear.Common.SerializationManager類來自定義特定類型的序列化方式。一般,我們可以在應用程序啟動的時候,如Web應用程序的Application_Start中,調用SerializationManager.RegisterSerializeHandler()/UnregisterSerializeHandler()方法注冊和注銷對特定類型的自定義序列化和反序列化方法。
4 關於自定義ServiceMQServer和ServiceHost
NBear默認提供的ServiceMQServer和ServiceHost都是非常簡單的控制台程序實現,它們的有效源碼分別都不到10行。它們對於日志也只是簡單的顯示出來。在現實的開發中,這往往會不能滿足我們的需求,此時,我們可以參照這兩個程序的源碼,實現您自己的ServiceMQServer和ServiceHost。比如,我們可以將它們寫成Windows Service,或者Windows Form程序。
//本文結束