一.概述

知名类型的SingleCall对象可以在客户程序的方法调用之后被垃圾收集器清理掉,因为它没有保持状态,属于无状态的。而客户激活的类型的对象和知名类型的SingleTon对象都属于生存期长的对象,如果在客户程序停止使用远程对象之前,远程对象被禁用了,则客户程序会得到一个RemotingException异常。因为该对象已经和下一个方法调用(从客户程序进行的方法调用)断开了连接,只要客户程序需要该对象,它就必须被激活。

微软的DCOM技术使用了Ping机制,在这种机制下,客户程序有规律的对服务程序发出Ping请求,以通知服务程序自己仍旧活着,并通知服务程序自己需要使用哪个对象。.NET Remoting使用的是基于租约的生存期机制,在租约期内,对象一直存活着,直到租借时间结束,.NET Remoting使用Leasing程序完成了这项工作。

.NET Remoting允许我们通过一些方式来修改对象的租约时间,一种方式是编写程序代码来完成,另外一种方式是使用配置文件(有关配置文件的介绍可以参见《使用.NET Remoting开发分布式应用——配置文件篇》的内容),还有一种方式是通过发起人(Sponsor)来配置租约。先来看一下租约配置选项的默认值:

租约配置
 默认值(秒)
 
LeaseTime
 300
 
RenewOnCallTime
 120
 
SponsorshipTimeout
 120
 
LeaseManagerPollTime
 10
 

使用LeaseTime选项可以定义远程对象的最长租借时间。如果客户程序一段时期内不再需要远程对象了,那么该对象将被禁用。每次客户程序使用远程对象调用方法时,RenewOnCallTime定义的一个值会递增租借时间。SponsorshipTimeout选项定义了在调用结束之前的那段默认时间,而LeaseManagerPollTime定义了发起人必须返回延长的那部分租借时间。

租约可以实现 ILease 接口并存储一个属性集合,用于确定更新的策略和方法。您也可以使用调用来更新租约。每次调用远程对象上的方法时,租约时间都会设置为目前 LeaseTime 最大值加上 RenewOnCallTime。LeaseTime 即将过期时,发起者会被要求更新租约。因为我们有时会遇上网络不稳定,所以可能会找不到租约发起者。为了确保不在服务器上留下无效对象,每个租约都带有一个 SponsorshipTimeout。该值指定了租约终止之前等待租约发起者回复的时间长度。如果 SponsershipTimeout 为零,CurrentLeaseTime 会被用于确定租约的过期时间。如果 CurrentLeaseTime 的值为零,则租约不会过期。配置或 API 可用于替代 InitialLeaseTime、SponsorshipTimeout 和 RenewOnCallTime 的默认值。

租约管理器维护着一个按发起时间从大到小存储的发起者列表(它们实现 ISponsor 接口)。需要调用发起者以更新租约时间时,租约管理器会从列表的顶部开始向一个或多个发起者要求更新租约时间。列表顶部的发起者表示其以前请求的租约更新时间最长。如果发起者没有在 SponsorshipTimeOut 时间段内响应,则它会被从列表中删除。通过调用 GetLifetimeService 并将对象租约作为参数,即可以获得该对象租约。该调用是 RemotingServices 类的一个静态方法。如果对象在应用程序域内部,则该调用的参数是对象的本地引用,且返回的租约也是该租约的本地引用。如果对象是远程的,则代理会作为一个参数传递,且返回给调用方的是租约的透明代理。

二.通过配置文件配置租约

在服务器上的应用程序配置文件中编写生存期的配置。在这种方式下,生存期配置对整个应用程序都有效。在应用程序配置文件的<lifttime>标记中,可以通过修改特性的方式来配置。

示例代码:

1<?xml version="1.0" encoding="utf-8" ?>
  2<configuration>
  3    <system.runtime.remoting>
  4        <application>
  5            <service>
  6                <wellknown 
  7                    mode="Singleton" 
  8                    type="RemotingSamples.HelloServer, General" 
  9                    objectUri="SayHello" />
 10            </service>
 11            <channels>
 12                <channel port="8086" ref="http"/>
 13            </channels>
 14            
 15            <lifetime 
 16               leaseTime="7M" 
 17               sponsorshipTimeout="7M" 
 18               renewOnCallTime="7M"
 19               leaseManagerPollTime="7S"
 20               />
 21        </application>
 22    </system.runtime.remoting>
 23</configuration>
 24

三.编写代码配置租约

如果我们需要一些带有不同的生存期要求的远程对象,那么最好是通过编程的方式来为对象设置生存期。在远程对象中,可以覆盖InitializeLifetimeService()方法。基类MarshalByRefObject中的InitializeLifetimeService()方法会返回一个对Ilease接口(该接口可用于修改默认值)的引用,因为只有在租约没有生效的时候才可能修改默认值,所以,我们需要检查租约的当前状态,并把它和枚举值LeaseState.Initial进行比较。

