关于.NET Remoting你需要知道的一切






4.89/5 (93投票s)
.NET Remoting 自.NET推出以来就已可用。是时候最终好好了解它了。希望本文能帮助您。
作者: Olexandr Malko
日期: 09/29/2008
引言
.NET Remoting 自.NET推出以来就已可用。是时候最终好好了解它了。希望本文能帮助您。本文附带了许多示例。我们决定不将所有功能一次性加载到一个项目中。尽管从一个最终应用程序切换到另一个应用程序只需更改几行代码,但为了避免出现“//uncomment this to gain that”之类的文本,将会有独立的解决方案。所有示例均使用 Visual Studio 2003 解决方案引入。因此,您应该能够使用 VS2005 和 VS2008 打开它们。
有时图片上的对象没有编号,即使它们被称为“第二个”或“第五个”。我将使用从上到下、从左到右计数的规则进行编号。
Content
1. 什么是.NET Remoting?
2. 展示.NET Remoting的简单项目
3. 配置文件和代码中的配置
4. 远程对象激活的类型
4.1. 服务器端对象激活。单例(Singleton)
4.2. 服务器端对象激活。单次调用(SingleCall)
4.3. 客户端对象激活
5. 什么是租约时间?如何控制它?
6. 对客户端隐藏实现。通过接口暴露进行远程处理。
7. 自定义类型作为参数和返回值
8. 通过远程通道自定义异常
9. .NET Remoting 中的事件
10. 异步调用
11. 一个服务器中的多个服务。一个客户端应用程序的多个服务器链接
12. 总结
1. 什么是.NET Remoting?
".NET Remoting" 是.NET Framework 中用于两个应用程序通过网络(例如,在一个PC内、在局域网内甚至全球范围内)进行交互的手段。此外,在.NET中,我们可以在一个进程中运行多个应用程序域(Application Domains)。.NET Remoting 是在这些域之间进行交互的方式。
.NET Remoting 中有两种常用的协议类型:用于二进制流的 tcp 和用于 SOAP 流的 http。在本文中,所有示例都将使用二进制通道 tcp。它需要更少的流量负载和更好的性能,因为没有 XML 解析的开销。对于我们的生产项目来说,这是一个很大的优势。
对于分布式应用程序来说,通常有一个服务器应用程序和一个客户端应用程序。在.NET Remoting 中,我们可以拥有任意数量的客户端,并且所有这些客户端应用程序都可以使用同一个服务器。.NET Remoting 不仅仅是一个具有低级方法的套接字。它是一个框架,您可以在其中远程使用类,能够调用方法,将自定义类型作为参数传递并将其作为返回值获取,在进程之间抛出异常,传递回调委托并在以后远程调用它们,以及执行异步调用。
2. 展示.NET Remoting的简单项目
Remoting 交互需要
1) 在交互的两个点都可用的服务类型描述
2) 点 #1 – 宿主(例如服务器),它持有我们服务类型的一个实例化远程处理对象
3) 点 #2 – 客户端应用程序,它可以连接到服务器并使用远程处理对象
现在,让我们看一下下面的图片。您可以看到两个独立的进程。服务器持有一个 MyService 的真实实例。这个实例可以被其他进程通过 .NET Remoting 使用。客户端进程并没有实例化 MyService 的实例。它只是有一个透明代理。当客户端应用程序调用 MyService 代理的方法时,代理将这些调用重定向到客户端进程中的 .NET Remoting 层。该远程处理层知道将此类调用发送到哪里——因此,调用通过网络(例如,通过 .NET Remoting 通道)直接发送到我们的远程服务器进程。之后,服务器端的远程处理层知道是应该使用已经存在的 MyService 实例还是创建新的实例。这取决于激活的类型。激活类型可以在 *.xml 配置文件中或通过代码进行配置。所有这些都将在本文后面描述。
您可以在下载中找到“Simple Remoting”解决方案。它由三个核心项目组成。本文中的几乎所有示例都将包含它们
1) ONXCmn - 包含 MyService 类型定义的类库
2) ONXServer – 托管 MyService 服务的可执行控制台应用程序。
3) ONXClient – 展示如何使用远程 MyService 服务的可执行控制台应用程序。
您可以启动任意数量的客户端应用程序。所有这些都将由一个服务器应用程序提供服务。但是,您不能同时启动多个服务器。这是因为有一个端口用于监听远程客户端应用程序。您不能在同一网卡和同一端口上启动多个套接字监听器。
此外,我想提请您注意 Log 和 Utils 类。它们将用于所有示例。您会发现 Log 在每次打印输出时打印时间戳非常有用。此外,它还会打印当前线程的 ID——这样我们就可以很容易地看到是否同一线程用于一组操作。至于 Utils 类,它会转储所有已注册的远程服务和客户端类型的信息。这有助于您在某些配置错误时捕获问题。
static void Utils.DumpAllInfoAboutRegisteredRemotingTypes()
public class MyService : MarshalByRefObject { public MyService() { Log.Print("Instance of MyService is created"); } public string func1() { Log.Print("func1() is invoked"); return "MyService.func1()"; } }
在这里我们描述了我们的远程处理类型——MyService。它必须派生自 MarshalByRefObject。这个父类告诉我们的 MyService 类不按值发送——它只按引用引用。我们的 MyService 只有一个服务方法——“string func1()”。每当我们调用“func1()”时,我们都会打印日志消息并返回一个值。正如您可能猜到的那样,我们在服务器应用程序中实例化 MyService 对象,并在客户端应用程序中使用它。这就是为什么我们应该期望日志消息出现在服务器控制台中,而不是客户端控制台中。MyService() 构造函数也是如此。关于对象创建的日志消息应该出现在服务器控制台中。
现在,Server 类
class MyServer { [STAThread] static void Main(string[] args) { RemotingConfiguration.Configure("ONXServer.exe.config"); Utils.DumpAllInfoAboutRegisteredRemotingTypes(); Log.WaitForEnter("Press EXIT to stop MyService host..."); } }
如果您是第一次接触.NET Remoting,可能会感到惊讶。这里没有什么特别复杂的地方。为什么它能工作?“Utils.DumpAllInfoAboutRegisteredRemotingTypes()”只是简单地调用来打印注册的.NET服务。“Log.WaitForEnter(..)”只是用户提示按下ENTER键来关闭我们的控制台应用程序。所以,真正将我们常规的控制台应用程序变成.NET Remoting服务器的唯一一行代码是“RemotingConfiguration.Configure("ONXServer.exe.config")”。这个方法读取*.xml文件,并从中获取足够的信息来在某个端口启动套接字监听器并等待来自远程客户端应用程序的请求!这是一种很好的方法,因为您可以更改应用程序的行为,而无需更改和重新编译我们的代码。现在,让我们看看这个配置文件。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <wellknown type="ONX.Cmn.MyService, ONXCmn" objectUri="MyServiceUri" mode="SingleCall" /> </service> <channels> <channel ref="tcp" port="33000" /> </channels> </application> </system.runtime.remoting> </configuration>
Remoting 在 <configuration><system.runtime.remoting><application> 节中配置。服务器和客户端配置都是如此(是的,客户端也通过 *.xml 文件配置)。对于服务器,我们有 <service> 节,它可能包含一个或多个 <wellknown> 节。这个 wellknown 节是您描述您的服务以供客户端应用程序可用的地方。它有 3 个属性
1) 完整类型描述 – 描述当从客户端收到此周知类型的请求时实例化哪种类型。完整类型值由带完整命名空间路径的类型名称组成,逗号后是此类型所在的程序集名称。
2) objectUri – 这是客户端应用程序将请求的唯一名称。客户端应用程序通常通过“URI”而不是通过直接类型名称请求服务。当您看到“6. 对客户端隐藏实现。通过接口暴露进行远程处理”主题时,您就会明白原因。
3) 此参数可以是“SingleCall”或“Singleton”。在“SingleCall”的情况下,来自任何客户端的每个方法调用都由新创建的 MyService 实例提供服务。在“Singleton”配置中,所有客户端应用程序的所有调用都由 MyService 对象的单个实例提供服务。
如果您的应用程序中需要注册多个服务,请在“service”节内一个接一个地列出“wellknown”节。
此外,除了“service”节,还有“channels”节。在这里我们可以定义多个通道。在我们的示例中,我们只定义了“tcp”通道。它将监听端口 33000。
现在,让我们看看客户端配置
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <client> <wellknown type="ONX.Cmn.MyService, ONXCmn" url="tcp://:33000/MyServiceUri" /> </client> </application> </system.runtime.remoting> </configuration>
您可能会注意到服务器和客户端配置之间有很多相似之处。不同之处在于,在客户端配置中,我们有“<client />”节而不是“<service />”。这使得应用程序能够理解,当我们创建 MyService 的实例时,我们实际上是想远程请求这个类。此外,wellknown 节有一个“url”属性,它将连接到“localhost”机器的端口 33000,并请求 URI 为 MyServiceUri 的命名服务。属性“type”指示应用程序在客户端应用程序代码尝试在客户端实例化 MyService 对象时使用此远程处理。因此,在客户端应用程序中没有创建 MyService 的实际实例。我们只创建一个代理,它知道每当我们调用某个方法时将我们的调用请求发送到哪里。
最后是客户端应用程序
class MyClient { [STAThread] static void Main(string[] args) { RemotingConfiguration.Configure("ONXClient.exe.config"); Utils.DumpAllInfoAboutRegisteredRemotingTypes(); MyService myService = new ONX.Cmn.MyService(); Log.Print("myService.func1() returned {0}", myService.func1()); Log.WaitForEnter("Press ENTER to exit..."); } }
如您所见,它和服务器控制台应用程序一样简单。您只需调用“RemotingConfiguration.Configure("ONXClient.exe.config")”即可正确注册我们的 MyService 类型。然后,您会转储所有已注册的远程类型的信息。之后,您会创建 MyService 的“实例”。正如您现在所了解的,只会创建一个透明代理。然后,您调用“MyService.func1()”方法。此调用将转到服务器应用程序,从那里获取返回值,将其传递给客户端应用程序,并在我们客户端的日志中打印出来。
以下是我们示例在服务器和客户端控制台中得到的结果
SERVER: [1812] [2008/10/05 21:30:15.595] ALL REGISTERED TYPES IN REMOTING -(BEGIN)--------- [1812] [2008/10/05 21:30:15.595] WellKnownServiceTypeEntry: type='ONX.Cmn.MyService, ONXCmn'; objectUri=MyServiceUri; mode=SingleCall [1812] [2008/10/05 21:30:15.595] ALL REGISTERED TYPES IN REMOTING -(END) --------- [1812] [2008/10/05 21:30:15.595] Press EXIT to stop MyService host... [5068] [2008/10/05 21:30:20.876] Instance of MyService is created [5068] [2008/10/05 21:30:20.876] func1() is invoked
CLIENT: [7388] [2008/10/05 21:30:20.736] ALL REGISTERED TYPES IN REMOTING -(BEGIN)--------- [7388] [2008/10/05 21:30:20.798] WellKnownClientTypeEntry: type='ONX.Cmn.MyService, ONXCmn'; url=tcp://localhost:33000/MyServiceUri [7388] [2008/10/05 21:30:20.798] ALL REGISTERED TYPES IN REMOTING -(END) --------- [7388] [2008/10/05 21:30:20.892] myService.func1() returned MyService.func1()
您可以看到,即使我们在客户端应用程序代码中有“new MyService()”,服务实例也是在服务器应用程序中创建的!
3. 配置文件和代码中的配置
我们为“Simple Remoting”解决方案执行的所有配置都可以通过代码完成,而无需额外的 *.xml 配置文件。有时在代码中完成配置会更容易,但这会使快速调整或修改配置变得更困难。这就是为什么在本文中我将继续使用 *.xml 文件,因为它们也更容易阅读。但是出于安全或其他任何原因,您仍然可以将配置存储在某些文件或数据库中,然后教您的应用程序读取这些配置数据并根据需要注册代码中的远程处理类型。
作为一个简单的例子,这里有一段代码,它实现了与我们上一个主题中“Simple Remoting”示例中客户端应用程序相同的配置
//RemotingConfiguration.Configure("ONXClient.exe.config"); RemotingConfiguration.RegisterWellKnownClientType( typeof(MyService), "tcp://:33000/MyServiceUri");
您可能想查阅 MSDN 以获取有关代码中 .NET Remoting 配置的更多详细信息。
4. 远程对象激活的类型
远程对象的激活有 3 种类型:2 种服务器端激活和 1 种客户端激活
1) 服务器端单例 (Server Side Singleton) - 当第一个请求来自某个客户端应用程序时,对象在服务器上创建。当您在客户端应用程序中“创建”实例时,服务器上没有任何反应。服务器仅在客户端应用程序调用远程对象的第一个方法时才采取行动。在单例模式下,所有客户端应用程序共享在服务器上创建的单个远程对象实例。即使您在客户端应用程序中创建了多个对象,它们仍然使用来自服务器应用程序的同一个单个对象。
2) 服务器端单次调用 (Server Side SingleCall) - 对象为每个方法调用而创建。因此,无论有多少客户端应用程序正在运行都无关紧要。来自任何客户端应用程序的每个方法调用都有这个生命周期
- 服务器创建远程对象的新实例
- 服务器针对新创建的远程对象调用请求的方法
- 服务器释放远程对象。因此,现在远程对象可用于垃圾回收。
3) 客户端激活 (Client Side Activation) - 对象在服务器应用程序中随着客户端应用程序中的每个“new”操作符而被创建。客户端应用程序对这个远程对象拥有完全控制权,并且不与其他客户端应用程序共享它。此外,如果您在客户端应用程序中创建 2 个或更多远程对象 - 是的,在服务器应用程序中将创建相同数量的远程对象。之后,您可以像没有涉及 .NET Remoting 一样独立地使用每个实例。这里唯一的问题是租用时间 (Lease Time),它可能会比您预期的更早销毁服务器应用程序上的远程对象。请参阅“5. 什么是租用时间?如何控制它?”
对于服务器激活对象,您需要注册“周知类型”(well known type)。对于客户端激活对象,您需要注册“激活类型”(activated type)。让我们更仔细地看看每种激活类型。
4.1. 服务器端对象激活。单例(Singleton)
在这种激活类型中,直到第一个请求从其中一个客户端发出,服务器上才创建对象。对象创建后有多少调用无关紧要。有多少客户端应用程序试图使用我们的服务器对象也无关紧要——所有这些调用都指向单个远程对象,例如下图中“MyService 的实例”。
此外,我想提请您注意,即使您在客户端应用程序中请求了多个 MyService 实例(参见图中的 myService1 和 myService2),这两个变量仍将指向客户端进程中的单个 TransparentProxy。这是因为对于“周知”类型,在“服务器激活对象”模型下,每个进程一个代理就足够了。
如果租约时间到期,Singleton 可能会在服务器上被销毁。在这种情况下,客户端应用程序的新请求将创建一个新的 Singleton,并以相同的方式使用它——例如,所有客户端请求都使用单个对象。请参阅“5. 什么是租约时间?如何控制它?”
要使用这种激活类型,您应该像这样配置服务器的周知类型
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <wellknown type="ONX.Cmn.MyService, ONXCmn" objectUri="MyServiceUri" mode="Singleton" /> </service> <channels> <channel ref="tcp" port="33000"/> </channels> </application> </system.runtime.remoting> </configuration>
客户端配置如下
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <client> <wellknown type="ONX.Cmn.MyService, ONXCmn" url="tcp://:33000/MyServiceUri" /> </client> </application> </system.runtime.remoting> </configuration>
至于示例,请找到“SAO Singleton”解决方案。客户端代码如下
class MyClient { [STAThread] static void Main(string[] args) { RemotingConfiguration.Configure("ONXClient.exe.config"); Utils.DumpAllInfoAboutRegisteredRemotingTypes(); string result; //create myService1 Log.WaitForEnter("1) Press ENTER to create Remote Service..."); MyService myService1 = new MyService(); Log.Print("myService1 created. Proxy? {0}", (RemotingServices.IsTransparentProxy(myService1)?"YES":"NO")); //query myService1.func1() Log.WaitForEnter("2) Press ENTER to query 1-st time..."); result = myService1.func1(); Log.Print("myService1.func1() returned {0}", result); //query myService1.func2() Log.WaitForEnter("3) Press ENTER to query 2-nd time..."); result = myService1.func2(); Log.Print("myService1.func2() returned {0}", result); //create myService2 Log.WaitForEnter("4) Press ENTER to create another instance of Remote Service..."); MyService myService2 = new MyService(); Log.Print("myService2 created. Proxy? {0}", (RemotingServices.IsTransparentProxy(myService2)?"YES":"NO")); //query myService2.func1() Log.WaitForEnter("5) Press ENTER to query from our new Remote Service..."); Log.Print("myService2.func1() returned {0}", myService2.func1()); Log.WaitForEnter("Press ENTER to exit..."); } }
我们得到
SERVER: [4424] [2008/10/05 22:31:52.369] Instance of MyService is created, MyService.id=1 [4424] [2008/10/05 22:31:52.369] func1() is invoked, MyService.id=1 [4424] [2008/10/05 22:31:53.056] func2() is invoked, MyService.id=1 [4424] [2008/10/05 22:31:54.556] func1() is invoked, MyService.id=1
CLIENT: >1) Press ENTER to create Remote Service... [7076] [2008/10/05 22:31:51.416] myService1 created. Proxy? YES 2) Press ENTER to query 1-st time... [7076] [2008/10/05 22:31:52.400] myService1.func1() returned MyService#1.func1() 3) Press ENTER to query 2-nd time... [7076] [2008/10/05 22:31:53.056] myService1.func2() returned MyService#1.func2() 4) Press ENTER to create another instance of Remote Service... [7076] [2008/10/05 22:31:53.650] myService2 created. Proxy? YES 5) Press ENTER to query from our new Remote Service... [7076] [2008/10/05 22:31:54.556] myService2.func1() returned MyService#1.func1()
在此示例中,服务器端只创建了一个 MyService 实例。它处理了所有 3 次调用,即使其中 2 次调用来自 myService1,1 次调用来自 myService2。
4.2. 服务器端对象激活。单次调用(SingleCall)
至于在服务器端创建对象,我们面临着同样的情况——客户端应用程序中“new MyService()”不会创建任何对象。但是一旦您在客户端代码中调用了任何方法,该调用就会被定向到服务器应用程序。.NET Remoting 会为每个此类查询创建新的实例。正如您在下图中看到的,从 2 个客户端应用程序发送了 5 次调用。这使得 .NET Remoting 创建了 5 个 MyService 实例。每个实例只使用一次——用于单次调用。请注意,“MyService 实例”#3 和 #5 是通过“myService1.func1()”的相同调用创建的,但 .NET Remoting 仍然为每次调用创建了独立的实例。
为客户端应用程序中的所有 MyService 对象创建了一个透明代理(参见第二个客户端)。
要使用这种激活类型,您应该像配置 SSA Singleton 那样配置服务器的周知类型。唯一的区别是模式应设置为“SingleCall”。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <wellknown type="ONX.Cmn.MyService, ONXCmn" objectUri="MyServiceUri" mode="SingleCall" /> </service> <channels> <channel ref="tcp" port="33000"/> </channels> </application> </system.runtime.remoting> </configuration>
客户端配置与 SSA Singleton 完全相同
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <client> <wellknown type="ONX.Cmn.MyService, ONXCmn" url="tcp://:33000/MyServiceUri" /> </client> </application> </system.runtime.remoting> </configuration>
至于示例,请找到“SAO SingleCall”解决方案。
SERVER: >[3472] [2008/10/05 22:21:57.662] Instance of MyService is created, MyService.id=1 [3472] [2008/10/05 22:21:57.662] func1() is invoked, MyService.id=1 [3472] [2008/10/05 22:22:00.381] Instance of MyService is created, MyService.id=2 [3472] [2008/10/05 22:22:00.381] func2() is invoked, MyService.id=2 [3472] [2008/10/05 22:22:04.849] Instance of MyService is created, MyService.id=3 [3472] [2008/10/05 22:22:04.849] func1() is invoked, MyService.id=3
CLIENT: 1) Press ENTER to create Remote Service... [7252] [2008/10/05 22:21:54.209] myService1 created. Proxy? YES 2) Press ENTER to query 1-st time... [7252] [2008/10/05 22:21:57.693] myService1.func1() returned MyService#1.func1() 3) Press ENTER to query 2-nd time... [7252] [2008/10/05 22:22:00.381] myService1.func2() returned MyService#2.func2() 4) Press ENTER to create another instance of Remote Service... [7252] [2008/10/05 22:22:02.756] myService2 created. Proxy? YES 5) Press ENTER to query from our new Remote Service... [7252] [2008/10/05 22:22:04.849] myService2.func1() returned MyService#3.func1()
在我们的示例中,“id”是服务器端创建的 MyService 对象的每个实例的唯一 ID。如您所见,我们在 SERVER 应用程序中创建的实例数量与调用次数相同(例如,myService1 调用 2 次,myService2 调用 1 次——总共 3 次)。
另外,根据时间戳您可以得出结论,MyService 是在“func#()”调用时立即创建的。
4.3. 客户端激活
这是一种非常好的激活类型,因为它让您像“根本没有远程处理”一样使用对象。对于您的每个“new”操作符,都会创建一个不同的对象实例。您的实例是在服务器上远程创建的,并且从不与其他客户端应用程序共享。因此,对于客户端应用程序来说,这种激活类型与您以常规方式创建对象的使用案例非常接近,不涉及 .NET Remoting。
myService、myService1 和 myService2 是三个真实的对象,它们在服务器上实例化并由客户端应用程序透明地使用。请注意,在所描述的三种激活类型中,这是唯一一种为客户端 #2 创建了多个代理的类型。这是因为代理的数量将等于您的客户端应用程序迄今为止创建的远程对象的数量。
要使用这种激活类型,您应该使用“<activated />”节配置服务器,而不是使用周知类型。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <activated type="ONX.Cmn.MyService, ONXCmn" /> </service> <channels> <channel ref="tcp" port="33000"/> </channels> </application> </system.runtime.remoting> </configuration>
客户端配置也使用“<activated />”节
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <client url="tcp://:33000"> <activated type="ONX.Cmn.MyService, ONXCmn" /> </client> </application> </system.runtime.remoting> </configuration>
请注意,使用此激活类型,“url”参数在“<client />”节中指定。这里不需要 objectURI,因为 .NET Remoting 会知道要使用什么类型。
此外,租约期满也涉及此激活类型。请参阅
至于示例,请找到“CAO”解决方案。我将不展示客户端代码文本,因为它与上面两个测试的代码相同。唯一的更改是控制激活类型的配置。现在,我们得到
SERVER: >[6956] [2008/10/05 22:38:47.075] Instance of MyService is created, MyService.id=3 [6956] [2008/10/05 22:38:49.918] func1() is invoked, MyService.id=3 [6956] [2008/10/05 22:38:52.559] func2() is invoked, MyService.id=3 [6956] [2008/10/05 22:38:54.965] Instance of MyService is created, MyService.id=4 [6956] [2008/10/05 22:38:57.231] func1() is invoked, MyService.id=4
CLIENT: 1) Press ENTER to create Remote Service... [2280] [2008/10/05 22:38:47.090] myService1 created. Proxy? YES 2) Press ENTER to query 1-st time... [2280] [2008/10/05 22:38:49.918] myService1.func1() returned MyService#3.func1() 3) Press ENTER to query 2-nd time... [2280] [2008/10/05 22:38:52.559] myService1.func2() returned MyService#3.func2() 4) Press ENTER to create another instance of Remote Service... [2280] [2008/10/05 22:38:54.965] myService2 created. Proxy? YES 5) Press ENTER to query from our new Remote Service... [2280] [2008/10/05 22:38:57.231] myService2.func1() returned MyService#4.func1()
在此示例中,MyService 实例在客户端应用程序代码命中“new MyService()”命令时立即在服务器端创建。您可以看到 myService1 的创建有些延迟(15 毫秒)。这是因为这是我们客户端应用程序对服务器的首次调用。它需要建立应用程序之间的物理网络连接并执行所有其他隐藏的 .NET Remoting 握手。至于 myService2,它在同一毫秒内创建。此外,如您所见,我们客户端的每个 myService# 都由服务器端相应的 MyService 实例提供服务。
5. 什么是租约时间?如何控制它?
在进程间协调的情况下,服务器不知道客户端是否仍然会使用对象。对于服务器应用程序中的远程处理对象来说,最简单的方法是计算自对象创建以来或自上次某个客户端使用对象(例如,进行了一些方法调用)以来经过了多长时间。
有些方法可以通过配置文件(如下所示)和代码设置租约时间
using System; using System.Runtime.Remoting.Lifetime; ... LifetimeServices.LeaseTime = TimeSpan.FromMinutes(30); LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(30); LifetimeServices.LeaseManagerPollTime = TimeSpan.FromMinutes(1);
LeaseTime – 是 AppDomain 的初始租用时间跨度。
RenewOnCallTime - 每当服务器对象收到调用时,租约延长的时长。
LeaseManagerPollTime - 租约管理器激活以清理过期租约的时间间隔。
详细信息请参阅 MSDN。
它是这样工作的。对于每个服务器对象,我们可以从 Lease 助手获取 CurrentLeaseTime。这个 CurrentLeaseTime 是对象剩余的存活时间。.NET Remoting LeaseManager 会定期唤醒并检查服务器应用程序中每个可用的服务器对象。每次检查时,它都会减少每个已检查对象的 CurrentLeaseTime。如果对象过期,则其引用将被移除,并且该对象将被标记为等待垃圾回收。每次远程调用服务器对象时,该对象的 CurrentLeaseTime 都会设置为 RenewOnCallTime 时间跨度。
请查看“Lease Time”解决方案。如您所见,它使用 Singleton 模式的服务器激活。它应该让所有客户端和客户端应用程序中的所有 MyService 对象使用服务器上的同一个 MyService 实例。
但我们只将租约时间配置为 5 秒
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> ... <lifetime leaseTime="5S" renewOnCallTime="5S" leaseManagerPollTime="1S" /> </application> </system.runtime.remoting> </configuration>
这使得 .NET Remoting 在没有任何客户端查询的情况下,在 5 秒过去后将远程处理对象标记为垃圾回收。
1) Press ENTER to create Remote Service... [5044] [2008/10/01 14:17:47.442] myService1 created. Proxy? YES 2) Press ENTER to query 1-st time... [5044] [2008/10/01 14:17:48.552] myService1.func1() returned MyService#4.func1() 3) Press ENTER to query 2-nd time... [5044] [2008/10/01 14:18:03.334] myService1.func2() returned MyService#5.func2() 4) Press ENTER to create another instance of Remote Service... [5044] [2008/10/01 14:18:04.099] myService2 created. Proxy? YES 5) Press ENTER to query from our new Remote Service... [5044] [2008/10/01 14:18:04.990] myService2.func1() returned MyService#5.func1()
请参阅输出中的“3)”。如您所见,我们在第二次调用查询之前等待了太长时间(例如,>5 秒)。这导致服务器忘记了 MyService#4 并创建了新的 MyService#5。之后,在“5)”中,我们在 2 秒内调用了 func1(),它使用了 MyService#5,因为它在服务器端尚未过期。
在这里,我们再次启动客户端应用程序,并连续按下 ENTER 键,没有任何延迟。如您所见,所有三次调用都使用了相同的 MyService 实例。
1) Press ENTER to create Remote Service... [5380] [2008/10/01 14:30:39.355] myService1 created. Proxy? YES 2) Press ENTER to query 1-st time... [5380] [2008/10/01 14:30:39.589] myService1.func1() returned MyService#6.func1() 3) Press ENTER to query 2-nd time... [5380] [2008/10/01 14:30:39.652] myService1.func2() returned MyService#6.func2() 4) Press ENTER to create another instance of Remote Service... [5380] [2008/10/01 14:30:39.808] myService2 created. Proxy? YES 5) Press ENTER to query from our new Remote Service... [5380] [2008/10/01 14:30:39.980] myService2.func1() returned MyService#6.func1()
我们也可以让我们的远程处理对象永不过期。为此,我们需要重写 MarshalByRefObject 的一个方法并使其返回“null”
public class MyService : MarshalByRefObject { ... public override object InitializeLifetimeService() { return null; } }
如果您将此重写添加到 LeaseTime 解决方案中,您会看到,即使我们等待了很长时间并且在配置中指定了 <lifetime> 参数——我们的 MyService 也不会过期,并且会重复用于所有调用。
2) Press ENTER to query 1-st time... [3056] [2008/10/01 14:36:51.455] myService1.func1() returned MyService#1.func1() >3) Press ENTER to query 2-nd time... [3056] [2008/10/01 14:37:14.049] myService1.func2() returned MyService#1.func2()
还有一种“赞助”机制,允许根据应用程序需求自定义租约时间。您可以阅读 MSDN 中的“sponsors”主题以获取更多信息。
6. 对客户端隐藏实现。通过接口暴露进行远程处理。
将远程对象的实现暴露给外界并不总是一个好主意。这出于安全原因,也因为复杂实现的程序集大小。此外,实现可能使用您不希望部署到客户端计算机的其他程序集。在这种情况下,最好将我们的 MyService 类拆分为
1) 我们将暴露给客户端的接口
2) 以及实现本身。
此时,我们可以将我们的类型放入单独的程序集中,只将一小部分产品交付给客户端计算机。
然后在交付时,我们只需要将产品的一小部分放在客户端计算机上。
您可以找到“对客户端应用程序隐藏实现”解决方案来查看它是如何实现的。其思想是按 Uri 请求远程类型,并将返回的对象转换为接口。在服务器端,此类 Uri 请求将实例化我们服务器库程序集中定义的实际实现。
服务器配置
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <wellknown type="ONX.Server.MyService, ONXServerLib" objectUri="MyServiceUri" mode="Singleton" /> </service> <channels> <channel ref="tcp" port="33000"/> </channels> </application> </system.runtime.remoting> </configuration>
客户端配置(这里无需定义周知类型)
<?xml version="1.0" encoding="utf-8" ?> <configuration> </configuration>
通过 IMyService 访问 MyService 的客户端代码
IMyService myService1 = Activator.GetObject( typeof(IMyService), "tcp://:33000/MyServiceUri" ) as IMyService; string result = myService1.func1();
注意,如果您决定通过接口隐藏实现,则无法使用客户端激活对象。这是因为客户端激活需要在客户端实例化类对象。但客户端没有类型信息——只有接口。因此,您只能使用周知类型定义(例如,服务器激活对象)来完成此操作。
7. 自定义类型作为参数和返回值
如果您想将自己的类型作为参数传递给远程对象的方法……如果您想将此类类型作为函数的结果获取……您唯一需要做的就是使您的类型可序列化。这很简单——只需为您的类型描述添加 [Serializable] 属性。请注意,如果您的类型包含自定义类型的成员,则这些包含的类型也应该可序列化。至于 int、double、string、ArrayList 等标准类型——它们中的大多数都已经可序列化。
请参阅带有示例的“Custom Types”解决方案
[Serializable] public class MyContainer { private string str_; private int num_; public MyContainer(string str, int num) { str_ = str; num_ = num; } public string Str { get{ return str_;} } public int Num { get{ return num_;} } public override string ToString() { return string.Format("MyContainer[str=\"{0}\",num={1}]", Str, Num); } } public class MyService : MarshalByRefObject { public MyContainer func1(MyContainer param) { Log.Print("func1() is invoked, got {0}", param); return new MyContainer("abc", 123); } }
带有此客户端代码
class MyClient { [STAThread] static void Main(string[] args) { MyService myService = new MyService(); Log.Print("myService created. Proxy? {0}", (RemotingServices.IsTransparentProxy(myService)?"YES":"NO")); MyContainer container1 = new MyContainer("From Client", 555); MyContainer container2 = myService.func1(container1); Log.Print("myService.func1() returned {0}", container2); } }
它会给您这样的服务器输出
[3660] [2008/10/03 10:05:27.970] func1() is invoked, got MyContainer[str="From Client",num=555]
以及这样的客户端输出
[2696] [2008/10/03 10:05:27.892] myService created. Proxy? YES [2696] [2008/10/03 10:05:27.970] myService.func1() returned MyContainer[str="abc",num=123]
8. 通过远程通道自定义异常
抛出标准 Exception 类没有限制,因为它已经拥有所需的一切。至于自定义异常,这里是所需的待办事项列表
1) 一般规则:所有自定义异常都应该从 Exception 类或其派生类派生。
2) 它必须具有 [Serializable] 类属性
3) 它必须有一个构造函数
MyException(SerializationInfo info, StreamingContext context)
4) 它必须重写
void GetObjectData(SerializationInfo info, StreamingContext context)
5) 如果您的自定义异常有一些成员,则应注意读写到/从流中。
这是我们“Exceptions”解决方案中的自定义异常
[Serializable] public class MyException : ApplicationException { private string additionalMessage_; public MyException(string message, string additionalMessage) :base(message) { additionalMessage_ = additionalMessage; } public MyException(SerializationInfo info, StreamingContext context) :base(info, context) { additionalMessage_ = info.GetString("additionalMessage"); } public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData (info, context); info.AddValue("additionalMessage", additionalMessage_); } public string AdditionalMessage { get{ return additionalMessage_;} } }
我们在“GetObjectData(…)”方法中保存成员数据。在反序列化过程中,我们在带有 SerializationInfo 作为参数的构造函数中恢复此值。
采用此 MyService 实现
public class MyService : MarshalByRefObject { public void func1() { throw new MyException("Main text for custom ex", "Additional text"); } public void func2() { throw new Exception("Main text for standard ex"); } }
我们只是尝试抛出自定义异常和标准异常。拥有这样的客户端实现
class MyClient { [STAThread] static void Main(string[] args) { RemotingConfiguration.Configure("ONXClient.exe.config"); MyService myService = new MyService(); try { myService.func1(); } catch(MyException ex) { Log.Print("Caught MyException: message=\"{0}\", add.msg=\"{1}\"", ex.Message, ex.AdditionalMessage); } try { myService.func2(); } catch(Exception ex) { Log.Print("Caught Exception: message=\"{0}\"", ex.Message); } Log.WaitForEnter("Press ENTER to exit..."); } }
我们得到输出(已精简)
[15:09:39.380] Caught MyException: message="Main text for custom ex", add.msg="Additional text" [15:09:39.380] Caught Exception: message="Main text for standard ex"
如果我们注释掉 MyException 类中 additionalMessage 字段的保存和恢复——反序列化后我们将得到默认字符串值。所以不会生成错误,但状态不会完全恢复。如果我们注释掉 [Serializable] 属性,我们将得到运行时异常。
9. .NET Remoting 中的事件
想象一下这个使用场景。我们的远程处理对象在服务器中实例化。在常规使用场景中,客户端应用程序通过调用其方法来使用远程处理对象。如果您希望它调用客户端应用程序中驻留的某个回调方法怎么办?您可以为客户端准备一些信息,然后等待客户端应用程序使用轮询机制并定期调用一些远程对象方法,例如“Information[] MyService.IsThereSomeInfoForMe()”。但实际上我们可以使用事件机制。不过,有一些限制
1) 服务器应用程序应该具有持有回调方法的类型的运行时类型信息。
2) 此回调方法必须是公共的且不能是静态的
3) 为了避免服务器等待并确保回调已收到接收方,我们必须用 [OneWay] 属性标记回调。这使得我们无法通过“return”值或“ref”或“out”参数返回任何数据。
4) 由于此类型的实例将在客户端实例化并在服务器端使用,因此它应该派生自 MarshalByRejObject 类。
请查看“Events”解决方案。所有这些限制使我们必须引入一些事件接收器并在 Cmn 程序集中定义它,以便服务器和客户端应用程序都可以使用它。
public class EventSink : MarshalByRefObject { public EventSink() { } [System.Runtime.Remoting.Messaging.OneWay] public void EventHandlerCallback(string text) { } public void Register(MyService service) { service.EventHandler += new OnEventHandler(EventHandlerCallback); } public void Unregister(MyService service) { service.EventHandler -= new OnEventHandler(EventHandlerCallback); } }
由于我们希望此接收器实际调用我们的回调,我们不能使用多态性并在派生类中覆盖一些方法,这些方法将在客户端应用程序的代码中定义。这将违反上述规则 #1——服务器需要知道我们的类型。因此,我们使用委托机制并将客户端的回调作为构造函数参数传递给 EvenSink。这是 EventSink 类的完整代码
public class EventSink : MarshalByRefObject { private OnEventHandler handler_; public EventSink(OnEventHandler handler) { handler_ = handler; } [System.Runtime.Remoting.Messaging.OneWay] public void EventHandlerCallback(string text) { if (handler_ != null) { handler_(text); } } public void Register(MyService service) { service.EventHandler += new OnEventHandler(EventHandlerCallback); } public void Unregister(MyService service) { service.EventHandler -= new OnEventHandler(EventHandlerCallback); } }
此外,自 .NET Framwork v1.1 起,对某些类型的反序列化存在安全限制。为了覆盖默认设置,我们需要将 filterLevel 设置为“Full”。这是完整的服务器配置文件。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <wellknown type="ONX.Cmn.MyService, ONXCmn" objectUri="MyServiceUri" mode="Singleton" /> </service> <channels> <channel ref="tcp" port="33000"> <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> </channel> </channels> </application> </system.runtime.remoting> </configuration>
和客户端配置
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <client> <wellknown type="ONX.Cmn.MyService, ONXCmn" url="tcp://:33000/MyServiceUri" /> </client> <channels> <channel ref="tcp" port="0"> <clientProviders> <formatter ref="binary" /> </clientProviders> <serverProviders> <formatter ref="binary" typeFilterLevel="Full" /> </serverProviders> </channel> </channels> </application> </system.runtime.remoting> </configuration>
也可以通过代码进行配置。有关详细信息,请参阅 MSDN。现在看看 MyService 类。
public delegate void OnEventHandler(string message); public class MyService : MarshalByRefObject { public event OnEventHandler EventHandler; public string func1() { PublishEventAnfScheduleOneMore("Event from Server: func1() is invoked"); return "MyService.func1()"; } private void PublishEvent(string message) { if (EventHandler != null) { EventHandler(message); } } private void PublishEventAnfScheduleOneMore(string text) { PublishEvent(text); Thread t = new Thread(new ThreadStart(PublishEventIn5Seconds)); t.Start(); } private void PublishEventIn5Seconds() { Thread.Sleep(5000); PublishEvent("5 seconds passed from one of method calls"); } }
如您所见,当某个客户端调用“MyService.func()”时,我们立即调用回调,并且在 5 秒后,我们从单独的线程中再次调用一次。这是为了测试目的,以表明事件可以在任何时候(例如,甚至不是为了响应调用)被调用。我们启动一个单独的线程并将控制权返回给调用“func1()”的客户端。然后,在 5 秒后,我们启动的线程将为所有已注册的事件处理程序引发事件。一旦客户端注册其事件处理程序,它将接收来自服务器的所有事件。
这是我们客户端应用程序的精简代码。完整版本可在“Events”解决方案中获取
class MyClient { private MyService myService_; private EventSink sink_; public MyClient() { //create proxy to remote MyService myService_ = new ONX.Cmn.MyService(); //create event sink that can be invoked by MyService sink_ = new EventSink(new OnEventHandler(MyEventHandlerCallback)); //register event handler with our event sink //(after that event sink will invoke our callback) sink_.Register(myService_); } public void MyEventHandlerCallback(string text) { Log.Print("Got text through callback! {0}", text); } public void Test() { Log.Print("myService.func1() returned {0}", myService_.func1()); } [STAThread] static void Main(string[] args) { RemotingConfiguration.Configure("ONXClient.exe.config"); MyClient c = new MyClient(); c.Test(); Log.WaitForEnter("Press ENTER to exit..."); } }
以下是其中一次测试运行的精简输出
[5412] [09:43:55] myService.func1() returned MyService.func1() [5412] [09:43:55] Press ENTER to exit... [7724] [09:43:55] Got … callback! Event from Server: func1() is invoked [7724] [09:44:00] Got … callback! 5 seconds passed from one of method calls
如您所见,我们第一次调用“func1()”后立即收到初始事件,5 秒后又收到一个。请注意,回调函数是在单独的线程上调用的。因此,如果您需要同步某些数据访问,请务必小心。
10. 异步调用
这个主题与没有远程处理的简单异步调用没有太大区别。让我们分析“Async Calls”解决方案。它有一个简单的 MyService 实现
public class MyService : MarshalByRefObject { public string func1(string text) { Log.Print("func1(\"{0}\") is invoked", text); return text+DateTime.Now.ToString("HH:mm:ss.fff"); } }
以下是它在客户端应用程序中如何使用的示例
delegate string GetStringHandler(string arg); class MyClient { private const int NUMBER_OF_INVOCATIONS = 5; private static void OnCallEnded(IAsyncResult ar) { GetStringHandler handler = ((AsyncResult)ar).AsyncDelegate as GetStringHandler; int index = (int)ar.AsyncState; string result = handler.EndInvoke(ar); Log.Print("myService.func1() #{0} is done. Result is \"{1}\"", index, result); } [STAThread] static void Main(string[] args) { RemotingConfiguration.Configure("ONXClient.exe.config"); MyService myService = new MyService(); Log.Print("myService created. Proxy? {0}", (RemotingServices.IsTransparentProxy(myService)?"YES":"NO")); for(int index=1;index<=NUMBER_OF_INVOCATIONS;++index) { Log.Print("Invoking myService.func1() #{0}...", index); GetStringHandler handler = new GetStringHandler(myService.func1); handler.BeginInvoke("from Client", new AsyncCallback(OnCallEnded), index); } Log.WaitForEnter("Press ENTER to exit..."); } }
如您所见,我们在“for”循环中循环 5 次。每次迭代中,我们都会创建一个与“MyService.func1”方法原型对应的委托,并使用“BeginInvoke(…)”进行异步调用。由于我们将“OnCallEnded”方法作为回调传递,因此当异步调用完成后,我们将在 OnCallEnded 方法中获得控制权。在那里,我们获得委托的引用并通过调用“EndInvoke(ar)”获取结果。
以下是客户端应用程序输出的示例
[0216] [2008/10/03 16:39:45.243] myService created. Proxy? YES [0216] [2008/10/03 16:39:45.243] Invoking myService.func1() #1... [0216] [2008/10/03 16:39:45.274] Invoking myService.func1() #2... [0216] [2008/10/03 16:39:45.274] Invoking myService.func1() #3... [0216] [2008/10/03 16:39:45.290] Invoking myService.func1() #4... [0216] [2008/10/03 16:39:45.290] Invoking myService.func1() #5... [0216] [2008/10/03 16:39:45.290] Press ENTER to exit... [2248] [2008/10/03 16:39:45.290] myService.func1() #2 is done. Result is "from Client16:39:45.274" [3868] [2008/10/03 16:39:45.290] myService.func1() #1 is done. Result is "from Client16:39:45.274" [2248] [2008/10/03 16:39:45.290] myService.func1() #3 is done. Result is "from Client16:39:45.290" [2248] [2008/10/03 16:39:45.290] myService.func1() #5 is done. Result is "from Client16:39:45.290" [3868] [2008/10/03 16:39:45.290] myService.func1() #4 is done. Result is "from Client16:39:45.290"
我们甚至很幸运地按顺序获得了 5 次调用,这与原始顺序不同——调用 #2 比调用 #1 更早结束。调用 #4 和 #5 也是如此。
另外请注意,并非所有调用都在同一线程中运行。而且它们都与我们发起 5 次调用的线程不同。
11. 一个服务器中的多个服务。一个客户端应用程序的多个服务器链接
我查阅的所有 MSDN 和互联网上的示例都只显示服务器应用程序中的单个远程处理对象类型。我很好奇我们如何在单个服务器中引入多个服务。以及我们如何在一个客户端应用程序中使用多个服务器。事实证明这并不难,但亲眼所见总比猜测要好。
让我们分析服务器端具有 2 个周知类型的情况
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <service> <wellknown type="ONX.Cmn.MyService1, ONXCmn" objectUri="MyService1Uri" mode="SingleCall" /> <wellknown type="ONX.Cmn.MyService2, ONXCmn" objectUri="MyService2Uri" mode="SingleCall" /> </service> <channels> <channel ref="tcp" port="33000"/> </channels> </application> </system.runtime.remoting> </configuration>
您不能
1) 拥有多个具有相同协议的通道(例如,“ref”参数)。否则,您将收到一个异常,指出该协议已注册。但您可以指定多个通道,如果它们用于不同的协议。
2) 每个已知类型都应具有唯一的 objectUri。否则,类型定义将重叠,并且只有一个类型可用。
对于上述配置,我们的辅助方法“Utils.DumpAllInfoAboutRegisteredRemotingTypes();”给出
[7496] [2008/10/05 00:01:04.047] ALL REGISTERED TYPES IN REMOTING -(BEGIN)--------- [7496] [2008/10/05 00:01:04.047] WellKnownServiceTypeEntry: type='ONX.Cmn.MyService2, ONXCmn'; objectUri=MyService2Uri; mode=SingleCall [7496] [2008/10/05 00:01:04.047] WellKnownServiceTypeEntry: type='ONX.Cmn.MyService1, ONXCmn'; objectUri=MyService1Uri; mode=SingleCall [7496] [2008/10/05 00:01:04.047] ALL REGISTERED TYPES IN REMOTING -(END) ---------
在我们的例子中,客户端配置如下所示
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.runtime.remoting> <application> <client> <wellknown type="ONX.Cmn.MyService1, ONXCmn" url="tcp://:33000/MyService1Uri" /> <wellknown type="ONX.Cmn.MyService2, ONXCmn" url="tcp://:33000/MyService2Uri" /> </client> </application> </system.runtime.remoting> </configuration>
如果我们想使用来自不同服务器的服务,每个服务器都会监听不同的端口。因此,每个“<wellknown/>”节中都会有不同的端口。
如果您想亲自尝试,有“Two Services in single Server”解决方案。
12. 总结
感谢您的时间。希望有所帮助。欢迎任何评论。一旦有评论和时间,我将尝试调整本文。远程处理愉快!