.NET Remoting 技术全解析
1. .NET Remoting 简介
在分布式系统开发中,.NET Remoting 是一项强大的技术。想象这样一个场景:正在开发一个分布式计费系统,客户端是商业街的分期付款租赁门店,而总部的中央服务器负责处理客户发票、债务催收等任务。客户端需要服务器执行诸如信用检查、记录租赁开始、终止租赁、处理信用卡付款等任务。虽然可以通过 TCP/IP 发送字符串,让服务器在远程端解析,但使用 .NET Remoting 直接调用
customer.terminateLease()
方法,让 .NET 处理网络传输会更加简单。
2. .NET Remoting 工作原理
使用 .NET Remoting 时,需要创建客户端和服务器,同时创建一个执行所需功能的对象。连接的两端都需要知道对象的类型,客户端需要知道服务器的 IP 地址和端口,其余工作由 .NET 完成。
传输方式有两种选择:基于 HTTP 的 SOAP(可移植性好)和基于 TCP 的二进制(性能高)。用于 Remoting 的 SOAP 与行业标准格式有所不同,其可移植性不如等效的 Web 服务。
为防止客户端创建大量对象并丢弃实例耗尽服务器资源,Remoting 有内置的垃圾回收系统。对象的生命周期可以设置为函数执行时间(singlecall)、类的生命周期(singleton)或服务器定义的生命周期(发布对象)。除发布对象外,远程对象的生命周期在
RemotingConfiguration.RegisterWellKnownServiceType
调用中指定。
发布对象的实例化方式略有不同,示例代码如下:
RemoteObject obj = new RemoteObject(1234);
RemotingServices.Marshal (obj,"RemotingServer");
Dim obj as RemoteObject
obj = new RemoteObject(1234);
RemotingServices.Marshal(obj,"RemotingServer")
这样创建的对象表现为单例模式,好处是可以创建具有非默认构造函数的对象。
Remoting 的关键是创建一个继承自
MarshalByRefObject
的类。该对象可以在服务器上下文中运行,并通过服务器公开其方法和属性。在服务器上下文中运行时,可以通过该类访问服务器上的本地资源,如文件和数据库。调用该类方法返回的对象在客户端上下文中运行,称为按值对象(By Value 对象)。按值对象不能访问服务器资源,但可以预先填充从服务器资源获取的数据。远程对象通过序列化按值对象并将其通过网络传输到客户端来返回该对象,此机制需满足两个条件:
1. 对象必须标记为
[Serializable]
或实现
ISerializable
。
2. 客户端必须持有按值对象的元数据,以便正确反序列化。
3. 实现 Remoting
下面通过一个简单的示例演示 Remoting 应用,客户端应用程序可以从服务器请求一个数字,每次调用该数字都会递增。
3.1 创建类库项目
在 Visual Studio .NET 中创建一个新的类库项目,并输入以下代码:
using System;
namespace RemoteObject
{
public class IDGenerator : System.MarshalByRefObject
{
private int lastID = 0;
public int getID()
{
return (lastID++);
}
}
}
Imports System
Namespace RemoteObject
Public Class IDGenerator
Inherits System.MarshalByRefObject
Private lastID As Integer = 0
Public Function getID() As Integer
lastID = lastID + 1
Return (lastID)
End Function
End Class
End Namespace
该类继承自
System.MarshalByRefObject
,使对象能够通过 Remoting 通道传输。
3.2 创建服务器应用程序
在 Visual Studio .NET 中创建一个新的 Windows 窗体项目。点击
Project
->
Add References
->
Browse
,然后选择上一步编译生成的 DLL。还需要选择
System.Runtime.Remoting
命名空间。
private void Form1_Load(object sender, System.EventArgs e)
{
HttpChannel channel = new HttpChannel(8085);
ChannelServices.RegisterChannel(channel);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(RemoteObject.IDGenerator),
"RemotingServer",
WellKnownObjectMode.Singleton);
}
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Dim channel As HttpChannel = New HttpChannel(8085)
ChannelServices.RegisterChannel(channel)
RemotingConfiguration.RegisterWellKnownServiceType(
(New RemoteObject.RemoteObject.IDGenerator).GetType(),
"RemotingServer",
WellKnownObjectMode.Singleton)
End Sub
从代码可以看出,通信将在端口 8085 上使用基于 HTTP 的 SOAP 进行。对象将以单例模式创建,这意味着它是有状态的,
LastID
的值在调用之间将被保留。
同时,需要引入以下支持命名空间:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using RemoteObject;
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http
Imports RemoteObject
3.3 创建客户端应用程序
在 Visual Studio .NET 中创建一个新的 Windows 窗体项目。点击
Project
->
Add References
,选择上一步编译生成的 DLL。在窗体上绘制一个按钮,命名为
btnGetID
,并输入以下代码:
private void btnGetID_Click(object sender, System.EventArgs e)
{
RemoteObject.IDGenerator remObject = (RemoteObject.IDGenerator)Activator.GetObject(
typeof(RemoteObject.IDGenerator),
"http://localhost:8085/RemotingServer");
if (remObject == null)
MessageBox.Show("cannot locate server");
else
MessageBox.Show(Convert.ToString(remObject.getID()));
}
Private Sub btnGetID_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Dim remObject As RemoteObject.IDGenerator = CType(Activator.GetObject(
(New RemoteObject.IDGenerator).GetType(),
"http://localhost:8085/RemotingServer"),
RemoteObject.IDGenerator)
If remObject Is Nothing Then
MessageBox.Show("cannot locate server")
Else
MessageBox.Show(Convert.ToString(remObject.getID()))
End If
End Sub
此代码中的远程对象调用为同步调用,如果客户端在等待方法返回时可以执行其他操作,则应使用异步或单向调用。同样,需要引入以下支持命名空间:
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using RemoteObject;
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http
Imports RemoteObject
3.4 测试应用程序
同时运行客户端和服务器,然后多次点击客户端上的按钮,会看到消息框中的数字不断增加。
4. 异步使用远程对象
可以使用委托实现远程对象的异步使用,委托是 .NET 中函数指针的等效物。委托在客户端类中声明,但在任何方法之外,其函数原型与要调用的同步方法相同。例如,一个名为
getDetails()
的远程方法返回字符串,对应的委托如下:
delegate String GetDetailsDelegate();
Delegate Function GetDetailsDelegate() as string
假设远程对象已实例化并命名为
obj
,可以按以下方式调用
getDetails()
方法:
GetDetailsDelegate gdDelegate = new GetDetailsDelegate(obj.GetDetails);
IASyncResult gdAsyncres = gdDelegate.BeginInvoke(null, null);
Dim gdDelegate as GetDetailsDelegate
Dim gdAsyncres as IASyncResult
gdDelegate = new GetDetailsDelegate(AddressOf obj.GetDetails)
gdAsyncres = gdDelegate.BeginInvoke(nothing, nothing)
此代码会立即返回,服务器将开始执行远程对象上的
GetDetails
方法。为了获取调用的返回值,客户端必须在委托上执行
EndInvoke
方法,该方法是阻塞的,只有在服务器响应后才会返回。调用方式如下:
String details = gdDelegate.EndInvoke(gdAsyncres);
Dim details as string
Details = gdDelegate.EndInvoke(gdAsyncres)
除了使用委托,还可以使用
OneWay
属性实现异步调用。单向调用与标准异步调用方式相同,但
EndInvoke
方法是非阻塞的,无论服务器是否响应都会立即返回,适用于非关键的“调用即忘”方法。要实现单向函数,只需在接口定义的方法上标记
[OneWay()]
属性。
5. 部署 Remoting 服务
在商业应用中使用 Remoting 时,有一些技巧可以使软件更健壮和易于管理。客户端必须在编译时能够确定要接收的对象类型,因此如果已经将客户端部署到全球数百万用户,就不能更改对象,否则所有客户端将无法工作。解决此问题的方法是让客户端引用对象的接口而不是对象本身,这样可以在不破坏兼容性的情况下更改对象方法的实现。
以下是
RemoteObject.IDGenerator
类的接口示例:
using System;
public Interface IIDGenerator
{
public int getID();
}
Imports System
Public Interface IIDGenerator
Public Function NextOrder() As int
End Interface
使用共享接口并非为客户端提供远程对象访问的唯一方式,其主要缺点是对象的公共方法或属性更改时,必须向第三方发送新接口,并且不能将这些对象作为参数传递给在不同上下文中运行的函数。另一种方法是使用共享基类,客户端提供一个抽象基类,服务器从该基类继承并实现所需功能。这样的类可以传递给在不同上下文中运行的函数,但不能使用
new
运算符创建新对象,只能使用
Activator.GetObject()
。
为了解决部署问题,Microsoft 创建了一个名为
soapsuds
的实用工具。该命令行工具可用于从远程对象提取元数据,调用方式如下:
soapsuds –url:<URL>?wsdl -oa:<OUTPUT>.DLL –nowp
这将生成一个 DLL 文件,客户端程序可以使用该文件与远程对象进行通信。该 DLL 不包含任何实现细节,仅包含接口。
–nowp
参数用于指示是否将远程对象的 URL 硬编码到 DLL 中。硬编码 URL 的好处是可以使用
new
运算符创建远程对象,否则客户端必须使用
Activator.GetObject()
实例化对象。
6. 配置
Remoting 服务部署的一个主要问题是能否快速轻松地配置客户端。例如,如果必须更改托管远程对象的服务器的 IP 地址,更改代码以指向新 IP 地址、重新编译应用程序并要求所有客户升级软件可能会很麻烦。更合适的解决方案是将远程对象的位置保存在单独的 XML 文件中,需要时可以用热修复补丁替换。因此,.NET 提供了为 Remoting 客户端提供配置文件的方法,配置文件格式如下:
<configuration>
<system.runtime.remoting>
<application>
<channels>
<channel ref="http" port="1234" />
</channels>
<service>
<wellknown mode="Singleton" type="myNamespace.myClass, myAssembly" objectUri="myClass.soap">
</service>
</application>
</system.runtime.remoting>
</configuration>
假设该文件保存为
MyApp.exe.config
,可以使用以下代码从客户端实例化远程对象:
String filename = "MyApp.exe.config";
RemotingConfiguration.Configure(filename);
MyClass obj = new MyClass();
Dim filename as string
Filename = "MyApp.exe.config"
RemotingConfiguration.Configure(filename)
Dim obj as MyClass
Obj = new MyClass()
当然,客户端仍然需要
MyClass
类的定义才能创建该类的实例。可以向客户端提供
MyClass
的实现,但这会带来代码安全风险和部署问题。共享接口和共享基类方法都不适用于此示例,因此应使用
soapsuds
为客户端生成一个 DLL 来引用,以创建这些远程对象。使用
soapsuds
时应使用
–nowp
开关,以确保 DLL 不包含硬编码参数。
在大多数情况下,这应该是使用配置文件部署 Remoting 服务所需的全部内容。但有些开发人员可能会遇到远程对象返回包含自身方法的按值对象的问题。在这种情况下,客户端必须有按值对象的本地引用,以便反序列化对象并执行其方法。由于
soapsuds
生成的命名空间将与按值对象的命名空间相同,会出现命名空间冲突问题。为避免此冲突,应手动编辑
soapsuds
生成的代理 DLL 的源代码(可通过
–gc
开关调用
soapsuds
获取),编辑 C# 代码后更改命名空间。
7. 在 IIS 中托管远程对象
到目前为止,远程对象一直托管在简单的 Windows 应用程序中。实际上,远程对象服务器通常不需要用户界面,但通常需要能够在计算机上执行,而不管用户是否主动登录。因此,可能希望将远程对象服务器作为服务运行。另一种适用于共享托管环境的选择是在 IIS 中托管远程对象,通过在
web.config
文件中添加以下 XML 代码实现:
<configuration>
<system.runtime.remoting>
<application>
<service>
<wellknown
mode = "Singleton"
type = "RemoteObject.IDGenerator,RemotingServer"
objectUri = "RemoteObject.soap"
/>
</service>
</application>
</system.runtime.remoting>
</configuration>
8. 在 Windows 服务中托管远程对象
当应用程序设计为在计算机上无人值守运行且不需要用户界面时,应作为 Windows 服务运行。Windows 服务即使在没有用户当前登录时也会在后台运行,可以通过
Control Panel
->
Administrative Tools
->
Services
进行控制,包括启动、停止、重启服务以及查看服务信息。
虽然可以使用 IIS 作为 Remoting 对象的主机,但如果开发的是面向大众市场的软件产品,并非所有用户的计算机上都有 IIS,也不想麻烦安装。下面的示例需要前面示例中的客户端和对象,如果还未完成,现在是输入代码的好时机。
8.1 创建 Windows 服务项目
在 Visual Studio .NET 中创建一个新的 Windows 服务项目,滚动代码到
OnStart
和
OnStop
方法,并添加以下代码:
Thread thdServer;
protected override void OnStart(string[] args)
{
thdServer = new Thread(new ThreadStart(serverThread));
thdServer.Start();
}
Dim thdServer As Thread
Protected Overrides Sub OnStart(ByVal args() As String)
thdServer = New Thread(New ThreadStart(AddressOf serverThread))
thdServer.Start()
End Sub
OnStart
和
OnStop
事件在从
Administrative Tools
->
Services
启动或停止服务时触发,上述代码将在
serverThread
函数处启动一个新线程。注意,线程变量在方法调用之外,这为
OnStop
方法提供了一种通过停止线程来禁用服务的方式。
protected override void OnStop()
{
thdServer.Abort();
}
Protected Overrides Sub OnStop()
thdServer.Abort()
End Sub
serverThread
方法如下:
public void serverThread()
{
HttpChannel channel = new HttpChannel(8085);
ChannelServices.RegisterChannel(channel);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(RemoteObject.IDGenerator),
"RemotingServer",
WellKnownObjectMode.Singleton);
}
Public Sub serverThread()
Dim channel As HttpChannel = New HttpChannel(8085)
ChannelServices.RegisterChannel(channel)
RemotingConfiguration.RegisterWellKnownServiceType(
(New RemoteObject.RemoteObject.IDGenerator).GetType(),
"RemotingServer",
WellKnownObjectMode.Singleton)
End Sub
与之前一样,代码使用 HTTP 在端口 8085 上建立连接通道,对象以单例模式托管,即只创建一个对象实例。此模式是必需的,因为对象需要维护一个私有变量,该变量在所有调用远程对象的客户端之间共享。
8.2 安装服务
服务不能直接从命令行运行,必须进行安装。为准备服务部署,在设计视图中右键单击服务并选择
Add Installer
。点击
ServiceInstaller1
,将
ServiceName
属性设置为
MyService
,将
ServiceProcessInstaller1
的
Account
属性设置为
LocalSystem
。
最后,需要添加三个引用:一个指向
System.Configuration.Install.dll
,一个指向
System.Runtime.Remoting
,另一个指向
IDGenerator
程序集的编译 DLL。然后添加所需的命名空间:
using System.Configuration.Install;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Threading;
Imports System.Configuration.Install
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http
Imports System.Threading
在 Visual Studio .NET 中编译应用程序后,需要在 DOS 命令提示符下安装服务。导航到编译后的
.exe
文件所在路径,然后在命令提示符下输入以下命令:
path %path%;C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705
installutil service.exe
net start MyService
注意:
C:\windows\Microsoft.NET\Framework\v1.0.3705
路径可能因计算机而异,如果计算机上有两个版本的 .NET 框架,应使用最新版本。
通过以上步骤,我们详细介绍了 .NET Remoting 的工作原理、实现方法、异步使用、部署、配置以及不同的托管方式,希望能帮助你更好地理解和应用这一技术。
.NET Remoting 技术全解析
9. 总结与对比
在深入了解了 .NET Remoting 的各个方面后,我们来总结一下不同部署和使用方式的特点,并通过表格进行对比。
| 部署/使用方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 共享接口 | 可在不破坏兼容性的情况下更改对象方法实现 | 对象公共方法或属性更改时需向第三方发送新接口;不能将对象作为参数传递给不同上下文运行的函数 | 有第三方合作开发,且对象方法更改不频繁的场景 |
| 共享基类 | 类可传递给不同上下文运行的函数 |
不能使用
new
运算符创建新对象,只能用
Activator.GetObject()
| 需要在不同上下文传递对象,但不需要频繁创建新对象的场景 |
使用
soapsuds
工具
| 生成的 DLL 可用于客户端与远程对象通信,不包含实现细节 | 需处理 URL 硬编码和命名空间冲突问题 | 大规模部署客户端,需要统一管理对象元数据的场景 |
| 在 IIS 中托管 | 适用于共享托管环境 | 需要 IIS 环境支持 | 共享服务器资源,对服务器管理有一定要求的场景 |
| 在 Windows 服务中托管 | 可无人值守运行,无需用户界面 | 安装和配置相对复杂 | 面向大众市场的软件产品,需要在后台稳定运行的场景 |
10. 流程图梳理
下面通过 mermaid 格式的流程图来梳理 .NET Remoting 的基本使用流程:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B(创建类库项目):::process
B --> C(创建服务器应用程序):::process
C --> D(创建客户端应用程序):::process
D --> E{是否需要异步调用}:::decision
E -->|是| F(使用委托或 OneWay 属性实现异步调用):::process
E -->|否| G(进行同步调用):::process
F --> H(部署 Remoting 服务):::process
G --> H
H --> I{选择托管方式}:::decision
I -->|IIS| J(在 IIS 中托管远程对象):::process
I -->|Windows 服务| K(在 Windows 服务中托管远程对象):::process
J --> L([结束]):::startend
K --> L
11. 常见问题与解决方案
在使用 .NET Remoting 过程中,可能会遇到一些常见问题,以下是部分问题及对应的解决方案:
11.1 客户端无法找到服务器
- 可能原因 :服务器未启动、服务器 IP 地址或端口配置错误、网络连接问题。
- 解决方案 :检查服务器应用程序是否正常运行;确认客户端代码中服务器的 IP 地址和端口与服务器配置一致;检查网络连接是否正常。
11.2 命名空间冲突问题
-
可能原因
:使用
soapsuds工具生成的代理 DLL 命名空间与按值对象的命名空间相同。 -
解决方案
:手动编辑
soapsuds生成的代理 DLL 的源代码(通过–gc开关调用soapsuds获取),更改命名空间。
11.3 服务安装失败
- 可能原因 :缺少必要的引用、权限不足、路径配置错误。
-
解决方案
:确保添加了所有必要的引用,如
System.Configuration.Install.dll、System.Runtime.Remoting等;以管理员身份运行命令提示符;检查installutil命令中.exe文件的路径是否正确。
12. 代码优化建议
为了提高 .NET Remoting 应用的性能和可维护性,以下是一些代码优化建议:
-
异步调用优化
:对于非关键的远程方法调用,尽量使用
OneWay属性实现异步调用,减少客户端等待时间,提高整体应用性能。 -
资源管理
:在服务器端合理管理对象的生命周期,避免创建过多的对象实例,防止服务器资源耗尽。例如,根据实际需求选择合适的对象模式(如
Singleton、Singlecall)。 - 异常处理 :在客户端和服务器端代码中添加适当的异常处理机制,捕获并处理可能出现的异常,提高应用的健壮性。以下是一个简单的异常处理示例:
private void btnGetID_Click(object sender, System.EventArgs e)
{
try
{
RemoteObject.IDGenerator remObject = (RemoteObject.IDGenerator)Activator.GetObject(
typeof(RemoteObject.IDGenerator),
"http://localhost:8085/RemotingServer");
if (remObject == null)
{
MessageBox.Show("cannot locate server");
}
else
{
MessageBox.Show(Convert.ToString(remObject.getID()));
}
}
catch (Exception ex)
{
MessageBox.Show($"An error occurred: {ex.Message}");
}
}
Private Sub btnGetID_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Try
Dim remObject As RemoteObject.IDGenerator = CType(Activator.GetObject(
(New RemoteObject.IDGenerator).GetType(),
"http://localhost:8085/RemotingServer"),
RemoteObject.IDGenerator)
If remObject Is Nothing Then
MessageBox.Show("cannot locate server")
Else
MessageBox.Show(Convert.ToString(remObject.getID()))
End If
Catch ex As Exception
MessageBox.Show($"An error occurred: {ex.Message}")
End Try
End Sub
13. 未来展望
随着技术的不断发展,.NET Remoting 也可能会面临一些挑战和机遇。未来可能会朝着更加高效、安全和易于部署的方向发展。例如,与新兴的云计算和微服务架构相结合,提供更灵活的分布式解决方案;进一步优化垃圾回收机制和网络传输协议,提高性能和稳定性。同时,对于开发者来说,也需要不断学习和掌握新的技术和工具,以更好地应对未来的变化。
通过对 .NET Remoting 的全面介绍,我们了解了其工作原理、实现方法、部署和配置方式,以及常见问题的解决方案。希望这些内容能帮助你在实际开发中更好地应用 .NET Remoting 技术,构建出高效、稳定的分布式应用程序。
超级会员免费看

1222

被折叠的 条评论
为什么被折叠?