示例代码:

1 public override Object InitializeLifetimeService()
  2        { 
 
  3
  4            ILease lease = (ILease)base.InitializeLifetimeService();
  5            // Normally, the initial lease time would be much longer.
  6            // It is shortened here for demonstration purposes.
  7            if (lease.CurrentState == LeaseState.Initial)
  8            { 
 
  9                lease.InitialLeaseTime = TimeSpan.FromSeconds(3);
 10                lease.SponsorshipTimeout = TimeSpan.FromSeconds(10);
 11                lease.RenewOnCallTime = TimeSpan.FromSeconds(2);
 12            }
 13            return lease;
 14        }

租约的状态LeaseState枚举值如下表所示:

租约状态的枚举值
 说明
 
Active
 指明租约处于激活状态
 
Expired
 表明租约已经期满,不能再恢复。当租约管理器发现对象上的租约已经期满,它将联系处于发起人列表中的租约发起人,决定是否恢复它的租约。如果发起人的响应超时,它将尝试联系发起人列表中的下一个发起人。如果租约管理器不能成功的从任何一个发起人那里获得一个租约恢复响应,它将租约对象设置为Expired状态。一旦如此,租约对象就不能再复活,只能被垃圾收集器收集
 
Initial
 表明租约还没有被创建,但仍然没有被激活
 
Null
 租约还没有被初始化
 
Renewing
 表明租约已经期满,租约管理器正在寻找发起人。这个状态指出租约管理器正在尝试联系已经为这个对象的租约恢复而注册的租约发起人
 

只有当租约处于初始状态时,才可以更改租约属性。InitializeLifetimeService 的实现通常调用基类的相应方法,来检索远程对象的现有租约。如果在此之前从未对该对象封送过,则返回的租约会处于其初始状态且可以设置租约属性。一旦封送了对象,则租约会从初始状态变为激活状态,并忽略任何初始化租约属性的尝试(但有一种情况例外)。激活远程对象时将调用 InitializeLifetimeService。通过激活调用可以提供一个租约发起者的列表,而且当租约处于激活状态时,可以随时将其他发起者添加到列表中。

可以下列方式延长租约时间:

客户端可以调用 Lease 类上的 Renew 方法。 
租约可以向某个发起者请求 Renewal。 
当客户端调用对象上的某个方法时,RenewOnCall 值会自动更新租约。 
一旦租约过期,其内部状态会由 Active 变为 Expired,且不再对发起者进行任何调用,对象也会被作为垃圾回收。一般情况下,如果发起者分散在 Web 上或位于某个防火墙的后面,远程对象回叫发起者时会遇到困难。因此,发起者不必与客户端处于同一位置,只要远程对象能够访问得到,它可以为网络上的任意位置。

四.通过发起者来配置租约

我们也可以通过发起者来修改生存期服务数值。通过发起者配置,.NET Remoting运行时使用ISponsor接口来延长远程对象的生存期,ISponsor定义了Renewal()方法,.NET Remoting的基础结构会调用该方法来延长当前对象的租借时间。使用租约参数,可以读取当前租约的配置和租借时间的实际情况。我们必须使用返回值为对象定义额外的租借时间。在下面的示例代码中,创建了一个发起者,并修改它的相关的配置参数。

示例代码:

1using System;
  2using System.Runtime.Remoting;
  3using System.Runtime.Remoting.Channels;
  4using System.Runtime.Remoting.Channels.Tcp;
  5using System.Runtime.Remoting.Channels.Http;
  6using System.Runtime.Remoting.Activation;
  7using System.Runtime.Remoting.Lifetime;
  8using System.IO;
  9
 10namespace RemotingSamples 
 11{ 
 
 12    public class Client
 13    { 
 
 14        public static void Main(string[] args)
 15        { 
 
 16            //使用TCP通道得到远程对象
 17            ChannelServices.RegisterChannel(new HttpChannel());
 18
 19            HelloServer obj = (HelloServer)Activator.GetObject(
 20              typeof(RemotingSamples.HelloServer),
 21              "http://localhost:8086/SayHello");
 22            if (obj == null)
 23            { 
 
 24                System.Console.WriteLine(
 25                    "Could not locate HTTP server");
 26            }
 27            
 28
 29            MySponsor sponsor = new MySponsor();
 30            sponsor.RenewalTime = TimeSpan.FromMinutes(2);
 31            sponsor.Register(obj);
 32
 33            ILease lease = (ILease)obj.GetLifetimeService();
 34            if (lease != null)
 35            { 
 
 36                Console.WriteLine("Lease Configuration:");
 37                Console.WriteLine("InitialLeaseTime: " +
 38                    lease.InitialLeaseTime);
 39                Console.WriteLine("RenewOnCallTime: " +
 40                    lease.RenewOnCallTime);
 41                Console.WriteLine("SponsorshipTimeout: " +
 42                    lease.SponsorshipTimeout);
 43                Console.WriteLine(lease.CurrentLeaseTime);
 44            }
 45
 46        }
 47
 48    }
 49
 50    public class MySponsor:ClientSponsor,ISponsor
 51    { 
 
 52        TimeSpan ISponsor.Renewal(ILease lease)
 53        { 
 
 54            Console.WriteLine("Renewal called");
 55
 56            return this.RenewalTime;
 57        }
 58    }
 59}
 60

