我買的是 "Advanced .NET Remoting" 初版, 寫得蠻不錯的, 範例也說得很清楚.
本篇主要說明 .Net Remoting 基本的設計方式與建議的做法, 希望能讓初學者能有一個基本的設計概念, 並避免一些常見的錯誤.
.Net Remoting 最簡單的架構如圖一: 其中綠色圓圈表示 Client-Server 彼此呼叫的介面
圖一 .Net Remoting 基本架構
- 新增一個類別庫的專案, 例如: RemotingLib.
- 在專案中新增一個類別, 例如: RemotingMsg.cs
- 設定 RemotingMsg 這個類別繼承 MarshalByRefObject 類別.
- 撰寫供 Client 呼叫的函式.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849using
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)
12345678910111213141516171819202122232425262728293031using
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 物件後, 進行呼叫.
123456789101112131415161718192021222324252627282930313233using
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)
//取得 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) 12345678910
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 的實作)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960using
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.
程式碼參考如下:123456789101112131415161718192021222324252627282930313233using
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)
//取得 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): 1234567
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
configuration
>
<
appSettings
>
<
add
key
=
"remotingUri"
</
appSettings
>
</
configuration
>
- 修改 Remoting 呼叫的方式如下: 1234567
//透過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 資訊: 12345678910111213141516171819
<?
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 檔: 程式碼看起來簡單多了, 只要一行就完成 Remoting 的設定.123456789101112131415161718192021222324
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)) { }
}
}
}
也許有人會問:
- 我可以讓 RemotingMsg 這個類別的另一個建構子(有參數的), 做為 Remoting 預設的服務物件嗎?
- 我想要在 RemotingServer 端啟動的時候, 這個 Remoting 物件就跟著啟動, 可以嗎?
- 修改 RemotingServer 的 App.config 檔: 將 <service> 標籤 mark 掉. (要刪掉也行啦) 1234567891011121314151617
<?
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 ) 1234567891011121314151617181920212223242526272829
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
礙於內容太多, 此作法留在下一篇.
4 則留言:
請問一下,我使用您的方法建立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], 確認是否可連到, 如果可以連到, 那你的程式應該可以從外面連到.
張貼留言