.NET Remoting 的一个简单但实用的示例 第 2 部分






4.84/5 (15投票s)
通过一个简单但可能实用的示例介绍 .NET Remoting。
引言
本文是我之前题为“简单但可能实用的 .NET Remoting 示例”的文章的后续。在我之前的文章中,我阐述了启动 .NET Remoting 项目所需的基本代码。该文章的示例代码使用了**服务器激活**的远程对象。在本文中,我们将注意力转向**客户端激活**的远程对象,它们与服务器激活的对象不同,并且通常更有用。
在本文中,我将讨论以下概念
- 服务器激活和客户端激活远程对象之间的区别。
- 将对象传递给客户端时序列化的必要性。
- 对象封送、参数化和返回。
- 远程对象的生命周期。
再次提供了示例代码,其功能与第一篇文章相同,即演示在远程计算机上启动进程。我提供了这样一个示例,因为此类程序实用且经常用于需要远程控制的真实系统中。我希望读者能利用我的示例代码进一步实验,并提出一个真正专业且可行的解决方案。
服务器激活和客户端激活远程对象之间的区别
服务器激活和客户端激活远程对象之间的主要区别在于,服务器激活对象无法为客户端保留**状态**。**“单例”**服务器激活对象是例外。让我们一步一步地探索这一点。
服务器激活对象有两种:**“单次调用”**和**“单例”**模式。有关更多详细信息,请查阅 MSDN 文档中的 `WellKnownObjectMode` 枚举。
基本上,使用 `SingleCall` 模式规定,对于对象上的每个方法调用,都会创建一个远程对象的新实例。即使对象暴露了许多方法,也是如此。每次方法调用后,远程对象都会被销毁。
既然如此,不难想象这样的对象无法为客户端保留任何状态。
使用 `Singleton` 模式规定,每个传入的方法调用都由服务器端的同一个对象实例提供服务。此对象首先在客户端第一次方法调用时实例化。这意味着所有客户端将共享一个单一对象以获取所有服务。
这种对象在某种程度上**可以**为客户端保留状态。必须采用一些复杂的机制来确保每个客户端都保留一些由单例远程对象维护的私有数据。
这当然是可能的,但维护起来可能很繁琐。另一个缺点是,这种安排没有考虑到没有生命周期管理系统来确保如果客户端突然终止,服务器将能够清理为客户端预留的资源。.NET Remoting 系统专门为客户端激活的远程对象提供远程对象生命周期管理支持,这使得它更具吸引力。
**客户端激活对象**登场。请注意,这些对象是真正的远程对象。它们在**服务器端**而不是客户端实例化。一旦创建,它们就是私有离散对象,通常会一直存活,直到客户端不再需要引用它,之后对象会像往常一样被**垃圾回收器**销毁(我们将在本文后面检查远程对象生命周期)。每个创建的对象不与其他客户端共享。
这使得客户端激活对象真正为客户端保留状态。它们可以正常使用,尽管它们是在远程机器上创建和维护的。
这为我们的示例程序 (ProcessActivator) 带来了一些有用的优势。通过客户端激活方法,`ProcessActivator` 对象可用于在远程服务器端运行单个进程。同一个对象可以重复使用,以对同一个远程进程执行操作(例如,获取其所有加载的 DLL、获取其内存使用状态等)。
我们将在下面几节中研究源代码时检查这些内容。
新示例代码
新接口
我们当前示例代码的基本功能与我的第一篇文章中的相同:它在远程机器上启动进程并带有命令行参数。对于新代码,我创建了一个名为“`IProcessActivator_ClientActivated`”的新接口,以避免与第一个示例代码的“`IProcessActivator`”接口可能混淆。
在“`IProcessActivator_ClientActivated`”接口中,我定义了 `Run()` 方法,该方法与第一个示例代码具有完全相同的签名和规范,即它在服务器机器上启动一个进程。进程的名称在 `Run()` 的第一个参数中提供,命令行参数在第二个参数中提供。
作为示例代码的增强,并作为展示客户端激活远程对象有状态性的一种方式,我定义了一个名为 `GetModules()` 的新方法,该方法指定返回一个对象数组,每个对象都包含 `Run()` 启动的进程中加载模块(DLL、exe 等)的信息。
为此,`GetModules()` 方法指定返回一个包含实现“`IModuleEntry`”接口的对象的 `ArrayList`。让我们看看我们的“`IProcessActivator_ClientActivated`”和“`IModuleEntry`”接口。您可以在 `IProcessActivator_ClientActivated` 文件夹中 `IProcessActivator_ClientActivated.sln` 项目的 `IProcessActivator_ClientActivated.cs` 源文件中找到这些内容。
public interface IModuleEntry : ISerializable
{
IntPtr BaseAddress
{
get;
}
IntPtr EntryPointAddress
{
get;
}
string FileName
{
get;
}
int ModuleMemorySize
{
get;
}
string ModuleName
{
get;
}
}
public interface IProcessActivator_ClientActivated
{
bool Run(string strProgramName, string strArgumentString);
ArrayList GetModules();
}
如前所述,`IProcessActivator_ClientActivated.GetModules()` 方法返回一个 `ArrayList`,其中包含实现 `IModuleEntry` 接口的对象。每个此类对象旨在存储由 `Run()` 方法启动的进程中加载模块(DLL、exe)的信息。
`IModuleEntry` 接口定义了五个只读属性,它们是
- `BaseAddress` - 这是模块加载的内存地址。
- `EntryPointAddress` - 这是系统加载和运行模块时运行的函数的内存地址(例如:`DllMain()`)。
- `FileName` - 这是模块的完整路径。
- `ModuleMemorySize` - 这是加载模块所需的内存量。
- `ModuleName` - 这是进程模块的名称。
这些只是 `IModuleEntry` 可以定义的一些示例属性。根据个人需求,还可以定义更多。请查阅 MSDN 文档中 `ProcessModule` 类以获取更多详细信息和想法。
服务器代码
服务器代码已增强,包含新的实现代码以及托管客户端激活远程对象所需的代码。让我们从研究 `ProcessActivator_ClientActivated` 类开始检查代码。
`ProcessActivator_ClientActivated` 类像往常一样派生自 `MarshalByRefObject`。它还实现了 `IProcessActivator_ClientAct` 接口。
class ProcessActivator_ClientActivated : MarshalByRefObject,
IProcessActivator_ClientActivated.IProcessActivator_ClientActivated
{
private Process m_process = new Process();
public ProcessActivator_ClientActivated()
{
Console.WriteLine("ProcessActivator_ClientActivated constructor.");
}
...
...
...
}
`ProcessActivator_ClientActivated` 类定义了一个名为 `m_process`(类型为 `Process`)的 private
成员数据。`m_process` 成员数据用于保存新的 `Process` 类实例。
为了帮助证实为每个客户端创建了一个单独的对象,我在构造函数代码中添加了控制台输出字符串,以显示何时调用构造函数。这纯粹是为了测试和演示目的。读者可以将其删除而没有任何后果。
新的 `Run()` 方法。
public bool Run(string strProgramName, string strArgumentString)
{
bool bRet = false;
m_process.StartInfo = new ProcessStartInfo(strProgramName,
strArgumentString);
bRet = m_process.Start();
if (bRet)
{
Console.WriteLine("Program {0} started.", strProgramName);
m_process.WaitForInputIdle();
Console.WriteLine("Program {0} now in idle mode.",
strProgramName);
}
return bRet;
}
`Run()` 方法已从以前的版本增强,以使用 `Process.Start()` 方法的非静态实现。`m_process` 成员数据此后将保存新创建进程的状态。现在,我添加的额外代码是调用 `Process.WaitForInputIdle()` 方法(上面以粗体突出显示)。
请注意 `WaitForInputIdle()` 方法的文档
“使用 `WaitForInputIdle` 强制应用程序的处理等待,直到消息循环返回到空闲状态。当具有用户界面的进程执行时,每当操作系统向进程发送 Windows 消息时,其消息循环就会执行。然后进程返回到消息循环。当进程在消息循环中等待消息时,据说它处于空闲状态。此状态很有用,例如,当您的应用程序需要等待启动进程完成其主窗口的创建,然后应用程序才能与该窗口通信时。
如果进程没有消息循环,`WaitForInputIdle` 将立即返回
false
。”
调用 `WaitForInputIdle()` 对我们的 `ProcessActivator` 代码很重要,因为它强制 `Run()` 等待直到启动进程稳定并且不再加载任何模块(DLL、OCX 等)。
这使得稍后对 `GetModules()` 的任何调用都能正常成功。如果我们没有调用 `WaitForInputIdle()`,则立即调用 `GetModules()`(在调用 `Run()` 之后)可能会导致异常(例如 `IExplore.exe`)。请注意这个小而重要的事实。鼓励读者暂时注释掉 `WaitForInputIdle()` 调用并了解负面影响。
新的 `GetModules()` 方法
public ArrayList GetModules()
{
ProcessModuleCollection pmc = m_process.Modules;
ArrayList ar_ret = new ArrayList();
int i = 0;
Console.WriteLine("GetModules() started."); /* For testing purposes. */
for (i = 0; i < pmc.Count; i++)
{
ar_ret.Add(
(object)(new ModuleEntry(
pmc[i].BaseAddress,
pmc[i].EntryPointAddress,
pmc[i].FileName,
pmc[i].ModuleMemorySize,
pmc[i].ModuleName
)
));
}
Console.WriteLine("GetModules() ended.");
/* For testing purposes. */
return ar_ret;
}
`GetModules()` 方法的实现使用 `m_process` 的 `Modules` 属性向我们返回一个 `ProcessModuleCollection` 集合对象。此集合对象包含 `ProcessModule` 对象。每个 `ProcessModule` 表示加载到特定进程中的 `.dll` 或 `.exe` 文件。我们使用每个 `ProcessModule` 对象向我们返回有关每个模块的信息,并构建我们的 `IModuleEntry` 对象数组。
正如从 `GetModules()` 方法中可以看出,每个 `ProcessModule` 对象中包含的信息被提取并存储在一个单独的 `ModuleEntry` 对象中。每个 `ModuleEntry` 对象都是 `IModuleEntry` 接口的实现。
这些 `ModuleEntry` 对象中的每一个都被插入到一个 `ArrayList` 对象中,然后该对象返回给调用者。
现在,您可能会想:为什么费心定义 `IModuleEntry` 接口,然后开发此接口的具体实现(`ModuleEntry` 类),然后创建一个 `ModuleEntry` 对象的 `ArrayList` 以返回给 `GetModules()` 调用者?
为什么不简单地让 `GetModules()` 方法返回一个 `ProcessModuleCollection` 集合对象呢?这肯定会使 `GetModules()` 的生活简单得多。原因是 `ProcessModuleCollection` 集合类**未标记为可序列化**。我们接下来研究 `ModuleEntry` 类时会详细介绍这一点。
`ModuleEntry` 类
[Serializable]
class ModuleEntry : IModuleEntry
{
private IntPtr m_ipBaseAddress;
private IntPtr m_ipEntryPointAddress;
private string m_strFileName = "";
private int m_iModuleMemorySize = 0;
private string m_strModuleName = "";
public ModuleEntry
(
IntPtr ipBaseAddress,
IntPtr ipEntryPointAddress,
string strFileName,
int iModuleMemorySize,
string strModuleName
)
{
m_ipBaseAddress = ipBaseAddress;
m_ipEntryPointAddress = ipEntryPointAddress;
m_strFileName = strFileName;
m_iModuleMemorySize = iModuleMemorySize;
m_strModuleName = strModuleName;
}
private ModuleEntry
(
SerializationInfo info,
StreamingContext context
)
{
m_ipBaseAddress = (IntPtr)info.GetValue("m_ipBaseAddress",
typeof(IntPtr));
m_ipEntryPointAddress = (IntPtr)info.GetValue("m_ipEntryPointAddress",
typeof(IntPtr));
m_strFileName = (string)info.GetValue("m_strFileName", typeof(string));
m_iModuleMemorySize = (int)info.GetValue("m_iModuleMemorySize",
typeof(int));
m_strModuleName = (string)info.GetValue("m_strModuleName", typeof(string));
}
...
...
...
}
回想 `IModuleEntry` 接口的原始规范
public interface IModuleEntry : ISerializable
这意味着任何 `IModuleEntry` 的实现也必须实现 `ISerializable` 接口。这至关重要,因为任何用作远程方法参数或返回值一部分的对象都必须是可序列化的。
现在,因为 `ModuleEntry` 类实现了 `ISerializable` 接口,所以定义了 `GetObjectData()` 方法和特殊的 private
构造函数(它接受 `SerializationInfo` 和 `StreamingContext` 参数)。
我已尝试使 `ISerializable` 接口实现尽可能简单,以避免让初级读者对不必要的担忧感到不知所措。实际上,甚至可以在不实现 `ISerializable` 接口的情况下定义 `IModuleEntry` 接口。但是,`ModuleEntry` 类必须保持可序列化,因此只需将 `[Serializable]` 属性应用于 `ModuleEntry` 类即可。
`ModuleEntry` 类的其余部分大部分是 `IModuleEntry` 接口的 "get
" 访问器的简单(甚至微不足道的)实现。实际属性由类的 private
成员数据实现
private IntPtr m_ipBaseAddress;
private IntPtr m_ipEntryPointAddress;
private string m_strFileName = "";
private int m_iModuleMemorySize = 0;
private string m_strModuleName = "";
我还为 `ModuleEntry` 创建了一个方便的构造函数,它接受所有五个成员数据的值。此构造函数将在 `GetModules()` 方法中使用。它是一个简单的类,用于保存模块数据。没有多余的功能。
`Main()` 函数和远程对象托管代码
服务器的 Main()
函数在代码片段之后描述
static void Main(string[] args)
{
TcpServerChannel channel = new TcpServerChannel(9000);
HttpServerChannel http_channel = new HttpServerChannel(8000);
ChannelServices.RegisterChannel(channel);
ChannelServices.RegisterChannel(http_channel);
ActivatedServiceTypeEntry remObj =
new ActivatedServiceTypeEntry(typeof(ProcessActivator_ClientActivated));
RemotingConfiguration.ApplicationName = "ProcessActivator_ClientActivated";
RemotingConfiguration.RegisterActivatedServiceType(remObj);
Console.WriteLine("Press [ENTER] to exit.");
Console.ReadLine();
}
如我的第一个示例代码中所用,我定义了两个通道,用于传输对 `ProcessActivator_ClientActivated` 对象的任何请求。
请注意客户端激活的远程对象服务器执行对象托管的方式。以前,在我们的服务器激活的远程对象托管代码中,我们创建了一个 `WellKnownServiceTypeEntry` 对象,然后通过 `RemotingConfiguration.RegisterWellKnownServiceType()` 方法调用注册它。
在我们的新客户端激活远程对象托管代码中,我们创建一个 `ActivatedServiceTypeEntry` 对象,在构造函数中指定远程对象的类型,然后通过 `RemotingConfiguration.RegisterActivatedServiceType()` 方法调用注册它。我们还通过 `RemotingConfiguration.ApplicationName` 属性指定远程对象的 URI。此 URI 在我们的服务器中设置为“`ProcessActivator_ClientActivated`”。
另请注意,在构建 `ActivatedServiceTypeEntry` 对象时,URI 未与远程对象的类型绑定。这与服务器激活的示例代码不同,在服务器激活的示例代码中,URI 与 `WellKnownServiceTypeEntry` 对象一起指定
WellKnownServiceTypeEntry remObj = new WellKnownServiceTypeEntry
(
typeof(ProcessActivator),
"ProcessActivator",
WellKnownObjectMode.SingleCall
);
稍后,在服务器激活远程对象的客户端代码中,当我们执行以下操作时
IProcessActivator.IProcessActivator process_activator =
(IProcessActivator.IProcessActivator)Activator.GetObject
(
typeof(IProcessActivator.IProcessActivator),
"tcp://:9000/ProcessActivator"
);
URL 参数的 URI 部分(即 `ProcessActivator`)将返回值(存储在 `process_activator` 中)绑定到 URI 为 `ProcessActivator` 的特定远程对象。
我们将在稍后的客户端代码部分中检查客户端连接到客户端激活远程对象的方式。
服务器代码的其余部分与第一个示例代码相同。也就是说,我们只是等待用户按下 ENTER 键,之后整个服务器应用程序结束。
接下来我们将检查客户端代码。
客户端代码
客户端代码如下所示
class ProcessActivator_ClientActivated_Client
{
static RealProxy m_real_proxy = null;
static object m_transparent_proxy = null;
static ILease m_lease = null;
...
...
...
/* The main entry point for the application. */
[STAThread]
static void Main(string[] args)
{
try
{
UrlAttribute[] attr = { new
UrlAttribute(@"https://:8000/ProcessActivator_ClientActivated") };
ObjectHandle object_handle = null;
IProcessActivator_ClientActivated.IProcessActivator_ClientActivated
process_activator = null;
ArrayList process_module_collection = null;
...
...
...
/* Create a client channel from which to receive the Remote Object. */
TcpClientChannel tcp_channel = new TcpClientChannel();
/* Register the channel. */
ChannelServices.RegisterChannel(tcp_channel);
/* During the following call to Activator.CreateInstance(), the constructor
of the class that implements the IProcessActivator_ClientActivated
interface will be invoked. */
object_handle = Activator.CreateInstance
(
"ProcessActivator_ClientActivated",
"ProcessActivator_ClientActivated.ProcessActivator_ClientActivated",
attr
);
/* Unwrap the delivered object and cast it to the
IProcessActivator_ClientActivated interface.*/
process_activator
= (IProcessActivator_ClientActivated.IProcessActivator_ClientActivated)
(object_handle.Unwrap());
/* We can now get hold of the ILease object within the newly created
Remote Object. */
m_real_proxy = RemotingServices.GetRealProxy(process_activator);
m_transparent_proxy = m_real_proxy.GetTransparentProxy();
m_lease
= (ILease)(((MarshalByRefObject)m_transparent_proxy).GetLifetimeService());
...
...
...
/* Start up a process in the remote machine site. */
process_activator.Run("notepad.exe", "");
/* Sleep for 270 seconds (4 and a half minutes). */
/* The timer will continue to monitor the Remote Object lease. */
Thread.Sleep(270 * 1000);
/* After the sleeping period, we attempt to call a remote method. */
process_module_collection = process_activator.GetModules();
/* Perform processing of the returned value from GetModules(). */
for (int i = 0; i < process_module_collection.Count; i++)
{
IModuleEntry me = (IModuleEntry)process_module_collection[i];
Console.WriteLine(me.FileName);
}
...
...
...
}
catch(FileNotFoundException ex)
{
Console.WriteLine(ex.FileName);
}
catch(TargetInvocationException tie)
{
Console.WriteLine(tie.Message);
}
return;
}
}
在上面的代码片段中,为了清晰起见,我故意过滤掉了一些代码。虽然与服务器激活远程对象(在我之前的示例中)的客户端代码相比,客户端代码看起来稍微复杂一些,但请注意,客户端源代码的一半与远程对象生命周期管理及其演示有关,另一部分与实际远程对象创建有关。
简而言之,对于客户端激活的远程对象调用,客户端调用 `Activator.CreateInstance()` 方法来创建远程对象的实例。通过 `CreateInstance()` 实例化的这个远程对象将一直存在,直到所谓的**租约时间**到期(从而导致垃圾回收发生,进而销毁远程对象)。我们将在稍后讨论租约机制。
请注意,并非所有重载的 `Activator.CreateInstance()` 方法都可以用来创建远程对象。其中大多数用于创建本地对象。要创建远程对象,我们需要使用 `CreateInstance()` 的一个版本,其中可以传入**激活属性**。
此类激活属性在 MSDN 文档中最好地描述为:“**一个或多个可以参与激活的属性数组**”。
在 `CreateInstance()` 的三个版本中,我们将使用最简单的一个,它接受三个参数
public static ObjectHandle CreateInstance(
string assemblyName,
string typeName,
object[] activationAttributes
);
我们通过以下方式调用此函数
UrlAttribute[] attr =
{ new UrlAttribute(@"https://:8000/ProcessActivator_ClientActivated") };
object_handle = Activator.CreateInstance
(
"ProcessActivator_ClientActivated",
"ProcessActivator_ClientActivated.ProcessActivator_ClientActivated",
attr
);
在我们的代码中,我们使用“`ProcessActivator_ClientActivated`”作为**程序集**的名称,在该程序集中查找名为“`ProcessActivator_ClientActivated.ProcessActivator_ClientActivated`”的类型。
请注意,无论我们指定什么程序集名称作为第一个参数,.NET 框架都必须能够找到它。因此,因为我已将此参数指定为“`ProcessActivator_ClientActivated`”,所以我已将“`ProcessActivator_ClientActivated.exe`”包含在与客户端可执行文件“`ProcessActivator_ClientActivated_Client.exe`”相同的目录中。
虽然远程对象的实际类型名称“`ProcessActivator_ClientActivated.ProcessActivator_ClientActivated`”必须在 `CreateInstance()` 中指定(事实上,我认为这是相当必要的)并没有困扰我,但令我非常困惑的是,为什么 .NET Framework 必须找到远程对象的程序集才能使 `CreateInstance()` 工作。请阅读稍后关于**“关于 Activator.CreateInstance() 的一些评论”**的部分,以对该主题进行更深入的分析。
参数“`attr`”是 `UrlAttribute` 对象的数组。此 `UrlAttribute` 对象帮助我们指定远程对象的通道和对象名称。因此,毫不奇怪,对于 `UrlAttribute` 的构造函数,我们指定了
https://:8000/ProcessActivator_ClientActivated.
HTTP 协议、主机地址(我们示例代码中的 localhost)和端口号 (8000) 是使用的通道,然后远程对象的名称是“`ProcessActivator_ClientActivated`”。
关于客户端激活远程对象创建的下一个值得注意的事情是,从 `CreateInstance()` 调用返回了一个 `ObjectHandle`。`ObjectHandle` 最好由 MSDN 文档描述为**“..由远程生命周期服务跟踪的远程 `MarshalByRefObject`...”**
从 `CreateInstance()` 方法返回的 `ObjectHandle` 对象必须解包才能检索包含的实际远程对象。实际上,包含的远程对象仍然是在远程站点上创建的真实远程对象的代理。
解包后,我们仍然需要将返回值(类型为 object
)强制转换为 `IProcessActivator_ClientActivated` 类型
process_activator =
(IProcessActivator_ClientActivated.IProcessActivator_ClientActivated)
(object_handle.Unwrap());
接下来我们可以调用 `IProcessActivator_ClientActivated` 接口的任何暴露方法。
关于 Activator.CreateInstance() 的一些评论
回想 `Activator.CreateInstance()` 方法。我之前提到过,我认为必须将远程对象“`ProcessActivator_ClientActivated.ProcessActivator_ClientActivated`”的实际类型名称指定为参数。
这与服务器调用 `RemotingConfiguration.ApplicationName` 中指定的 URI 以及客户端 `UrlAttribute` 传递给 `CreateInstance()` 的 URI 未与远程对象的实际类型绑定这一事实是一致的。
相同的 `UrlAttribute` 可以在另一个 `CreateInstance()` 调用中使用,第二个参数指定不同的类型名称。
在我看来,这种设计也符合 C++ 和 COM 中对象创建和接口实现的设计。
在 C++ 中,接口指针实例化为具体实现,如下所示
IMyInterface* pIMyInterface = new CMyInterfaceImpl();
请注意对实际实现类名称(`CMyInterfaceImpl`)的引用。
在 COM 中,使用了类似的方式
::CoCreateInstance(CLSID_MyClassID, NULL, CLSCTX_INPROC_SERVER , IID_IMyInterface, (LPVOID *)&pIMyInterface);
实现 `IMyInterface` 接口的 COM 对象的类 ID 必须为客户端所知。
这种指示实际具体类(或类型)名称的样式在 `Activator.CreateInstance()` 中得以延续。程序集名称本身也必须指定这一事实仅仅表明程序集名称将用作实际类型本身标识符的一部分。因此,程序集名称“`ProcessActivator_ClientActivated`”和具体类型名称“`ProcessActivator_ClientActivated.ProcessActivator_ClientActivated`”都被一起用于标识要远程实例化的具体 .NET 类。
在 C++、COM 和 .NET Remoting 中,实际实现标识符(例如,类名 `CMyInterfaceImpl`、类 ID `CLSID_MyClassID` 或 .NET 类型“`ProcessActivator_ClientActivated.ProcessActivator_ClientActivated`”)无需硬编码,并且可以从配置文件(INI 文件或 XML 文件等)中读取。
这确保了后期绑定和可配置的可能性。
然而,令我非常困扰的是,.NET Framework 必须在本地找到实际的程序集,`CreateInstance()` 才能工作。
经过一番思考并与同事讨论后,我提出了以下一些理论
必须指定程序集名称这一事实表明了 Remoting 的最初预期用途之一,即它应仅限于封闭的 LAN/内网分布式系统。
Remoting 对象不打算供“外部世界”使用。只有 Web 服务才暴露给外部世界。要使用远程对象,您的本地客户端系统必须拥有服务器使用的实际程序集的副本。
我宁愿不要求实际的程序集,或者可以指定远程程序集的路径。但据我目前所知,这两种情况都不可能。
.NET Framework 对此程序集有什么用途,我目前确实不知道。如果读者中有谁了解这种用途,请与我们分享 :-)
对象封送、参数化和返回
请注意,远程方法调用的参数类型不限于基本数据类型。方法返回值也同样不受限制,正如我们可以返回 `ModuleEntry` 对象的 `ArrayList` 所见。
在 .NET Remoting 的上下文中,我们必须区分两种类型的类
- **按值封送的类** - 此类实例被序列化并通过远程处理通道传递。这些类必须实现 `ISerializable` 接口或使用 `[Serializable]` 属性进行标记。请注意,从此类实例化的对象完全通过通道封送给客户端。它们独立于它们来自的服务器。按值封送的类也称为**未绑定类**,因为它们不包含任何依赖于它们被封送的应用域的数据。
- **按引用封送的类** - 此类实例具有所谓的**远程身份**。这些对象不会通过网络传递,而是将代理返回给客户端。此类必须派生自 `MarshalByRefObject` 类。`MarshalByRefObject` 实例也称为**应用程序域绑定对象**,因为它们实际上只存在于它们创建的应用程序域中。
通过上述定义,我希望事情变得更清楚,我们看到各个部分是如何组合在一起的。在我们的示例代码中,`ProcessActivator_ClientActivated` 类派生自 `MarshalByRefObject` 类,因此按引用封送给客户端。客户端接收服务器中创建的实际对象的代理。
我们的 `ModuleEntry` 对象 `ArrayList` 按值封送。它们当然是首先在服务器中创建的(在 `GetModules()` 方法期间)。之后,它们完全封送给客户端。实际的 `ModuleEntry` 对象在客户端重新创建。这就是 `ModuleEntry` 类实现 `ISerializable` 接口的原因。
远程对象生命周期
如果没有讨论远程对象生命周期管理,关于客户端激活远程对象的文章就不完整。为什么这个话题很重要?那么,客户端和服务器如何知道对方是否不再可用?
如果服务器在客户端上崩溃,一旦客户端对远程对象进行方法调用,就会抛出 `System.Runtime.Remoting.RemotingException` 类型的异常。在这种情况下,客户端应处理此异常并决定最佳操作方案(例如,重新创建对象等)。
服务器的情况可能更严重。如果客户端崩溃,服务器通常无法检测到这一点(除非已建立某种预定义的相互生命检测机制,例如 ping)。这可能导致未使用的和未释放的资源堆积。
对于远程对象生命周期管理,.NET Framework 提供了**租赁分布式垃圾回收器 (LDGC)**。对于在创建它的应用程序域之外引用的每个客户端激活远程对象,都会创建一个所谓的租赁。此租赁具有租赁时间,当它到期时,远程对象将断开连接并进行垃圾回收。
在生命周期管理的上下文中,必须引入几个术语
- **LeaseTime** - 这指的是远程对象存活的时间长度。默认值为 300 秒。
- **RenewOnCallTime** - 这是远程方法调用被调用后(如果当前租赁时间低于此值)租赁重置的时间。默认值为 120 秒。
- **SponsorshipTimeout** - 这是远程处理基础结构搜索可用赞助商的时间。默认值为 120 秒。
- **LeaseManagerPollTime** - 这定义了租赁管理器执行租赁到期对象搜索的时间间隔。默认值为 10 秒。
我们肯定关心租约时间。虽然可以配置它,但我们不能让远程对象的租约**永不**到期。这是糟糕的设计,并将增加前面讨论的资源泄漏问题的可能性。
如果我们需要一个远程对象超过默认租约时间,我们不能让远程对象自行到期然后重新创建对象。如果这就是您想要的,那么在您的项目中完全可以这样做。但是,我假设通常的要求是维护远程对象的生命周期,直到应用程序明确不再需要它们。
关于租赁续订,有三种方法可以实现
- 通过**隐式续订** - 在客户端调用远程方法时完成。
- 通过**显式续订** - 通过 `ILease.Renew()` 方法完成。
- 通过**赞助** - 想法是让客户端创建一个赞助商对象,该对象自动延长租赁时间。
在这三种方法中,隐式续订不适用。当我们调用远程方法时,我们将面临远程对象租赁到期的风险。
赞助似乎是一个有吸引力的选择,而且**确实如此**,但实现起来也有些复杂。.NET 安全功能必须使用。赞助及其与安全问题的复杂关联对我们也没有吸引力,因为本文是为初级开发人员准备的。
因此,我们唯一的选择是显式续订方法。我们的客户端代码使用 `Renew()` 方法来续订远程对象的租约。
首先,我们必须获取远程对象透明代理的 `ILease` 接口。此时不深入太多细节,以下是您如何获取此 `ILease` 接口的方法
m_real_proxy = RemotingServices.GetRealProxy(process_activator);
m_transparent_proxy = m_real_proxy.GetTransparentProxy();
m_lease
= (ILease)(((MarshalByRefObject)m_transparent_proxy).GetLifetimeService());
为了方便获取 `ILease` 对象,我们为 `ProcessActivator_ClientActivated_Client` 类定义了以下成员数据
static RealProxy m_real_proxy = null;
static object m_transparent_proxy = null;
static ILease m_lease = null;
`ILease` 对象可以通过调用远程对象透明代理的 `GetLifetimeService()` 方法获得。我还没有完全掌握 Remoting Proxies,但我稍后会深入研究它,届时将与大家分享我的发现。
回到租赁续订,为了及时续订远程对象租赁,我们需要为我们的客户端应用程序创建一个计时器。这就是为什么我们定义了名为“`aTimer`”的 `Timer` 对象并将超时设置为合适的值(30 秒)
System.Timers.Timer aTimer = new System.Timers.Timer(30000);
我们还将计时器事件处理程序设置为我们用户定义的 `OnTimedEvent()` 函数,并最初禁用了 `Timer` 对象
aTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
aTimer.AutoReset = true;
aTimer.Enabled = false;
/*Disable the timer first until m_lease has been properly initialized.*/
在通过调用 `GetLifetimeService()` 正确获取 `ILease` 对象(`m_lease`)之前,我们需要禁用 `Timer` 对象。成功调用 `GetLifetimeService()` 后,我们再次启用 `Timer`
/* Only now do we turn on the timer...*/
aTimer.Enabled = true;
现在让我们检查我们的 `Timer` 事件处理程序
/* When the timer event is raised, we perform a check on the current lease time
of the Remote Object. */
private static void OnTimedEvent(object source, ElapsedEventArgs e)
{
/* Perform something only when the ILease object is not null. */
if (m_lease != null)
{
TimeSpan time_span_current_lease = m_lease.CurrentLeaseTime;
double dbTime_current_lease = time_span_current_lease.TotalSeconds;
LeaseState lease_state = m_lease.CurrentState;
Console.WriteLine("Current Lease Time : {0}.", dbTime_current_lease);
if ((lease_state == LeaseState.Active) && (dbTime_current_lease <= 60))
{
/* Once the lease time has fallen below 60 seconds,
we seek to renew the lease. */
TimeSpan time_span_renew = m_lease.Renew(TimeSpan.FromSeconds(300));
double dbTimeRenew = time_span_renew.TotalSeconds;
Console.WriteLine("Renewed Lease Time To : {0}.", dbTimeRenew);
}
}
}
在这里,我们首先检查 `ILease` 对象 (`m_lease`) 是否非空。如果非空,则表示 `m_lease` 已正确初始化。然后我们继续访问 `m_lease` 对象的 `CurrentLeaseTime` 和 `CurrentState` 属性。然后我们检查租约的状态是否仍处于活动状态,以及租约时间是否已减少到小于或等于 60 秒。
如果租赁状态为活动状态且租赁时间已降至小于或等于 60 秒,我们将执行租赁续订(通过 `ILease.Renew()` 方法)。
在我们的客户端示例代码中,我试图通过让主线程在 `Run()` 方法调用后休眠 4.5 分钟(270 秒)来演示此租赁续订。在此休眠期间,`Timer` 对象将继续工作,检查是否需要续订租赁时间。
在 `ThreadSleep()` 函数返回之前,我们的 `Timer` 回调函数会将远程对象租约时间再次续订到 300 秒。现在,在 `Thread.Sleep()` 返回后,我将调用远程对象的 `GetModules()` 方法并显示“notepad.exe”的所有加载模块。调用 `GetModules()` 成功,没有任何租约时间到期。
我故意重复调用 `Thread.Sleep()`(具有相同的 4.5 分钟超时值),然后再次调用 `GetModules()`。正如您运行示例代码时将显而易见的,第二次调用 `GetModules()` 将再次成功。
结论
我再次衷心希望读者能从本文及其示例代码中受益。客户端激活的远程对象与服务器激活的远程对象相比,创建和使用起来肯定更复杂。我希望我的文章能很好地解释了内部的复杂性,并且我的示例代码将为项目提供一个良好的起点。
我当然希望在不久的将来探讨代理,并希望撰写一篇关于此的文章。我还没有涉及配置文件,也将对此进行研究。