摘要
遠程執行是C#中一種使開發人員能夠使用遠程對象的基礎架構。遠程對象是一種位於調用者應用域之外的對象。本文中的例子說明了如何使用二種遠程對象訪問機制(值傳遞和地址傳遞),它還通過一個簡單的、功能強大的任務服務器的實現說明了分布式計算中遠程對象的強大功能。
任務服務器能夠接受所有能實現ITask界面的對象,並在其應用域中運行這些對象。更為重要的是,它能夠一次從多個客戶端接受任務。
在學習完本篇文章後,讀者將能夠:
━━建立服務器/客戶端對象之間的連接。
━━按值傳遞對象。
━━按地址傳遞對象。
━━理解遠程任務分配的概念。
遠程對象
遠程對象通過地址傳遞對象或者通過對象的值傳遞對象。
在第一種情況下,對象的地址由應用域A傳遞到應用域B,但對象的方法調用在應用域A和應用域B之間。對象在應用域A中存在和運行,但在應用域B中也象是個本地的對象。
在第二種情況下,整個對象及其附屬的實體(被稱作對象圖表)被串行化成字節的形式,並從應用域A被傳送到應用域B。然後,對象在應用域B被“反串行化”並恢復到原來的狀態。現在,對象就在應用域B上存在和運行了。
建立對象主機(也被稱作服務器)
設立服務器需要作的第一步是在對象進行通訊的二個應用域間建立一條通道,它可以是一條TCP/IP通道或HTTP通道。TCP通道的速度較快,適用於對網絡內信息包傳輸限制較少的網絡使用,HTTP通道更靈活,適合在互聯網等廣域網上使用。
我們將使用TCP/IP通道,而且將在同一台機器的二個不同的應用域名上同時運行服務器端和客戶機端。因此,輸入下面的代碼在TCP/IP堆棧上的8065端口創建通道myChannel:
TcpChannel myChannel = new TcpChannel(8065);
下面是向.NET的通道服務注冊myChannel通道,這將使該通道可以在服務器應用域之外被訪問。我們可以通過下面的代碼實現這一目的:
ChannelServices.RegisterChannel(myChannel);
最後一步是告訴.NET的遠程執行基礎架構有關我們要開放的對象的有關情況,我們需要公布對象的類型和位置,客戶端定位對象所使用的名稱和.NET的遠程基礎架構對對這一對象調用的處理方式。我們可以通過下面的代碼獲取對象的類型:
Type objectType = new MyCoolObject().GetType()
通過下面的代碼就可以向.NET遠和基礎架構注冊該對象:
RemotingConfiguration.RegisterWellKnownServiceType(
objectType, "MyCoolObject",
WellKnownObjectMode.Singleton);
對象的調用有二種處理方式:Singleton和SingleCall。在Singleton方式中,在第一次客戶端方法調用時創建對象,並保持對象存在直到客戶端中止連接或對象自然死亡;在SingleCall方式中,每次客戶端的方法調用都會創建對象,對象只在方法調用持續期間存在,一旦方法調用結束,對象就會死亡。SingleCall方式中,客戶端連接不會隨方法調用的結束而中止,只有對象會隨著方法調用的結束而被殺死。
為遠程對象建立客戶端
對客戶端的第一個要求是遠程對象的類要在客戶端的本機上,.NET遠程執行基礎架構代理將使用它對與遠程對象間傳遞的信息進行解釋和裝配。
需要再次建立一個通道,然後向.NET遠程基礎架構進行注冊,使該通道成為可用的:
TcpChannel myChannel = new TcpChannel();
ChannelServices.RegisterChannel(myChannel);
需要注意的是,我們在創建通道時沒有指定端口地址。我們將在要求服務器給出我們要調用的遠程對象的引用時指定端口地址,代碼如下所示:
MyCoolObject mine = (MyCoolObject)Activator.GetObject(
typeof(MyCoolObject),
"tcp://localhost:8085/MyCoolObject");
第一個參數獲取我們要定位的對象的類型,第二個參數指定遠程對象的URL。一旦我們得到該對象,就需要將其類型由普通的對象轉換為MyCoolObject類型。
現在,我們得到了位於服務器端的遠程對象的引用,我們可以將該對象看作是本地的對象。
功能強大的任務服務器
任務服務器是一個如何利用.NET的按值傳遞遠程對象機制的實例。任務服務器有一個名字為TaskRunner的對象,客戶端可以獲取它的引用,TaskRunner可以從客戶端接受任何實現ITask界面的對象。
ITask界面強迫客戶端使用Run()和Identify()二個方法創建任務對象,客戶端然後就可以創建一個復雜的、對資源敏感的實現ITask界面的任務,然後將它提交給任務服務器,在它自己的應用域中執行。這意味著沒有充足計算資源的客戶端可以充分利用任務服務器的資源執行自己無力完成的、比較復雜的任務。
因此,需要計算有3000位小數的圓周率的客戶端就可以創建一個完成相關計算和實現ITask界面的對象,該任務然後被使用TaskRunner對象提交給任務服務器執行。
ITask界面強迫客戶端從它們的Run()方法返回一個對象,當然,該對象的值可能為空,但也有可能會包含有意義的值。
簡單地說,按值傳遞的遠程執行最適合這樣一種情況,即服務器沒有客戶端希望在遠程環境中執行的對象的明確的表示。
按值傳遞對象
一個對象被按值傳遞時,它需要被轉換成一種可以傳輸的格式。一個應用域中的對象占用著一塊系統內存,而且能夠對其他對象發給它的消息作出反應。要將該對象傳遞到其他應用域中,必須將它串行化。這意味著它必須能夠被分解為字節流,然後能夠在目的地被重新組合為原來的對象。
要實現這一點,就必須使用編譯器可串行化的特性,使編譯器對一些結構進行呂行化處理。我們可以將整個類標記為可串行化的,也可以將包括一些選定的方法在內的類的一部分標記為可串行化的。在任務服務器環境中,整個的客戶端任務對象必須能夠被串行化。代碼如下所示:
Using System;
?[Serializable()]
class ClientTask {
?
需要注意的是,如果一個對象有外部的庫文件等附屬的部分,被該對象使用的部分也必須被串行化,這是在遠程應用域中重新建立對象所必需的。
運行例子程序
這個例子是用C# for .NET Beta 2編寫的,並在C# for .NET Beta 2中通過了測試。
為了使用任務服務器,我們必須以下面的順序對程序進行編譯:
csc /target:library /out:ITask.dll ITask.cs
csc /target:library /out:TaskRunner.dll TaskRunner.cs
csc /r:ITask.dll /r:TaskRunner.dll TaskServer.cs
csc /r:ITask.dll /r:TaskRunner.dll TaskClient.cs
上面的順序非常重要,因為TaskServer和TaskClient類要用到TaskRunner和ITask庫。
要運行這個例子程序,首先啟動服務器代碼,並等待出現下面的提示:
[i] Task Server Started, press to exit?
然後就可以運行客戶端程序了。
在TaskClient類中有一個實現ITask界面而且能夠可串行化的類,該任務將求出二個數的積,並返回一個int類型的對象。我建議讀者創建自己的任務,並試著在任務服務器上運行它,這將使你親身體會到這種分布式計算的巨大威力。它可以使客戶端軟件開發人員充分利用任務服務器上豐富的資源完成復雜的、需要大量資源的任務。
我們可以利用任務服務器完成諸如數字處理、壓縮、加密、排序等各種操作。
正面是本文中例子所涉及的源代碼,及其簡單的注解,供有興趣的讀者參考:
// ITask.cs
// 用戶可以使用該界面來創建自己的任務,它完成下面的二種操作:
// ·服務器可以通過調用方法Run()來運行建立的任務。
// ·客戶端可以保證任務從方法Run()中啟動。
// 其中還有一個Identify()方法,服務器用它顯示一些有關任務的信息。
// 該界面被編譯為同一個名字空間下的單獨的庫文件,使得任務服務器的管理員能夠將該界面作為所有能夠在//他的任務服務器上運行的任務的契約進行分發。
// 客戶端將繼承該類,創建自己的任務對象,提交給服務器運行。
namespace TaskServer {
// 必須將它定義為一個界面
public interface ITask {
object Run();
string Identify();
}
}
// TaskRunner.cs
// 該對象用來運行由客戶端提交的任務,提交的任務將在服務器的應用域執行。
// TaskRunner對象以引用的方式傳遞給客戶端,無需對它進行串行化
// TaskRunner接受所有實現ITask界面的任務,它需要二個參數:Run()和Identify()。
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
namespace TaskServer {
public class TaskRunner : MarshalByRefObject {
ITask remoteTaskObject;
public TaskRunner() {
Console.WriteLine("\n[i] TaskRunner activated");
}
public string LoadTask(ITask task) {
Console.WriteLine("\n[i] Loading new task...");
if(task == null) {
Console.WriteLine("[e] Task reference is NULL. Task not Loaded.");
return "[e] Task not loaded.";
}
remoteTaskObject = task;
Console.WriteLine("[i] Task has been loaded.");
Console.WriteLine("[i] Task ID: " + remoteTaskObject.Identify() + "\n");
return "[i] Task loaded. Welcome to the All Powerful TaskServer.";
}
public object RunTask() {
Console.WriteLine("\n[i] Running the task...");
object result = remoteTaskObject.Run();
Console.WriteLine("[i] Task finished.");
return result;
}
}
}
// TaskServerEngine.cs
// 這個類用來啟動任務服務器應用程序。它建立C#遠程執行的後端,加載通道,注冊TaskRunner對象,然後讓 //遠程執行機制的後端監測TCP通道上的連接請求
using System;
using System.IO;
// 下面的庫用於向遠程執行機制注冊我們的對象
using System.Runtime.Remoting;
// 下面的庫用於向通道服務注冊我們的TCP通道設備
using System.Runtime.Remoting.Channels;
// 該庫提供了用來與遠程應用域(客戶端)通訊所需要的TCP通道
using System.Runtime.Remoting.Channels.Tcp;
namespace TaskServer {
public class TaskServerEngine {
// 我們只需要一個方法,它可以是靜態的,因為我們不需要建立這個類的實例,該方法的作用僅僅是 //創建並創始化TaskServerEngine。
public static void Main() {
// 向用戶表明我們正在啟動服務器類
Console.WriteLine("The All Powerful Task Server!");
Console.WriteLine("....\"Your Task is My Command\"....");
Console.WriteLine("\n[i] Starting Task Server...");
try {
// 創建TCP通道
Console.WriteLine("[i] -- Creating TCP channel");
TcpChannel chan = new TcpChannel(8085);
// 向.NET的遠程服務注冊新創建的TcpChannel,使客戶端可以使用這一服務
Console.WriteLine("[i] -- Registering TCP channel");
ChannelServices.RegisterChannel(chan);
//向遠程機制的後端注冊TaskRunner對象,RegisterWellKnownServiceType方法將完成這一 //操作,並接收下面的參數:
// *內容為TaskRunner對象映象的類型對象
// * 向客戶公布的服務名字
// * 對象的模式:Singleton意味著所有客戶端請求共享一個對象服務;SingleCall意味著 // 每個客戶端請求使用一個新的對象服務。
Console.WriteLine("[i] -- Registering the TaskRunner object");
Type theType = new TaskRunner().GetType();
RemotingConfiguration.RegisterWellKnownServiceType(theType, "TaskServer", WellKnownObjectMode.Singleton);
Console.WriteLine("[i] Task Server started, press to exit...");
Console.ReadLine();
} catch (Exception e) {
Console.WriteLine("[!] An error occured while initialising the TaskServerEngine.");
Console.WriteLine(e);
}
}
}
}
// TaskClient.cs
// 這是一個客戶端應用域,客戶端的任務是建立一個任務對象,並將它提交給任務服務器
using System;
// 建立與任務服務器的連接所必需的庫文件
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using TaskServer;
namespace HappyClientWithTask {
[Serializable()]
// 下面是我們創建的任務
class ClientTask : ITask {
private int num1, num2;
private int result;
public ClientTask(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
}
public object Run() {
result = num1 * num2;
return (object)result;
}
public string Identify() {
return("I am a multiplication task.");
}
}
public class Client {
public static void Main() {
Console.WriteLine("\nWelcome to the humble, lowly client Application Domain.\n");
ClientTask clientTask = new ClientTask(100,100);
try {
Console.WriteLine("[i] Connecting to TaskServer...");
Console.WriteLine("[i] - Opening TCP Channel");
TcpChannel chan = new TcpChannel();
Console.WriteLine("[i] - Registering the channel");
ChannelServices.RegisterChannel(chan);
Console.WriteLine("[i] Connected to TaskServer");
// 從TaskServer中獲取TaskRunner對象的引用
// Activator類中的GetObject方法需要二個參數:
// * 對象類型
// * 對象的URI位置
Console.WriteLine("[i] Getting a reference to the TaskRunner Object");
TaskRunner taskRunner = (TaskRunner)Activator.GetObject(typeof(TaskServer.TaskRunner),
"tcp://localhost:8085/TaskServer");
if(taskRunner == null) {
Console.WriteLine("[e] Could not locate server.");
Console.WriteLine("[i] Exiting...");
return;
}
Console.WriteLine("[i] We have an object reference!");
// 下面我們將把任務對象傳遞給任務服務器
Console.WriteLine("\n[i] Submitting our task to the server...");
string response = taskRunner.LoadTask(clientTask);
Console.WriteLine("[i] Server says: " + response);
Console.WriteLine("\n[i] Running the task and awaiting feedback...");
object result = taskRunner.RunTask();
Console.WriteLine("[aaa-uuuum] The Great and Powerful TaskServer Says: " + (int)result);
} catch (Exception e) {
Console.WriteLine("[e] An exception occurred.");
Console.WriteLine(e);
}
}
}
}