五.总结

通过租约来管理远程对象的生存期可以作为引用计数的一种替代方法,因为当网络连接的性能不可靠时,引用计数会显得复杂和低效。尽管有人会坚持认为远程对象的生存期比所需的时间要长,但与引用计数和连接客户相比,租约降低了网络的繁忙程度,将会成为一种非常受欢迎的解决方案。

附录:一个完整的用程序代码配置租约生存期的例子

Server.cs
 1using System;
  2using System.Runtime.Remoting;
  3using System.Runtime.Remoting.Channels;
  4using System.Runtime.Remoting.Channels.Tcp;
  5using System.Runtime.Remoting.Channels.Http;
  6
  7namespace RemotingSamples 
  8{ 
 
  9
 10    public class Server
 11    { 
 
 12        public static int Main(string [] args) 
 13        { 
 
 14
 15
 16             TcpChannel chan1 = new TcpChannel(8085);
 17            HttpChannel chan2 = new HttpChannel(8086);
 18
 19            ChannelServices.RegisterChannel(chan1);
 20            ChannelServices.RegisterChannel(chan2);
 21
 22            //服务器端激活。
 23            RemotingConfiguration.RegisterWellKnownServiceType
 24                (
 25                typeof(HelloServer),
 26                "SayHello",
 27                WellKnownObjectMode.Singleton
 28                );      
 29
 30            System.Console.WriteLine("Press Enter key to exit");
 31            System.Console.ReadLine();
 32            return 0;
 33        }
 34
 35    }
 36}
 37

HelloWord.cs

1using System;
  2using System.Collections.Generic;
  3using System.Text;
  4using System.Runtime.Remoting.Lifetime;
  5
  6namespace RemotingSamples
  7{ 
 
  8    public class HelloServer : MarshalByRefObject
  9    { 
 
 10        public HelloServer()
 11        { 
 
 12            Console.WriteLine("HelloServer activated");
 13        }
 14        public String HelloMethod(String name)
 15        { 
 
 16            Console.WriteLine(
 17                "Server Hello.HelloMethod : {0}", name);
 18            return "Hi there " + name;
 19        }
 20
 21        // Overrides the lease settings for this object.
 22        public override object InitializeLifetimeService()
 23        { 
 
 24
 25            ILease lease = (ILease)base.InitializeLifetimeService();
 26            // Normally, the initial lease time would be much longer.
 27            // It is shortened here for demonstration purposes.
 28            if (lease.CurrentState == LeaseState.Initial)
 29            { 
 
 30                lease.InitialLeaseTime = TimeSpan.FromSeconds(3);
 31                lease.SponsorshipTimeout = TimeSpan.FromSeconds(10);
 32                lease.RenewOnCallTime = TimeSpan.FromSeconds(2);
 33            }
 34            return lease;
 35        }
 36
 37    }
 38}
 39
 40        
 41
 42
 43

Client.cs
 

1using System;
  2using System.Runtime.Remoting;
  3using System.Runtime.Remoting.Channels;
  4using System.Runtime.Remoting.Channels.Tcp;
  5using System.Runtime.Remoting.Channels.Http;
  6using System.Runtime.Remoting.Activation;
  7using System.Runtime.Remoting.Lifetime;
  8using System.IO;
  9
 10namespace RemotingSamples 
 11{ 
 
 12    public class Client
 13    { 
 
 14        public static void Main(string[] args)
 15        { 
 
 16            //使用TCP通道得到远程对象
 17            ChannelServices.RegisterChannel(new HttpChannel());
 18
 19            HelloServer obj = (HelloServer)Activator.GetObject(
 20              typeof(RemotingSamples.HelloServer),
 21              "http://localhost:8086/SayHello");
 22            if (obj == null)
 23            { 
 
 24                System.Console.WriteLine(
 25                    "Could not locate HTTP server");
 26            }
 27            
 28
 29            ILease lease = (ILease)obj.GetLifetimeService();
 30            if (lease != null)
 31            { 
 
 32                Console.WriteLine("Lease Configuration:");
 33                Console.WriteLine("InitialLeaseTime: " +
 34                    lease.InitialLeaseTime);
 35                Console.WriteLine("RenewOnCallTime: " +
 36                    lease.RenewOnCallTime);
 37                Console.WriteLine("SponsorshipTimeout: " +
 38                    lease.SponsorshipTimeout);
 39                Console.WriteLine(lease.CurrentLeaseTime);
 40            }
 41
 42        }
 43
 44    }
 45
 46}
 47