我買的是 "Advanced .NET Remoting" 初版, 寫得蠻不錯的, 範例也說得很清楚.
本篇主要說明 .Net Remoting 基本的設計方式與建議的做法, 希望能讓初學者能有一個基本的設計概念, 並避免一些常見的錯誤.
.Net Remoting 最簡單的架構如圖一: 其中綠色圓圈表示 Client-Server 彼此呼叫的介面
圖一 .Net Remoting 基本架構
- 新增一個類別庫的專案, 例如: RemotingLib.
- 在專案中新增一個類別, 例如: RemotingMsg.cs
- 設定 RemotingMsg 這個類別繼承 MarshalByRefObject 類別.
- 撰寫供 Client 呼叫的函式.
using System; namespace RemotingLib { public class RemotingMsg : MarshalByRefObject { //預設的問候語 string strHelloMsg = "Hello~"; /// <summary> /// RemotingMsg的建構子(無參數) /// </summary> public RemotingMsg() { Console.WriteLine("RemotingMsg started"); } /// <summary> /// RemotingMsg的建構子(參數是用來設定預設的問候語) /// </summary> /// <param name="hello"></param> public RemotingMsg(string hello) { this.strHelloMsg = hello; Console.WriteLine("RemotingMsg started: set msg " + this.strHelloMsg); } /// <summary> /// 取得Remoting Server的時間 /// </summary> /// <returns></returns> public DateTime GetServerTime() { return DateTime.Now; } /// <summary> /// 取得Remoting Server的問候訊息 /// </summary> /// <param name="name"></param> /// <returns></returns> public string GetServerHelloMsg(string name) { return string.Format("{0}:{1} {2}" , DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") , this.strHelloMsg , name); } } }
- 新增一個主控台應用程式專案 (ex: RemotingServer) 做為 .Net Remoting Server. (用 Windows 服務專案也可以, 這邊為了方便說明, 所以使用主控台應用程式專案)
- 在專案中加入三個參考: System.Configuration, System.Runtime.Remoting 和 RemotingLib 專案.
- 撰寫提供 Remoting 服務的相關程式碼: 參考資訊如下
- 採用TcpChannel. (參考 TcpChannel) (其他的 Channel, 可參考 此網頁)
- 連接埠為7777.
- objectUri 的名稱為RemotingTest.
- Remoring 物件的啟動方式為Singleton. (參考 WellKnownObjectMode)
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace RemotingServer { class Program { static void Main(string[] args) { try { //建立TCP 7777的通道 TcpChannel tcpChannel = new TcpChannel(7777); //註冊此通道(ensureSecurity設定false) ChannelServices.RegisterChannel(tcpChannel, false); //註冊 Remoting 服務的物件與物件啟動的方式(Singleton) RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemotingLib.RemotingMsg) , "RemotingTest", WellKnownObjectMode.Singleton); } catch (Exception e) { Console.WriteLine(e.ToString()); } string strInput; Console.WriteLine("請輸入'exit', 以結束程式."); while (!(strInput = Console.ReadLine()).Equals("exit", StringComparison.OrdinalIgnoreCase)) { } } } }
- 新增一個主控台應用程式專案 (ex: RemotingClient) 做為 .Net Remoting Client
- 在專案中加入三個參考: System.Configuration, System.Runtime.Remoting 和 RemotingLib 專案.
- 設定呼叫 Remoting 物件的 type 和 url , 並於取得 Remoting 物件後, 進行呼叫.
using System; using System.Configuration; using RemotingLib; namespace RemotingClient { class Program { static void Main(string[] args) { try { //透過Activator.GetObject取得指定 url 的 Remoting 物件, 並轉換該物件型別至RemotingLib.RemotingMsg //url中的IP與port, 要視 RemotingServer 的設定資訊, 進行調整 RemotingLib.RemotingMsg imsg = (RemotingLib.RemotingMsg)Activator.GetObject(typeof(RemotingLib.RemotingMsg) , "tcp://127.0.0.1:7777/RemotingTest"); //取得 Server 時間 Console.WriteLine("Get Server Time:"); Console.WriteLine(imsg.GetServerTime()); //取得 Server 的問候訊息 Console.WriteLine("Get Server Message:"); Console.WriteLine(imsg.GetServerHelloMsg(Environment.MachineName)); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } string strInput; Console.WriteLine("請輸入'exit', 以結束程式."); while (!(strInput = Console.ReadLine()).Equals("exit", StringComparison.OrdinalIgnoreCase)) { } } } }
圖二 RemotingServer 啟動畫面
圖三 RemotingClient 啟動畫面
圖四 RemotingServer 被呼叫後的畫面
*注意: 當 RemotingServer 一開始啟動的時候, Remoting 物件尚未被初始化, 直到有 RemotingClient 第一次呼叫後, 才會開始初始化第一個 Remoting 物件.
以上, 是最基本的 .Net Remoting 範例. 接下來開始就我個人的設計習慣, 說明如何調整, 以及為什麼調整.
- 首先, 也許有人注意到 RemotingLib 這個專案, 每次修改都需重新把 RemotingLib.dll 佈署到 Server 與 Client .
此種設計方式不僅造成將來 Remoting 物件實作部分異動後, Client 佈署上的困難, 也間接造成將實作的 dll 曝露在 Client 端的風險.
為了解決上述的問題, 我們可以新增一個 InterfaceLib 的類別庫專案, 然後讓此專案產生的 InterfaceLib.dll 取代在 Client 端的 RemotingLib.dll , 供 Client 端呼叫.
修改的程序如下:- 在 InterfaceLib 專案中新增一個介面程式 IMsg.cs, IMsg.cs 程式碼如下: (注意要用 public interface)
using System; namespace InterfaceLib { public interface IMsg { DateTime GetServerTime(); string GetServerHelloMsg(string name); } }
- RemotingLib 專案的修改如下:
- 將 InterfaceLib 專案加至 RemotingLib 專案的參考.
- 在 RemotingMsg.cs 中, 增加 IMsg 的實作.
補充(2009/8/14): 因為Remoting物件是有生命週期的(參考初始化租用期), 為了不讓這個物件被回收, 一般我都會override InitializeLifetimeService().
程式碼修改如下: (注意增加了 InterfaceLib 的 using , 及 IMsg 的實作)using System; using InterfaceLib; namespace RemotingLib { public class RemotingMsg : MarshalByRefObject, IMsg { //預設的問候語 string strHelloMsg = "Hello~"; /// <summary> /// RemotingMsg的建構子(無參數) /// </summary> public RemotingMsg() { Console.WriteLine("RemotingMsg started"); } /// <summary> /// RemotingMsg的建構子(參數是用來設定預設的問候語) /// </summary> /// <param name="hello"></param> public RemotingMsg(string hello) { this.strHelloMsg = hello; Console.WriteLine("RemotingMsg started: set msg " + this.strHelloMsg); } #region IMsg 成員 /// <summary> /// 取得Remoting Server的時間 /// </summary> /// <returns></returns> public DateTime GetServerTime() { return DateTime.Now; } /// <summary> /// 取得Remoting Server的問候訊息 /// </summary> /// <param name="name"></param> /// <returns></returns> public string GetServerHelloMsg(string name) { return string.Format("{0}:{1} {2}" , DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), this.strHelloMsg, name); } #endregion /// <summary> /// 讓此物件永不過期 /// </summary> /// <returns></returns> public override Object InitializeLifetimeService() { return null; } } }
- RemotingClient 端的 Program.cs 修改如下:
- 移除 RemotingLib 的參考, 將 InterfaceLib 專案加入參考.
- 呼叫 Remoting 物件後的轉型, 改為轉型至 IMsg.
程式碼參考如下:using System; using System.Configuration; using InterfaceLib; namespace RemotingClient { class Program { static void Main(string[] args) { try { //透過Activator.GetObject取得指定 url 的 Remoting 物件, 並轉換該物件型別至InterfaceLib.IMsg //url中的IP與port, 要視 RemotingServer 的設定資訊, 進行調整 InterfaceLib.IMsg imsg = (InterfaceLib.IMsg)Activator.GetObject(typeof(InterfaceLib.IMsg) , "tcp://127.0.0.1:7777/RemotingTest"); //取得 Server 時間 Console.WriteLine("Get Server Time:"); Console.WriteLine(imsg.GetServerTime()); //取得 Server 的問候訊息 Console.WriteLine("Get Server Message:"); Console.WriteLine(imsg.GetServerHelloMsg(Environment.MachineName)); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } string strInput; Console.WriteLine("請輸入'exit', 以結束程式."); while (!(strInput = Console.ReadLine()).Equals("exit", StringComparison.OrdinalIgnoreCase)) { } } } }
最後的專案結構如圖五. (注意: RemotingClient 只有參考 InterfaceLib 專案)
圖五 Remoting 專案結構
不過, 如果 Server 端要重新參考新的 RemotingLib.dll, 就需要重新啟動 Server .
但在 TcpChannel 模式下, 只要 Server 端重新提供服務, Client 端可在不重新啟動的情況下, 使用原本的 Remoting 物件繼續運作. (但要記得 Client 端不要在 try catch 後, 把 Remoting 物件給設定成 null)
這也是我比較喜愛使用 TcpChannel 模式的一個原因, IpcChannel 在這部分就很麻煩. - 在 InterfaceLib 專案中新增一個介面程式 IMsg.cs, IMsg.cs 程式碼如下: (注意要用 public interface)
- 第二部分是 RemotingClient 的修改. 這個比較簡單, 純粹是將 Remoting 的部分搬到設定檔.
- 在 RemotingClient 專案中, 新增一個"應用程式組態檔" . (此檔案在開發工具中叫作 App.config , 佈署後會叫作 "專案名稱.exe.config" )
- 將 RemotingClient 中呼叫的 url , 設定到組態檔中 (App.config):
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="remotingUri" value="tcp://127.0.0.1:7777/RemotingTest"/> </appSettings> </configuration>
- 修改 Remoting 呼叫的方式如下:
//透過Activator.GetObject取得指定 url 的 Remoting 物件 //, 並轉換該物件型別至InterfaceLib.IMsg //url中的IP與port, 要視 RemotingServer 的設定資訊, 進行調整 InterfaceLib.IMsg imsg = (InterfaceLib.IMsg)Activator.GetObject( typeof(InterfaceLib.IMsg) , ConfigurationManager.AppSettings["remotingUri"]);
- 最後一部分是 Server 的調整. 我習慣使用應用程式組態檔做 Remoting 物件的設定.
- 在 RemotingServer 專案中, 新增一個"應用程式組態檔" .
- 在 App.config 檔案中, 建立如下的 Remoting 資訊:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application name="ServerHost"> <service> <wellknown type="RemotingLib.RemotingMsg, RemotingLib" mode="Singleton" objectUri="RemotingTest"/> </service> <channels> <channel ref="tcp" port="7777" > <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> </channel> </channels> </application> </system.runtime.remoting> </configuration>
- <channel ref="tcp" port="7777" > : 等同於利用 TcpChannel 註冊一個 port 7777 的通道.
- <wellknown type="RemotingLib.RemotingMsg, RemotingLib" mode="Singleton" objectUri="RemotingTest"/> : 等同於透過 RemotingConfiguration.RegisterWellKnownServiceType 進行註冊.
這邊常有人搞錯 type="RemotingLib.RemotingMsg, RemotingLib" 的設定方式. 簡單來說, 逗號 (,) 前是有繼承 MarshalByRefObject 的類別名稱(含命名空間), 逗號 (,) 後是組件的名稱.
所以, 我們在這邊填入的是 RemotingLib.RemotingMsg 這個類別, 因為它繼承了 MarshalByRefObject ;至於組件名稱就填入 RemotingLib .
如果你將類別寫在 RemotingServer 專案中, 因為建置後的這個類別會在 RemotingServer.exe 中, 所以組件名稱就要填入 RemotingServer . (基本上這個組件名稱沒說一定是 dll 的名稱)
- 接下來修改一下 RemotingServer 專案中的 Program.cs 檔:
using System; using System.Runtime.Remoting; namespace RemotingServer { class Program { static void Main(string[] args) { try { //透過 RemotingConfiguration.Configure 將 Remoting 的設定讀入 RemotingConfiguration.Configure(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, false); } catch (Exception e) { Console.WriteLine(e.ToString()); } string strInput; Console.WriteLine("請輸入'exit', 以結束程式."); while (!(strInput = Console.ReadLine()).Equals("exit", StringComparison.OrdinalIgnoreCase)) { } } } }
程式碼看起來簡單多了, 只要一行就完成 Remoting 的設定.
也許有人會問:
- 我可以讓 RemotingMsg 這個類別的另一個建構子(有參數的), 做為 Remoting 預設的服務物件嗎?
- 我想要在 RemotingServer 端啟動的時候, 這個 Remoting 物件就跟著啟動, 可以嗎?
- 修改 RemotingServer 的 App.config 檔: 將 <service> 標籤 mark 掉. (要刪掉也行啦)
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application name="ServerHost"> <!--<service> <wellknown type="RemotingLib.RemotingMsg, RemotingLib" mode="Singleton" objectUri="RemotingTest"/> </service>--> <channels> <channel ref="tcp" port="7777" > <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> </channel> </channels> </application> </system.runtime.remoting> </configuration>
- 修改 RemotingServer 專案的 Program.cs 的程式: (注意: 一定要先使用 RemotingConfiguration.Configure , 再使用 RemotingServices.Marshal )
using System; using System.Runtime.Remoting; namespace RemotingServer { class Program { static void Main(string[] args) { try { //透過 RemotingConfiguration.Configure 將 Remoting 的設定讀入 RemotingConfiguration.Configure(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile, false); //將問候語改成"Hi~" RemotingMsg msg = new RemotingMsg("Hi~"); //透過RemotingServices.Marshal轉換此物件 //第二個參數 objectUri ,可參考 RemotingClient 的組態檔, 修改成從設定檔讀入 RemotingServices.Marshal(msg, "RemotingTest", typeof(RemotingMsg)); } catch (Exception e) { Console.WriteLine(e.ToString()); } string strInput; Console.WriteLine("請輸入'exit', 以結束程式."); while (!(strInput = Console.ReadLine()).Equals("exit", StringComparison.OrdinalIgnoreCase)) { } } } }
- 重新建置後, 依序啟動 RemotingServer.exe 和 RemotingClient.exe , 應該可以得到以下的畫面:
圖六 RemotingServer 啟動畫面2
圖七 RemotingClient 啟動畫面2
礙於內容太多, 此作法留在下一篇.
請問一下,我使用您的方法建立Client與Server,平常運作都正常,但是要是Client斷線要重連就會出現「物件'/Xxx'已經中斷連接或不在伺服器上」,一定要把Server重新啟動Client才能正常連接,請問是為什麼?該怎麼解決?
回覆刪除http://zaidox.com/net-remote-object-lifetime.html
回覆刪除請確認一下有無override InitializeLifetimeService()
請問如果我是寫在ASP.NET上面然後架在電信業者的固定IP伺服器上,這樣我還能夠用Remoting呼叫我的程式傳資料到網頁上嗎?
回覆刪除To Jason, 可先用telnet [ip] [port], 確認是否可連到, 如果可以連到, 那你的程式應該可以從外面連到.
回覆刪除