C/S架構的應用程序,將一些復雜的計算邏輯由客戶端轉移到服務器端可以改善性能,同時也為了其它方面的控制。.NET Remoting在局域網內調用的性能相當不錯。ERP系統中基於.NET Remoting和WCF構建一個應用程序服務器(Application Server)。
分布式應用設計目標:
1 客戶端的連接,服務器要能控制。服務器根據授權許可文件的內容,控制客戶端並發數。
2 服務器崩潰,客戶端要得到通知,掛起當前數據輸入操作,當服務器可用時,客戶端可自動重新連接 。
3 支持數據加密,對敏感的數據可用加密的端口和通道傳輸。
4 支持數據壓縮,改善數據傳輸效率,因為要做一個壓縮與解壓縮動作,性能有所降低。
5 安全控制,應用程序服務器阻止未授權的或未簽名的應用程序的連接。
6 客戶端向服務器傳送大文件,傳送圖片需要時性能優化
7 服務器端發現錯誤時,支持堆棧傳回客戶端以診斷原因。
8 開發和部署簡單方便。
先設計服務器與客戶端通信的接口,一個簡單銷售合同數據表的訪問接口代碼如下所示。
public interface ISalesContractManager { SalesContractEntity GetSalesContract(Guid sessionId, String ContractNo); SalesContractEntity GetSalesContract(Guid sessionId, String ContractNo, IPrefetchPath2 prefetchPath); SalesContractEntity GetSalesContract(Guid sessionId, String ContractNo, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList); EntityCollection GetSalesContractCollection(Guid sessionId, IRelationPredicateBucket filterBucket); EntityCollection GetSalesContractCollection(Guid sessionId, IRelationPredicateBucket filterBucket, ISortExpression sortExpression); EntityCollection GetSalesContractCollection(Guid sessionId, IRelationPredicateBucket filterBucket, ISortExpression sortExpression, IPrefetchPath2 prefetchPath); EntityCollection GetSalesContractCollection(Guid sessionId, IRelationPredicateBucket filterBucket, ISortExpression sortExpression, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList); SalesContractEntity SaveSalesContract(Guid sessionId, SalesContractEntity salesContractEntity); SalesContractEntity SaveSalesContract(Guid sessionId, SalesContractEntity salesContractEntity, EntityCollection entitiesToDelete); SalesContractEntity SaveSalesContract(Guid sessionId, SalesContractEntity salesContractEntity, EntityCollection entitiesToDelete, string seriesCode); void DeleteSalesContract(Guid sessionId, SalesContractEntity salesContractEntity); bool IsSalesContractExist(Guid sessionId, String ContractNo); bool IsSalesContractExist(Guid sessionId, IRelationPredicateBucket filterBucket); int GetSalesContractCount(Guid sessionId, IRelationPredicateBucket filterBucket); SalesContractEntity CloneSalesContract(Guid sessionId, String ContractNo); void PostSalesContract(Guid sessionId, String ContractNo); void PostSalesContract(Guid sessionId, SalesContractEntity salesContractEntity); }
再設計服務實現SalesContractManager,實現上面的接口。
[CommunicationService("SalesContractManager")] public class SalesContractManager : ManagerBase, ISalesContractManager { public SalesContractEntity GetSalesContract(Guid sessionId, String ContractNo) { return GetSalesContract(sessionId, ContractNo, null); } public SalesContractEntity GetSalesContract(Guid sessionId, String ContractNo, IPrefetchPath2 prefetchPath) { return GetSalesContract(sessionId, ContractNo, prefetchPath, null); }
注意到上面給上面的實現類添加了CommunicationService特性,也就是聲明實現類是一個服務。
先來回顧一下最簡單的.NET Remoting 客戶端與服務器端代碼設計模式。
服務器端的設計:
int port = Convert.ToInt32(ConfigurationManager.AppSettings["Port"]); BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider(); provider.TypeFilterLevel = TypeFilterLevel.Full; IDictionary props = new Hashtable(); props["port"] = port; TcpChannel channel = new TcpChannel(props, null, provider); ChannelServices.RegisterChannel(channel, false); RemotingConfiguration.RegisterWellKnownServiceType(typeof(ERP.BusinessLogic.SalesContractManager), "RemotingService", WellKnownObjectMode.SingleCall);
這里是用代碼寫死服務類,可以用配置文件增加服務類,也可用以反射的方法增加服務類。
客戶端調用的代碼如下:
ISalesContractManager salesContractManager =(IPdmServer)Activator.GetObject(typeof(ISalesContractManager),string.Format("{0}RemotingService", ApplicationServerUrl)); if (salesContractManager == null) throw new AppException("Sever configuration error"); salesContractManager.SaveSalesContract(guid, salesContract);
改善服務器端代碼,讓服務器主動搜索系統中打上CommunicationService特性的服務類。當新增加服務類型時,框架可自動識別並加載服務類型:
Assembly assembly = typeof(Manager).Assembly; Type[] types = assembly.GetTypes(); foreach (Type type in types) { if (type.Namespace == "ERP.BusinessLogic.Managers") { string serviceName = string.Empty; object[] attributes = type.GetCustomAttributes(typeof(CommunicationService), true); if (attributes.Length > 0) serviceName = type.Name; if (!string.IsNullOrEmpty(serviceName)) { if (clientActivatedServices.Contains(serviceName)) { RemotingConfiguration.RegisterActivatedServiceType(type); } else if (singletonServices.Contains(serviceName)) { RemotingConfiguration.RegisterWellKnownServiceType(type, serviceName + ".rem", WellKnownObjectMode.Singleton); } else { RemotingConfiguration.RegisterWellKnownServiceType(type, serviceName + ".rem", WellKnownObjectMode.SingleCall); } }
這樣節省了開發人員的服務發布時間。客戶端主要方法如下:
instance = ReflectionHelper.CreateObjectInstance<T>(type);
.NET Remoting可識別當前服務類型是否注冊過,如果有則會創建一個遠程代理,實現向服務器發送請求。
控制客戶端並發數:
.NET Remoting支持單件調用模式,客戶端不論調用次數,服務器端都只會是相同的一份對象,這樣可實現會話Session控制。ERP系統用戶登入時,檢查服務器登入會話表(Session,本質上是一個DataTable),判斷是否已經登入。同時也可以實現並發用戶控制,當登入的用戶數超過授權許可規定的用戶數,可阻止登入。
服務器崩潰,客戶端要得到通知,掛起當前數據輸入操作,當服務器可用時,客戶端可自動重新連接:
.NET Remoting支持客戶端服務器訂閱模式,服務器端可向客戶端發送消息。當服務器進程崩潰,或是無法連接到數據庫等原因發生時,需要及時向訂閱過的客戶端發送消息通知,客戶端界面收到通知后需要立即掛起ERP主界面,不允許任何操作。這樣可避免用戶辛苦的輸入數據后,點擊保存卻連接不上服務器,只好關閉重新輸入。
安全控制,應用程序服務器阻止未授權的或未簽名的應用程序的連接:
服務器控制客戶端的連接調用,在客戶端登入時,需要傳入當前客戶端程序集的版本,簽名標識Token,還有系統參數等,服務器端會將這些參數整合在一起,用MD5計算出一個哈希值。只有客戶端傳入的參數值經過MD5運算后,與服務器中這些相同的參數值MD5運算之后的值,完全相同。服務器端才允許客戶端繼續登入。
服務器端發現錯誤時,支持堆棧傳回客戶端以診斷原因:
上面創建服務器端的代碼中,有以下兩句是為了實現服務器端堆棧回傳到客戶端的,參考下面的代碼:
BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
provider.TypeFilterLevel = TypeFilterLevel.Full;
支持數據加密和數據壓縮:
使用自定義的GTCP信道(Channel)通信,此通道支持加密傳輸和數據壓縮傳輸。
開發和部署簡單方便:
Code Smith 6.5的模板會幫助生成ISalesContractManager和SalesContractManager兩個類型的源代碼,通過上面的講解知道,只需要給SalesContractManager加上CommunicationService特性即實現服務類的部署。
客戶端向服務器傳送大文件性能:
為了改善性能,對於文件傳輸類服務,單獨開放一個端口用於文件傳輸。