根據需求,我們的系統必須以C/S方式構建,而且是三層架構,這樣一來,就出現了服務器端和客戶端通信的問題。
為了解決雙方的通信問題,還要考慮效率、性能等方面,經過分析、試驗,我們根據效率、移植、開發難易等幾個因素,舍棄了一開始提出的WebService、消息隊列機制,以及有人建議的基於流I/O自己解析數據的通信方式,在分析了目前主流的RPC方式(DCOM、CORBA、.NET Remoting)及我們的開發平台后,最終選擇了微軟新推出的.NET Remoting機制。我們的原因如下:
1、.NET Remoting是目前分布式對象實現RPC的一種主要方式。
2、.NET Remtoing在性能上可以達到DCOM,或者與之相差不多。
3、.NET Remoting建立在.NET定義的公共數據類型CTS及運行環境CLR之上,和.NET框架有着很好的互操作性,因此功能強大切易於使用。
4、擴展性和安全性方面都比較好。
從試驗結果來看,該機制可以實現C/S模式下的雙方通信,而且在性能上具有很好的保障。根據我們開發完畢的系統性能來看,Remoting機制很好的實現了我們賦予它的任務,或者說,我們采用Remoting機制達到了我們預期的目標。
下面,對我們采用Remoting機制進行開發這一從無到有過程中的一些資料、感悟進行整理。http://blog.163.com/henan_lujun/blog/static/19538333200781324449126/
2.1 基本概念
.NET Remoting是微軟隨.NET推出的一種分布式應用解決方案,被譽為管理應用程序域之間的 RPC 的首選技,它允許不同應用程序域之間進行通信(這里的通信可以是在同一個進程中進行、一個系統的不同進程間進行、不同系統的進程間進行)。
更具體的說,Microsoft .NET Remoting 提供了一種允許對象通過應用程序域與另一對象進行交互的框架。也就是說,使用.NET Remoting,一個程序域可以訪問另外一個程序域中的對象,就好像這個對象位於自身內部,只不過,對這個遠程對象的調用,其代碼是在遠程應用程序域中進行的,例如在本地應用程序域中調用遠程對象上一個會彈出對話框的方法,那么,這個對話框,則會在遠程應用程序域中彈出。
.NET Remoting框架提供了多種服務,包括激活和生存期支持,以及負責與遠程應用程序進行消息傳輸的通訊通道。格式化程序用於在消息通過通道傳輸之前,對其進行編碼和解碼。應用程序可以在注重性能的場合使用二進制編碼,在需要與其他遠程處理框架進行交互的場合使用 XML 編碼。在從一個應用程序域向另一個應用程序域傳輸消息時,所有的 XML 編碼都使用 SOAP 協議。出於安全性方面的考慮,遠程處理提供了大量掛鈎,使得在消息流通過通道進行傳輸之前,安全接收器能夠訪問消息和序列化流。
.NET Remoting協同工作能力
下圖是.NET Remoting的體系結構圖
.NET Remoting通信體系結構
一般來說,.NET Remoting包括如下幾點主要元素:
Ø 遠程對象:運行在Remoting服務器上的對象。客戶端通過代理對象來間接調用該對象的服務,如上圖的“通信體系結構”所示。在.NET Remoting體系中,要想成為遠程對象提供服務,該對象的類必須是MarshByRefObject的派生對象。另外,要說明的是,需要在網絡上傳遞的對象,例如“參數”,則必須是可序列化的。
Ø 信道:信道是服務器和客戶機進行通信用的(這里的服務器和客戶機並不一定都是計算機,也可能是進程)。在.NET Remoting中,提供了三種信道類型:TCP、HTTP、IPC,另外,也可以定制不同的信道以適應不同的通信協議(至於如何定制,我尚未涉及到,因此,不好說)。
Ø 消息:客戶機和服務器通過消息進行信息交換,消息在信道中傳遞。這里的消息包括,遠程對象的信息,調用方法名稱,參數,返回值等。
Ø 格式標識符:該標識符標明了消息是按照什么樣的格式被發送到信道上的,目前.NET 2.0提供了兩種格式標識符:SOAP格式和二進制格式。SOAP格式標識符符合SOAP標准,比較通用,可以和非.NET 框架的Web服務通信。二進制格式標識符,則在速度、效率上面更生一籌,但通用性較SOAP差。另外,Remoting還支持自定義的格式標識符。(順便說一下:TCP信道,默認使用二進制格式傳輸,因為這個效率更高;Http信道則默認使用SOAP格式;不過在系統中,哪種信道具體使用哪種格式,則是可以根據需要設置的。)。
Ø 格式標識符提供程序:它用於把格式標識符和信道聯系起來。在創建信道時,可以指定所要使用的標識符提供程序,一旦指定了提供程序,那么消息被發送到信道上的格式也就確定了下來。
為序列化消息, .NET Remoting 提供了兩類格式程序接收器: BinaryFormatter和SoapFormatter。選擇的類型很大程度上取決於連接分布式對象的網絡環境的類型。由於. NET Remoting體系結構的可插入特性,可以創建自己的格式程序接收器,並插入到.NET Remoting基礎設施中。這種靈活性使基礎設施能夠支持可能的各種線路格式。
對於可以發送並接收二進制數據(例如TCP/IP)的網絡傳輸協議,可以使用System.Runtime.Serialization.Formatters.Binary名字空間中定義的BinaryFormatter類型。顧名思義,BinaryFormatter將消息對象序列化為一個二進制格式的流。這是消息對象在線纜間進行傳輸的最有效而簡潔的表示方式。一些網絡傳輸系統不允許發送和接收二進制數據。這類傳輸迫使應用程序在發送之前將所有的二進制數據轉換成ASCII文本表示形式。在這種情況下(或者要得到最佳協作能力的時候),.NET Remoting在System.Runtime.Serialization.Formatters.Soap名字空間中提供SoapFormatter類型。SoapFormatter使用消息的SOAP表示形式將消息序列化為流。
下面為創建信道的一個示例過程。
//標識符提供程序 BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
//信道名稱及端口 IDictionary dict = new Hashtable(); dict["name"] = "MacSystem.Server.Channel"; dict["port"] = 8087;
//注冊TCP通信信道 TcpServerChannel serverChannel = new TcpServerChannel(dict, serverProvider); ChannelServices.RegisterChannel(serverChannel, false); |
Ø 代理對象:前面也說過,客戶端不能直接調用遠程對象,客戶機只能通過代理對象來操作遠程對象。代理對象,又分為透明代理和真實代理。在客戶機看來,代理對象和遠程對象是一樣的。客戶機調用透明代理對象上的方法,透明代理再調用真實代理上的Invoke方法,Invoke方法再使用消息接受器把消息傳遞到信道上。
下圖是客戶機的方法調用導致消息在信道間傳遞的一個體系結構圖:
消息在信道上的傳遞過程
Ø 消息接受器:如上圖所示,消息接受器在服務器端和客戶端都有,接受真實代理的調用,把序列化的消息發布到信道上。
Ø 激活器:這涉及到對象生命期管理,客戶機使用激活器在服務器上創建遠程對象,或者說是申請一個遠程對象的引用。
Ø RemotingConfiguration類:該類用於配置遠程服務器和客戶機的一個實用類,它可以用於讀取配置文件或者動態地配置遠程對象。說明一點的是:RemotingConfiguration類中的大部分屬性、方法都是靜態的,這就意味着很多屬性,如應用程序名稱,只能通過當前屬性或配置文件設置一次。如果應用程序運行在宿主環境中,例如 Internet 信息服務 (IIS),則可能已經設置了該值(通常將其設置為虛擬目錄)。如果未設置應用程序名稱,則當前屬性將返回空引用
Ø ChannelServices類:該類用於注冊信道,並把消息分派到信道上。
//服務器端:演示如何使用 ApplicationName 屬性指示遠程處理應用程序的名稱 ChannelServices.RegisterChannel(new TcpChannel(8082));
RemotingConfiguration.ApplicationName = "HelloServiceApplication"; RemotingConfiguration.RegisterWellKnownServiceType( typeof(HelloService), "MyUri", WellKnownObjectMode.SingleCall );
//客戶端:演示如何從指定的應用程序訪問遠程對象 ChannelServices.RegisterChannel(new TcpChannel()); RemotingConfiguration.RegisterWellKnownClientType(typeof(HelloService), "tcp://localhost:8082/HelloServiceApplication/MyUri" );
HelloService service = new HelloService(); |
2.2應用步驟
2.2.1、服務器端
第一步:確定使用的信道。
前面已經說過,.NET Remoting提供了三種預定義的信道:TCP、HTTP、IPC,他們各有自己的特點,根據自己需要進行選擇。對於每個信道,都有一些可以配置的信息,如:
Ø 信道名稱:若不指定,則使用默認名稱,每種信道,都有默認的信道名稱;不過為了區分計,開發者最好給自己創建的信道命名。
Ø 信道使用的格式化提供程序:如果不指定,則使用默認形式。
Ø 信道優先級:優先級越高,則被選擇進行連接的機會則越大。
Ø 信道的端口:服務器端信道,則必須具備一個所有客戶都知道的端口即固定端口,客戶端則可以由系統自動分配端口。
Ø ……其它屬性參見MSDN
下面是一些創建信道的示例代碼:
// 1. 使用默認構造函數創建一個TCP偵聽信道 TcpServerChannel serverChannel = new TcpServerChannel(9090); ChannelServices.RegisterChannel(serverChannel);
// 2. 使用帶信道配置屬性的形式創建TCP偵聽信道 IDictionary dict = new Hashtable(); dict["port"] = 9090; dict["authenticationMode"] = "IdentifyCallers";
TcpServerChannel serverChannel = new TcpServerChannel(dict, null); ChannelServices.RegisterChannel(serverChannel);
// 3. 指定信道名稱、端口及格式標識符提供程序的形式創建信道 BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; TcpServerChannel channel = new TcpServerChannel( "Server Channel", 9090, serverProvider); ChannelServices.RegisterChannel(serverChannel,false); |
第二步:注冊信道。
注冊信道就是把已經創建的信道,使用ChannelServices類的RegisterChannel方法來“向信道服務注冊信道”。
示例代碼如上所示。
第三步:注冊遠程對象。
服務器端打算發布一個可以被客戶端調用的遠程對象,那它必須以某種方式“告訴”系統:我這里有一個這樣的服務可供你使用。這里的“告訴”的過程,就是注冊遠程對象。
在.NET Remoting中,注冊遠程對象,使用RemotingConfiguration類提供的靜態方法RegisterWellKnownServiceType或RegisterActivatedServiceType來進行注冊。至於選擇哪種注冊方式,則根據遠程對象的激活方式而定。關於這兩個方法,做如下說明:
RegisterWellKnownServiceType 功能:將服務端上的對象 Type 注冊為已知類型(“單個調用”(singlecall) 或singleton)。 激活方式:服務器端激活 服務器端激活模式又有兩種Singleton 和 SingleCall,說明如下
|
||
RegisterActivatedServiceType 功能:將服務端上的對象 Type 注冊為可根據請求從客戶端激活的類型 激活方式:客戶端激活 配置:“客戶端激活的對象”是當客戶端調用 new 或 Activator.CreateInstance() 時在服務器上創建的。客戶端本身使用生存期租用系統,可以參與到這些實例的生存期中。這種激活機制能夠提供最廣泛的設計靈活性。如果使用客戶端激活,當客戶端試圖激活對象時,激活請求將發送到服務器。這種機制允許使用參數化的構造函數和針對每個客戶端的連接狀態管理。使用客戶端激活,每個客戶端接受其特定的服務器實例提供的服務,從而簡化了多個調用時對象狀態的保存過程。但使用這些對象時一定要謹慎,因為很容易忘記會話是分布式的,對象實際上不僅在進程之外,而且在多層應用程序的情況下,還有可能在計算機之外(在 Internet 上設置一個屬性並不過分)。實用而不花哨的接口應該成為這里的准則:為了提高性能,我們可能需要在高度結合與松散耦合之間進行權衡。要創建客戶端激活類型的實例,可以通過編程的方法配置應用程序,也可以進行靜態配置。在服務器上進行客戶端激活的配置相當簡單。 |
第四步:注銷信道。
在程序關閉時,或者清理資源時,要關閉調已經注冊的信道,這樣好讓出服務所使用的計算機端口,方法就是調用ChannelServices.UnregisterChannel即可實現,示例代碼如下:
//卸載名稱為"MacSystem.Server.Channel的通信信道 IChannel[] regChannels = ChannelServices.RegisteredChannels;
foreach (IChannel channel in regChannels) { if (channel.ChannelName == "MacSystem.Server.Channel") { (IchannelReceiver).StopListenning(null); ChannelServices.UnregisterChannel(channel); break; } }//end foreach() |
2.2.2、客戶端
第一步:創建信道
客戶端的信道注冊,跟服務器端的注冊基本相同,差別在於它不必指定端口,和服務器端對應的,則使用TcpClientChannel、HttpClientChannel類等,當然也可以使用TcpChannel、HttpChannel類來注冊。
在創建了信道后,也是使用ChannelServices類的RegisterChannel方法來完成信道的注冊。
第二步:發現URL
客戶端要激活服務器上的遠程對象,也就是說要獲得一個遠程對象的本地代理,則必須首先獲得遠程對象的URL。該URL和Web瀏覽器的URL具有一樣的含義,具有如下的格式:
Protocol://server:port/URI
這里的協議,也就是信道的格式,如tcp、http、ipc。不過,由於IPC機制只能使用在單個機器上,因此,不需要使用服務器地址。
這里的URI則包括遠程對象的應用程序名成,對象的服務名稱,如下面的示例:
http://localhost:8085/helloapp/hello
tcp://localhost:8087/helloapp/hello
ipc://8088/helloapp/hello
第三步:創建對象
創建對象,也就是在客戶端激活服務器上的對象,並獲得這個遠程對象的一個本地代理(透明代理)。
在客戶端創建遠程對象,有兩種方法,一種是使用new方法來創建,另外一種方法則是使用激活類的Activator的創建方法。針對每一種方法,在不同的激活模式下,又稍有區別,解釋如下:
服務器端激活: 方法一:使用new激活遠程知名對象 RemotingConfiguration.RegisterWellKonwnClientType(typeof(Hello), “tcp://localhost:8085/helloapp/hi”); Hello hello = new Hello();
方法二:使用激活器創建服務器激活的已知對象 Hello hello = (Hello)(Activator.GetObject((typeof(Hello), “tcp://localhost:8085/helloapp/hi”); |
客戶端激活: 方法一:使用new激活遠程對象 RemotingConfiguration.RegisterActivatedClientType(typeof(Hello), “tcp://localhost:8085/helloapp/hi”); Hello hello = new Hello();
方法二:使用激活器 Hello hello = (Hello)(Activator.CreateInstance((typeof(Hello), “tcp://localhost:8085/helloapp/hi”); 說明:CreateInstance用於創建客戶激活的遠程對象。 |
說明一下,這里的方法一,new操作符並沒有創建新的遠程對象,而是泛湖一個與遠程對象相思的代理對象。
第四步:注銷信道
客戶端的信道注銷和服務器端是一樣的,這里就不再做過多說明。
2.3事件調用
這里的事件調用,指的是服務器對客戶端的事件做出反應,以及客戶端對服務器上的事件做出反應。 例如,在客戶端調用了某個操作時,服務器進行日志記錄;或者,在服務器向所有客戶端發送一條消息時,客戶端截獲此消息並進行處理等。
如此一來,就包括兩個方面,一個是服務器注冊客戶端事件,以攔截客戶端的某些事件;一個是客戶端注冊服務器上的事件。
不過,總的說來,Remoting的核心是遠程對象,因此,對事件的注冊、觸發等功能,還是和遠程對象有關。還可以這樣說,遠程對象是Remoting事件調用的核心。
2.3.1 服務器注冊客戶端事件
其實,服務器注冊客戶端事件,相對客戶端注冊服務器事件來說,是比較簡單的。因為,客戶端在調用服務器上的遠程對象時,代碼都在服務器上執行,服務器很容易截獲這一事件,從而進行自己的處理。
具體的實現思路是,在遠程對象中定義一個事件,然后在某方法內部,調用該事件的處理函數,這樣,在客戶端調用該方法時,就觸發了事件。
下面給出一個遠程對象的示例代碼:
public delegate void CallFooDelegate(string message);
public class RemoteObject:MarshByRefObject { public static event CallFooDelegate CallFooEvent; public void Foo(String message) { //……Do Something
//觸發事件 if (CallFooEvent != null) { CallFooEvent(message); } }
…… } |
說明一下,上面的示例代碼中,RemoteObject直接從MarshByRefObject對象集成,沒有實現其它任何接口,這樣在程序發布的時候,客戶端和服務器端都必須包含RemoteObject組件,如此一來,最常見的發布模型如下圖所示:
這樣,在客戶端就具有了遠程對象的代碼,這在某種程度上來說,是不安全的。因此,一個推薦的方案是:創建一個接口,讓遠程對象實現該接口,然后在客戶端,只包含該接口,在服務器端則包含兩個部分,這樣一來,組件示意圖如下:
2.3.2 客戶端注冊服務器事件
客戶端注冊服務器上的事件,是個畢竟復雜的過程,自己在剛開始的時候,以外和服務器注冊客戶端事件是一樣的,結果稍一思考,就冷汗一頭,原來根本不是那么回事兒。下面是我在此險阻前的開發歷程。
問題的提出:
在服務器注冊客戶端事件時,只要客戶端激活了遠程對象,就可以發送消息,而只要服務器訂閱了此事件,就可以處理客戶端的消息了。然而,就客戶端注冊服務器事件這一問題來說,如果把思路置反,由遠程對象發送消息,問題能否解決呢?
問題分析
然而,第一個事實是:在激活遠程對象前,服務所做必須做的是“注冊”該遠程對象類以給客戶端提供服務,然后由客戶端決定何時來創建一個遠程對象,也就是說,服務器端沒有顯式的創建過遠程對象,這樣,既然服務器端沒有顯式的對象,那么又如何來顯式的操作之以傳遞消息呢?
第二個事實是:按照服務器注冊客戶端激活的方式使用遠程對象,客戶端得到的遠程對象是根據需要創建的,在事件上來說,這些遠程對象並不保證具有完全一致的狀態。因此,在服務器端注冊對象后顯式創建一個對象的思路也並不可行。
第三個事實是:客戶端的使用的遠程對象只是一個代理,和服務器上的遠程對象處於完全不同的應用程序域,或者說,服務器上的對象和客戶端的對象是具有相似性但完全不同的兩個“物體”,因此,在服務器端,或者是在客戶端對各自對象所做的操作,是不會互相影響的。這同樣也說明了在服務器端注冊對象后顯式創建一個對象的思路也並不可行。
第四個事實:客戶端要訂閱服務器事件,那么事件處理程序向對象的注冊動作,也就應該在客戶端完成,而服務器端還需逐個調用各個客戶端注冊的處理程序,這就要求客戶端和服務器端所操作的對象是“同一個”。
這樣,為了實現客戶端注冊客戶端事件,必須讓遠程對象具有的特征也就很明顯了:客戶端得到的遠程對象實際上是服務器上對象的代理。
為了實現這一特征,最好的方法是在服務器上創建一個遠程對象,然后將此對象公布出來(注意,這個Sington方式有着本質的區別,Sington方式只是保證在任何一時間上只有一個對象存在於服務器上),本文第一部分所述的服務器端注冊方式都不能滿足這點,那么還有其它的注冊方式嗎?
經過查閱,我發現了RemotingServices.Marshal方法,該方法接受MarshalByRefObject,並將其轉換為具有指定 URI 和提供的Type 的 ObjRef 類的實例。該方法注冊遠程對象的方式如下:
RemoteObject Obj = new RemoteObject ();
ObjRef objRef = RemotingServices.Marshal(Obj,"BroadCastMessage.soap");
Marshal方法與前述的幾種注冊方式不一樣。前面的方式,遠程對象是根據客戶端調用的方式,來自動創建的。而該Marshal方法則顯式地創建了遠程對象實例,然后將其Marshal到通道中,形成ObjRef指向對象的代理。只要生命周期沒有結束,這個對象就一直存在。而此時客戶端獲得的對象,正是創建的Obj實例的代理。
如此,使用這種注冊方式,就可以保證每次客戶端所獲得的遠程對象都是“相同”的,這樣就滿足了客戶端注冊遠程服務器事件的要求。那么,使用這種方式來構建真正的系統,還需要什么呢?
實踐過程:
有了新的方法及思路,那么剩下的就是編寫遠程對象的功能代碼,之后在服務器上發布,然后客戶端進行連接並注冊事件。
寫出的遠程對象代碼如下:
public class RemoteObject : MarshalByRefObject , IRemoteObject { #region IRemoteObject 成員
public event GetClientMessageDelegate GetClientMessageEvent = null;
public event BroadcastMessageDelegate BroadcastMessageEvent = null;
public void SendMessageToServer(string user, string message) { //調用本地事件處理函數 if (GetClientMessageEvent != null) { GetClientMessageEvent( user, message); }
//將此客戶端消息作為廣播轉發 BroadcastMessage(user, message); }
#endregion
/// <summary> /// 發送廣播消息 /// </summary> /// <param name="caption">消息標識</param> /// <param name="message">消息內容</param> public void BroadcastMessage(string caption, string message) { if (BroadcastMessageEvent != null) { BroadcastMessageDelegate tmpEvent = null; foreach (Delegate del in BroadcastMessageEvent.GetInvocationList()) { try { tmpEvent = (BroadcastMessageDelegate)del; tmpEvent( caption, message); } catch (Exception e) { Console.WriteLine(e.Message); Console.WriteLine(e.StackTrace);
//注冊的事件處理程序出錯,刪除 BroadcastMessageEvent -= tmpEvent; }//end try-catch }//end foreach() }//end:if (BroadcastMessageEvent != null) }
public override object InitializeLifetimeService() { //return base.InitializeLifetimeService(); return null; } } |
服務上發布遠程對象的代碼如下:
private void PublishRemoteObject() { //tcp channel BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full; TcpServerChannel serverChannel = new TcpServerChannel( _ServerChannelName, _Port, serverProvider );
ChannelServices.RegisterChannel(serverChannel, false);
//RemotingConfiguration RemotingConfiguration.ApplicationName = "CoolQ";
//Marsh _MyRemoteObject.GetClientMessageEvent += new GetClientMessageDelegate(obj_GetClientMessageEvent); RemotingServices.Marshal(_MyRemoteObject, "RemoteObj");
} |
在客戶端也寫出了獲得遠程對象及注冊事件的方法如下:
//tcp channel BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; IDictionary dict = new Hashtable(); dict["port"] = 0; dict["name"] = _ClientChannelName; TcpChannel channel = new TcpChannel(dict, new BinaryClientFormatterSinkProvider(),serverProvider); ChannelServices.RegisterChannel(channel,false);
_MyRemoteObject = (IRemoteObject)(Activator.GetObject(typeof(IRemoteObject), "tcp://192.168.13.2:9527/CoolQ/RemoteObj")); _MyRemoteObject.BroadcastMessageEvent += new BroadcastMessageDelegate (ProcessBroadcastMessage); |
這樣,就“完成”了客戶端注冊服務器端事件的功能,立即編譯,滿懷希望的等着出現運行結果,然后人生不如意事十有八九啊,迎接我的確是一個
附帶的異常信息是:System.Reflection.TargetInvocationException: 調用的目標發生了異常。 ---> System.IO.FileNotFoundException: 未能加載文件或程序集“CollogueClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”或它的某一個依賴項。系統找不到指定的文件。
上面的信息出現在客戶端,卻提示找不到客戶端程序集呢,這是怎么回事呢?別急,一步一步來。跟蹤代碼,發現異常出現在_MyRemoteObject.BroadcastMessageEvent += new BroadcastMessageDelegate (ProcessBroadcastMessage)這里,也就是在向服務器注冊事件處理程序的時候出錯的。由於客戶端的遠程對象代理是服務器端對象通過序列化后得到的,因此,對遠程對象進行的事件注冊,實際上發生在服務器上,而在注冊時,.NET要加載ProcessBroadcastMessage所在的程序集,而這個程序集就是客戶端程序集,在服務器端當然沒有這個程序集,於是就出現這個異常了。
客戶端注冊服務器事件的處理代碼,必定要位於客戶端,而服務器事件又是由位於服務器上的遠程對象捕獲的,這樣,事件在服務器上,處理代碼處在客戶端上,這中間如何來架起橋梁呢?
看到橋梁這個“詞匯”,聯想起2.3.1部分中的遠程對象接口類的功能,我們能否也建立起一個中間類來做這個橋梁呢,而這個中間類就像接口類一樣,位於服務器端和客戶端,在服務器端,它接收遠程對象的調用,在客戶端,它復雜調用用戶注冊的處理代碼,也就是說,把服務器直接對客戶端代碼的調用,轉換成對中間類的調用,然后再由中間累代替服務器去調用客戶端代碼。
如此,中間類的代碼如下:
public class RemoteObjectWrapper : MarshalByRefObject { public event BroadcastMessageDelegate WrapperBroadcastMessageEvent = null;
/// <summary> /// 發送廣播消息的包裝函數 /// </summary> /// <param name="caption">標題</param> /// <param name="message">消息</param> public void WrapperBroadMessage( string caption, string message) { if (WrapperBroadcastMessageEvent != null) { WrapperBroadcastMessageEvent( caption, message); }//end if }
//重載生命周期函數,使之無限長 public override object InitializeLifetimeService() { //return base.InitializeLifetimeService(); return null; } } |
這個包裝類,放在一個公共程序集中,在發布的時候,服務器端和客戶端都有一份,這樣就保證了服務器可以加載到所需要的代碼段。
這樣,在客戶端注冊服務器事件,就使用這個中間類做橋梁,避免服務器直接加載客戶端代碼。客戶端注冊服務器事件調整后的代碼如下,
RemoteObjectWrapper _WrapperRemoteObject = new RemoteObjectWrapper(); _WrapperRemoteObject.WrapperBroadcastMessageEvent += new BroadcastMessageDelegate(_WrapperRemoteObject_WrapperBroadcastMessageEvent); _MyRemoteObject.BroadcastMessageEvent += new BroadcastMessageDelegate(_WrapperRemoteObject.WrapperBroadMessage); |
服務器端不需做調整。
到此,服務器事件注冊完畢,運行,調試,一路順暢,呵呵,問題“基本”解決。
也許,有人要問,為什么這樣就可以了呢,其實,我對這個地方也不甚明了,嘗試着再做點解釋吧。前面說過,使用中間類之前,服務器端的委托要裝載client程序集,於是出現了上面的異常。現在我們把遠程對象委托裝載的權利移交給RemoteObjectWrapper,而這個類的程序集在服務器上有,所以這個加載不存在問題。在實際運行過程中,這個類對象是放在客戶端的,所以它要裝載client程序集絲毫沒有問題。語句:
RemoteObjectWrapper _WrapperRemoteObject = new RemoteObjectWrapper();
_WrapperRemoteObject.WrapperBroadcastMessageEvent += new BroadcastMessageDelegate(_WrapperRemoteObject_WrapperBroadcastMessageEvent);
實現了這個功能。
不過此時雖然訂閱了事件,但事件還是客戶端的,沒有與服務端聯系起來。而服務端的事件是放到遠程對象中的,所以,還要訂閱事件,這個任務由遠程對象_MyRemoteObject來完成。但此時它訂閱的不再是客戶端的事件處理器了,而是RemoteObjectWrapper的觸發事件方法WrapperBroadMessage。那么此時委托同樣要裝載程序集,但此時裝載的就是WrapperBroadMessage所在的程序集了。由於裝載發生的地點是在服務端,而WrapperBroadMessage所在的程序集正是公共程序集(前面已說過,RemoteObjectWrapper 應放到公共程序集Common.dll中),而公共程序集在服務端和客戶端都已經部署了。自然就不會出現找不到程序集的問題了。
2.4其它說明
1、安全驗證問題
在ChannelServices類提供的RegisterChannel方法里面,一共有兩個版本,其中一個沒有下述的第二個參數,但微軟已經把其標記為“過時”的方法,建議使用者使用下面的方法:
ChannelServices.RegisterChannel(Ichannel channel, bool security)
針對這個方法中的第二個參數,剛開始沒有太在意,在使用時,直接使用true來進行調用,然而程序出現異常,把true改為false之后,程序即變得正常起來,這下才意識到這個參數原來不是擺設,趕快搜索,MSDN給出的解釋是:
esureSecurity參數,如果啟用了安全,則為 true;否則為 false。將該值設置為 false將不會使在 TCP 或 IPC 信道上所做的安全設置無效。對於 TcpServerChannel,將esureSecurity 設置為 true 將在 Win98 上引發異常(因為 Wi9x 上不支持安全 tcp 信道);對於 Http 服務器信道,這樣會在所有平台上引發異常(如果想要安全的 http 信道,用戶需要在 IIS 中承載服務)。
至於這個解釋,本人看的雲里霧里,不得要領,也許功力不夠,詳細的解釋乃至讓人明白的解釋,以后再說吧。
2、通道過濾類型: 沒有正在偵聽的已注冊服務器信道異常
從.NET 1.1開始,序列化的安全級別得到提高了。所以,在注冊通道時,應該將TypeFilterLevel設置為Full;
下面是一個簡單的演示代碼。
服務器端:
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable(); props["port"] = 8085;
TcpChannel chan = new TcpChannel(props,clientProvider,serverProvider); ChannelServices.RegisterChannel(chan); |
注意,除了服務器段要做這個修改外,客戶端也要做對應的修改。如果只修改服務器段不修改客戶端,就會有下面的異常產生:
An unhandled exception of type 'System.Runtime.Remoting.RemotingException' occurred in mscorlib.dll
Additional information: 此遠程處理代理沒有信道接收,這意味着服務器沒有正在偵聽的已注冊服務器信道,或者此應用程序沒有用來與服務器對話的適當客戶端信道。
客戶端的代碼跟服務器的代碼幾乎一樣:
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable(); props["port"] = 8084;
TcpChannel chan = new TcpChannel(props,clientProvider,serverProvider); ChannelServices.RegisterChannel(chan); |
說明一下:注意如果是在同一台機子上作測試,客戶端、服務器端的端口號不要一樣。
關於這個問題,微軟在MSDN中給出來了一些正式解釋:
依賴於運行時類型驗證的遠程處理系統必須反序列化一個遠程流,然后才能開始使用它,未經授權的客戶端可能會試圖利用反序列化這一時機。為了免受這種攻擊,.NET 遠程處理提供了兩個自動反序列化級別:Low 和 Full。Low(默認值)防止反序列化攻擊的方式是,在反序列化時,只處理與最基本的遠程處理功能關聯的類型,如自動反序列化遠程處理基礎結構類型、有限的系統實現類型集和基本的自定義類型集。Full 反序列化級別支持遠程處理在所有情況下支持的所有自動反序列化類型。
警告 不要以為控制反序列化是應用程序需要的唯一安全機制。在分布式應用程序中,即使嚴格控制序列化也不能防止這種危險的發生:即未經授權的客戶端截獲通信內容,然后以某種方式利用該通信內容,即使只是向其他用戶顯示數據,也會造成損害。因此,雖然 Low 反序列化級別對某些基於自動反序列化的攻擊類型提供了一定的保護,但您仍然必須考慮是否使用身份驗證和加密來為您的數據提供完全的保護。
如果應用程序需要使用僅在 Full 反序列化級別才可用的遠程處理功能,您必須提供身份驗證的類型和必要的加密級別,以保護任何在使用遠程方案中的這些高級功能時可能遭受風險的資源。
解決之道:
您可以通過編程方式或使用應用程序配置文件設置反序列化級別。
以編程方式設置反序列化級別,示例代碼見上面的服務器端代碼。
使用應用程序配置文件設置反序列化級別。
若要使用配置文件設置反序列化級別,必須顯式指定 <formatter> 元素的typeFilterLevel 屬性。雖然這通常是在服務器端指定的,但您還必須為注冊來偵聽回調的客戶端上的任何信道指定這一屬性,以控制其反序列化級別。以下示例為應用程序域中的 SoapFormatter 和 BinaryFormatter 顯式地將反序列化級別設置為 Low。
<configuration> <system.runtime.remoting> <application> <service> <wellknown type="ServiceType, common" objectUri="ServiceType.soap" mode="Singleton" /> </service> <channels> <channel ref="http"> <serverProviders> <provider ref="wsdl" /> <formatter ref="soap" typeFilterLevel="Low" /> <formatter ref="binary" typeFilterLevel="Low" /> </serverProviders> </channel> </channels> </application> </configuration>
|
這一問題影響到的APIs有:
System.Runtime.Serialization.ISerializable
System.Runtime.Remoting.ObjRef
System.Runtime.Remoting.Lifetime.ILease
System.Runtime.Remoting.Lifetime.ISponsor
System.Runtime.Remoting.Contexts.IContributeEnvoySink
System.Runtime.Remoting.Channels.SoapServerFormatterSinkProvider
System.Runtime.Remoting.Channels.BinaryServerFormatterSinkProvider
2.5 后續問題
至於Remoting,真乃一博大之學問,小可我才用不久,這里只是自己使用過程中遇到的一些問題歸結,至於Remoting中的很多方面,例如異步調用、配置文件等等,都沒涉及,以后有機會,再來細細研究。
轉載:http://blog.163.com/henan_lujun/blog/static/19538333200781324449126/