[ASP.NET学习笔记之二十七]实战.NET Remoting

实战.NET Remoting

 

Remoting技术简介

出现背景

Xml

Soap

序列化

Remoting出现的契机

l         分布式应用的需求迅速增长

进程之间通讯

局域网中计算机通讯

互联网中的通讯

各个领域:商业,娱乐,Peer-to-Peer,网格(Grid)……

l         原有的C/SB/S模式和技术已经不能胜任

串口RS232SocketRPCDCOM

什么是Remoting

l         Remoting的词根——Remote

Remote Object

分布式对象

l         Remoting的优势

性能

扩展性

可配置性

安全

生存周期管理

Remoting使用的技术

l         XML

l         SOAP简单对象传输协议

l         序列化

添加SerializableAttribute

实现ISerializable 接口

 

【实例代码】

主要演示如何把自定义对象序列化为二进制对象、XML对象和SOAP对象

首先看看自定义类

namespace mySerialize

{

    [Serializable]

    public class user

    {

        public DecimalList dl = new DecimalList();

        private decimal Sum, Avg;

        public user()

        {

          

        }

        public void cacl()

        {

            this.Sum = 0;

            foreach (decimal m in dl)

            {

                Sum += m;

            }

            this.Avg = Sum / dl.Count;

        }

    }

    //默认情况下自定义类是不可序列化的。

    //本来在user类可直接引用List<decimal>,但为了演示效果,使用自定义类DecimalList

    [Serializable]

    public class DecimalList : List<decimal>

    {

    }

}

再看看Windows Form窗体如何实现序列化:

public partial class Form1 : Form

{

    private user myUser;

    public Form1()

    {

        InitializeComponent();

    }

    private user BuildUser()

    {

        user u = new user();

        for (int i = 0; i < Convert.ToInt32(this.tb_num.Text); i++)

        {

            u.dl.Add(i);

        }

        u.cacl();

        return u;

}

//生成自定义类的对象实例

    private void btn_Create_Click(object sender, EventArgs e)

    {

        myUser = BuildUser();

}

//生成二进制对象

    private void btn_Bin_Click(object sender, EventArgs e)

    {

        FileStream fs = new FileStream("user.bin", FileMode.Create);

        BinaryFormatter bf = new BinaryFormatter();

        bf.Serialize(fs, myUser);

        fs.Close();

}

//生成XML对象

//注意命名空间using System.Xml.Serialization的引用

    private void btn_Xml_Click(object sender, EventArgs e)

    {

        FileStream fs = new FileStream("user.xml", FileMode.Create);

        System.Xml.Serialization.XmlSerializer xs = new XmlSerializer(typeof(user));

        xs.Serialize(fs, myUser);

        fs.Close();

}

//生成SOAP对象

//首先要手工添加System.Runtime.Remoting

//再引用System.Runtime.Serialization.Formatters.Binary

//System.Runtime.Serialization.Formatters.Soap的命名空间

    private void btn_Soap_Click(object sender, EventArgs e)

    {

        FileStream fs = new FileStream("userSoap.xml", FileMode.Create);

        SoapFormatter sf = new SoapFormatter();

        sf.Serialize(fs, myUser);

        fs.Close();

    }

}

Remoting 框架图

远程对象的两个含义

操作远程对象

对象运行在远程,客户端向他发送消息。

MarshalByRefObject

传递远程对象

将远程的对象拿到本地,或者将本地对象发送过去。

副本进行操作

[Serializable] ISerializable

谁来激活对象?

服务器激活(WellKnown

Singleton

SingleCall

客户端激活

通道(Channels

l         一个远程对象使用通道发送和接收消息

服务器选择一个通道来监听请求(request

客户端选择通道来和服务器通讯

l         Remoting提供了内置的通道

TCP通道和HTTP通道

你也可以编写自己的通道

开发Remoting三步走

1.         创建远程对象

2.         创建一个应用程序作为“宿主(host)”,以接收客户请求

3.         创建一个客户端调用远程对象

第一步:创建远程对象

继承System.MarshalByRefObject

public class HelloServer : MarshalByRefObject

{

……

}

第二步:创建宿主应用程序

l         注册通道

内置通道:TCPHTTP

l         注册服务器激活的远程对象(WellKnown

SingletonSingleCall

URL

l         运行宿主程序

第三步:建立客户端程序

l         注册通道

内置通道:TCPHTTP

l         根据URL得到对象代理

l         使用代理调用远程对象

传递参数

l         传递简单类型

intdoulbestringenum……

l         传递可序列化的类型

ArrayListHashtableDataSet……

l         传递自定义类型

 [Serializable]

【实例代码】

1.         首先看看简单的Remoting调用:

//创建远程对象

public class HelloWorld : MarshalByRefObject

{

    public HelloWorld()

    {

        Console.WriteLine("HelloServer activated");

    }

    public String HelloMethod(String name)

    {

        Console.WriteLine("Server Hello.HelloMethod : {0}", name);

        return "Hi there " + name;

    }

}

创建服务器端程序

class Program

{

    static int Main (string[] args)

    {

        TcpChannel tcpChannel = new TcpChannel(8085);

        HttpChannel httpChannel = new HttpChannel(8086);

 

        ChannelServices.RegisterChannel(tcpChannel);

        ChannelServices.RegisterChannel(httpChannel);

 

        RemotingConfiguration.RegisterWellKnownServiceType(

            typeof(HelloWorld), "user", WellKnownObjectMode.SingleCall);

 

        System.Console.WriteLine("Press Enter key to exit");

        System.Console.ReadLine();

        return 0;

    }

}

创建客户端程序

static void Main (string[] args)

{

    //使用TCP通道得到远程对象

    TcpChannel tcpChannel = new TcpChannel();

    ChannelServices.RegisterChannel(tcpChannel);

    //获取远程对象

    HelloWorld userTcp = (HelloWorld)Activator.GetObject(

                typeof(HelloWorld), "tcp://localhost:8085/user");

    if (userTcp == null)

    {

        System.Console.WriteLine("Could not locate TCP server");

    }

    //使用HTTP通道得到远程对象

    HttpChannel httpChannel = new HttpChannel();

    ChannelServices.RegisterChannel(httpChannel);

    HelloWorld userHttp = (HelloWorld)Activator.GetObject(

                typeof(HelloWorld), "tcp://localhost:8085/user");

    if (userHttp == null)

    {

        System.Console.WriteLine("Could not locate HTTP server");

}

 

    Console.WriteLine("Client1 TCP HelloMethod {0}",

                        userTcp.HelloMethod("Caveman1"));

    Console.WriteLine("Client2 HTTP HelloMethod {0}",

                        userHttp.HelloMethod("Caveman2"));

    Console.ReadLine();

}

以上都要注意对下面命名空间的引用和自定义类的引用:

using System.Runtime.Remoting;

using System.Runtime.Remoting.Channels;

using System.Runtime.Remoting.Channels.Tcp;

using System.Runtime.Remoting.Channels.Http;

2.         假设对user类进行扩展,利用Remoting传输自定义类,代码如下

namespace mySerialize

{

    [Serializable]

    public class User

    {

        string name = "";

        bool male = true;

 

        public User(string name, bool male)

        {

            this.name = name;

            this.male = male;

        }

        public string Name

        {

            get { return name; }

            set { name = value; }

        }

        public bool Male

        {

            get { return male; }

            set { male = value; }

        }

    }

    public class HelloWorld : MarshalByRefObject

    {

        public HelloWorld()

        {

            Console.WriteLine("HelloServer activated");

        }

        public String HelloMethod(String name)

        {

            Console.WriteLine(

                "Server Hello.HelloMethod : {0}", name);

            return "Hi there " + name;

        }

        public String HelloUserMethod(User user)

        {

            string title;

            if (user.Male)

                title = "先生";

            else

                title = "女士";

            Console.WriteLine("Server Hello.HelloMethod : 你好,{0}{1}",user.Name, title);

            return "你好," + user.Name + title;

        }

    }

}

服务器端代码不变,看看客户端怎么实现调用自定义类的方法:

static void Main (string[] args)

{

    TcpChannel tcpChannel = new TcpChannel();

    ChannelServices.RegisterChannel(tcpChannel);

    HelloWorld userTcp = (HelloWorld)Activator.GetObject(

                typeof(HelloWorld), "tcp://localhost:8085/user");

    if (userTcp == null)

    {

        System.Console.WriteLine("Could not locate TCP server");

    }

 

    HttpChannel httpChannel = new HttpChannel();

    ChannelServices.RegisterChannel(httpChannel);

    HelloWorld userHttp = (HelloWorld)Activator.GetObject(

                typeof(HelloWorld), "tcp://localhost:8085/user");

    if (userHttp == null)

    {

        System.Console.WriteLine("Could not locate HTTP server");

}

Console.WriteLine("Client1 TCP HelloUserMethod {0}",

userTcp.HelloUserMethod(new User("张生", true)));

Console.WriteLine("Client2 HTTP HelloUserMethod {0}",

userHttp.HelloUserMethod(new User("崔莺莺", false)));

}

3.         我们看看WellKnownObjectModeSingleCall属性和Singleton属性到底怎么不一样

首先我们还是对user类做一个修改,定义一个整型变量:

public class HelloWorld : MarshalByRefObject

{

    public int callCounter = 0;

 

    public HelloWorld()

    {

        Console.WriteLine("HelloServer activated");

    }

 

    public String HelloMethod(String name,out int counter)

    {

        counter = ++callCounter;

        Console.WriteLine("Server Hello.HelloMethod : {0} Counter :{1}",

name, callCounter);

        return "Hi there " + name;

    }

}

客户端的输出语句变换如下

//多次调用

int counter;

Console.WriteLine("Client1 TCP HelloMethod {0} Counter {1}", userTcp.HelloMethod("Caveman1", out counter), counter);

Console.WriteLine("Client2 HTTP HelloMethod {0} Counter {1}", userHttp.HelloMethod("Caveman2", out counter), counter);

Console.WriteLine("Client2 HTTP HelloMethod {0} Counter {1}",

userHttp.HelloMethod("Caveman3", out counter), counter);

当服务器端的WellKnownObjectMode属性为SingleCall,运行后结果如下:

当服务器端的WellKnownObjectMode属性为Singleton,运行后结果如下:

4.         最后比较一下Value vs RefObject

namespace mySerialize

{

    [Serializable]

    public class MySerialized

    {

        public MySerialized(int val)

        {

            a = val;

        }

        public void Foo()

        {

            Console.WriteLine("MySerialized.Foo called");

        }

        public int A

        {

            get

            {

                Console.WriteLine("MySerialized.A called");

                return a;

            }

            set

            {

                a = value;

            }

        }

        protected int a;

    }

 

    public class MyRemoteObj : System.MarshalByRefObject

    {

        public MyRemoteObj(int val)

        {

            a = val;

        }

        ~MyRemoteObj()

        {

            Console.WriteLine("MyRemote destructor");

        }

        public void Foo()

        {

            Console.WriteLine("MyRemote.Foo called");

        }

        public int A

        {

            get

            {

                Console.WriteLine("MyRemote.A called");

                return a;

            }

            set

            {

                a = value;

            }

        }

        protected int a;

    }

 

    public class HelloWorld : MarshalByRefObject

    {

        public HelloWorld()

        {

            Console.WriteLine("HelloServer activated");

        }

        public String HelloMethod(String name)

        {

            Console.WriteLine(

                "Server Hello.HelloMethod : {0}", name);

            return "Hi there " + name;

        }

        public MySerialized GetMySerialized()

        {

            return new MySerialized(4711);

        }

        public MyRemoteObj GetMyRemote()

        {

            return new MyRemoteObj(4712);

        }

    }

}

客户端的输出代码如下

MySerialized ser = userTcp.GetMySerialized();

if (!RemotingServices.IsTransparentProxy(ser))

{

    Console.WriteLine("ser is not a transparent proxy");

}

ser.Foo();

MyRemoteObj rem = userTcp.GetMyRemote();

if (RemotingServices.IsTransparentProxy(rem))

{

    Console.WriteLine("ser is a transparent proxy");

}

rem.Foo();

 

[MSDN注解]

Activator.GetObject 方法 (Type, String)

为指定类型和 URL 所指示的已知对象创建一个代理。

返回值

一个代理,它指向由所请求的已知对象服务的终结点。

调用代理向远程对象发送消息。对代理调用方法之前,不向网络发送消息。

 

RemotingServices.IsTransparentProxy 方法

返回一个布尔值,该值指示给定的对象是透明代理还是实际对象。

 

客户端在跨任何类型的远程处理边界使用对象时,对对象使用的实际上是透明代理。透明代理使人以为实际对象驻留在客户端空间中。它实现这一点的方法是:使用远程处理基础结构将对其进行的调用转发给真实对象。

透明代理本身由 RealProxy 类型的托管运行时类的实例收容。RealProxy 实现从透明代理转发操作所需的部分功能。代理对象继承托管对象(例如垃圾回收、对成员和方法的支持)的关联语义,可以将其进行扩展以形成新类。这样,该代理具有双重性质,一方面,它需要充当与远程对象(透明代理)相同的类的对象;另一方面,它本身是托管对象。

可以在不考虑 AppDomain 中任何远程处理分支的情况下使用代理对象。应用程序不需要区分代理引用和对象引用。但是,处理激活、生存期管理和事务等问题的服务提供程序需要进行这种区分。

SAOs的配置文件

配置文件

1.         使用配置文件的好处

简化代码

随时更改,通道,端口,URL的设置而不用重新编译

2.         .Net提供了Remoting配置文件的标准

3.         XML格式的配置文件

4.         推荐在实际项目中使用配置文件

 

配置文件举例――服务器端

<configuration>

<system.runtime.remoting>

<application>

<service>

<!-- RemotingSamples :名称空间-->

<!--HelloServer:类名-->

<!--General:类定义所在的程序集-->

<wellknown mode="Singleton" objectUri="SayHello"

type="RemotingSamples.HelloServer, General" />

</service>

<channels>

<!---ref:引用另外一个配置文件,httpmachine配置文件中配置-->

<channel port="8086" ref="http"/>

</channels>

</application>

</system.runtime.remoting>

</configuration>

 

配置文件举例——客户端

<configuration>

<system.runtime.remoting>

<application>

<client>

<wellknown url="http://localhost:8086/SayHello"

type="RemotingSamples.HelloServer, General" />

</client >

<channels>

<channel port=“0" ref="http"/>

</channels>

</application>

</system.runtime.remoting>

</configuration>

 

使用配置文件的代码

配置文件可以写在任意的.config文件中。

Server.cs

Server.exe.config:配置文件名

RemotingConfiguration.Configure("Server.exe.config");//硬编码

Client.cs

RemotingConfiguration.Configure(@"client.exe.config");

HelloServer obj = new HelloServer();

使用new方法,并不是得到构造函数,实际上是得到一个HelloServer的代理,可以设置断点查看是否为代理?

使用App.config作为配置文件

l         App.config在编译后名称自动变为[可执行文件的文件名].exe.config

l         如果使用App.config作为Remoting的配置文件,则代码可以这样写:

string cfg = System.AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;//软编码

RemotingConfiguration.Configure(cfg);

 

引用通道的配置

<configuration>

<system.runtime.remoting>

<application>

<client>

<wellknownurl=" http://localhost:8086/SayHello"

type="RemotingSamples.HelloServer, General" />

</client >

<channels>

<channel port=“0" ref="http"/>

</channels>

</application>

</system.runtime.remoting>

</configuration>

 

Machine.config

位置

%WINDIR%/Microsoft.NET/Framework/[version]/CONFIG

 

租约

对象生存周期

客户机检测服务器是否可用

       调用远程对象的方法是否出现System.Runtime.Remoting.RemotingException

服务器检测客户机是否可用

       租约分布式垃圾回收器(LDGC只对Singleton对象客户端激活对象有效

       SingleCall每一次调用后自己回收自己

Remoting 框架图

涉及到的内容:Client-Activated Singleton

 

租约的配置

租约配置

默认值(秒)

InitialLeaseTime

(初始租约时间)

300

RenewOnCallTime

120

SponsorshipTimeout

(发起者一旦租约到期的等待时间)

120

LeaseManagerPollTime

(租约管理者的轮询时间)

10

续约

隐式续约

       每次调用远程对象上的方法的时候自动进行。

显示的续约

       ILease.Renew();//通过代理的方法得到ILease接口,由客户端主动控制

发起租约

       ISponsor接口//实现这个接口,服务器端为发起者自动续约,由租约管理器自动管理

       ILease.Register()

[实例代码]

1.         服务器端激活:

RemotingConfiguration.RegisterWellKnownServiceType(

                typeof(HelloServer),

                "SayHello",

                WellKnownObjectMode.Singleton);

客户端的代码段:

ILease lease = (ILease)obj.GetLifetimeService();

if (lease != null)

{

    Console.WriteLine("Lease Configuration:");

    Console.WriteLine("InitialLeaseTime: " + lease.InitialLeaseTime);

    Console.WriteLine("RenewOnCallTime: " + lease.RenewOnCallTime);

    Console.WriteLine("SponsorshipTimeout: " + lease.SponsorshipTimeout);

    Console.WriteLine(lease.CurrentLeaseTime);

}

2.         客户端激活

object[] attrs = { new UrlAttribute("tcp://localhost:8085/Hello") };

HelloServer obj = (HelloServer)Activator.CreateInstance(

typeof(HelloServer), null, attrs);

服务器端代码段:

RemotingConfiguration.ApplicationName = "Hello";

RemotingConfiguration.RegisterActivatedServiceType(typeof(HelloServer));

3.         改变租约

l         写代码的方式:重写InitializeLifetimeService方式(MarshalByRefObject的方法

public override object InitializeLifetimeService()

{

    ILease lease=(ILease)base.InitializeLifetimeService();

    if(lease.CurrentState==LeaseState.Initial)

    {

        lease.InitialLeaseTime=TimeSpan.FromSeconds(3);

        lease.SponsorshipTimeout=TimeSpan.FromSeconds(10);

        lease.RenewOnCallTime=TimeSpan.FromSeconds(2);

    }

    return lease;

}

租约永不过期

lease.InitialLeaseTime=TimeSpan.FromSeconds(0);

l         写配置文件

在服务器配置文件的application节点下添加:

<lifetime leaseTime=" 7M " sponsorshipTmeout=" 7M " renewOnCallTime=" 7M "/>

在服务器段代码改成:

RemotingConfiguration.Configure("Server.exe.config");

 

CAOs

Remoting 框架图

涉及到的内容:Client-Activated 以及Registered Created by factory

保存客户状态

客户端激活对象

l         服务器为每个客户端创建一个实例

l         在租约时间到期并且垃圾回收器发挥作用之前对象将一直处于激活状态

【实例代码】

Remoting传输对象的类定义

public class ClientActivatedType : MarshalByRefObject

{

    int count = 0;

    public int Increase()

    {

        count++;

        Console.WriteLine("Increase Method was Called, Count is {0}",count);

        return count;

    }

}

从配置文件来看,一般来说如果是服务器激活的话,service或者client节点下的标签为wellknown;如果是客户端激活,即为activated

服务器端配置文件

<application>

     <service>

         <activated type="ClientActivatedType, General"/>

     </service>

     <channels>

         <channel port="8080" ref="http" />

     </channels>

</application>

代码文件

public static void Main (string[] Args)

{

    RemotingConfiguration.Configure("server.exe.config");

    Console.WriteLine("The server is listening. Press Enter to exit....");

    Console.ReadLine();

    Console.WriteLine("Recycling memory...");

    GC.Collect();

    GC.WaitForPendingFinalizers();

}

客户端配置文件

<application>

     <client url="http://localhost:8080">

         <activated type="ClientActivatedType, General"/>

     </client>

     <channels>

         <channel ref="http" port="0" />

     </channels>

</application>

代码文件

public static void Main (string[] Args)

{

 

    RemotingConfiguration.Configure("client.exe.config");

    ClientActivatedType CAObject = new ClientActivatedType();

 

    Console.WriteLine("Call Increase Method {0}", CAObject.Increase());

    Console.WriteLine("Call Increase Method {0}", CAObject.Increase());

    Console.ReadLine();

    Console.WriteLine("Call Increase Method {0}", CAObject.Increase());

    Console.WriteLine("Call Increase Method {0}", CAObject.Increase());

 

    Console.WriteLine("Press Enter to end the client application domain.");

    Console.ReadLine();

}

通过上面的文件,运行后我们可以看出

l         无论客户段执行多少个实例,count始终是从0开始,也就是说服务器为每个客户端创建一个实例

 

客户端发起租约

l         发起者(Sponsor

发起者是可以为远程对象更新租约的对象。

l         ISponsor接口

l         ClientSponsor

System.Runtime.Remoting.Lifetime

【实例代码】

public class ClientActivatedType : MarshalByRefObject

{

    // Overrides the lease settings for this object.

    public override Object InitializeLifetimeService()

    {

        ILease lease = (ILease)base.InitializeLifetimeService();

        // Normally, the initial lease time would be much longer.

        // It is shortened here for demonstration purposes.

        if (lease.CurrentState == LeaseState.Initial)

        {

            lease.InitialLeaseTime = TimeSpan.FromSeconds(3);

            lease.SponsorshipTimeout = TimeSpan.FromSeconds(10);

            lease.RenewOnCallTime = TimeSpan.FromSeconds(2);

        }

        return lease;

    }

    public string RemoteMethod()

    {

        // Announces to the server that the method has been called.

        Console.WriteLine("ClientActivatedType.RemoteMethod called.");

        // Reports the client identity name.

        return "RemoteMethod called. " + WindowsIdentity.GetCurrent().Name;

    }

}

注意typeFilterLevel="Full"的含意?

typeFilterLevel默认值是low状态,更改的原因在于客户端定义的MyClientSponsor类是要发送到服务器端,必然要经过序列化和反序列化的过程,为了让他们能够自动进行反序列化的操作,所以要把两个formatter的级别设置为full否则程序就会出错

客户端代码:

<application>

     <client url="http://localhost:8080">

         <activated type="ClientActivatedType, General"/>

     </client>

     <channels>

         <channel ref="http" port="0">

              <serverProviders>

                   <formatter ref="soap" typeFilterLevel="Full"/>

                   <formatter ref="binary" typeFilterLevel="Full"/>

              </serverProviders>

         </channel>

     </channels>

</application>

public class Client

{

    public static void Main (string[] Args)

    {

        // Loads the configuration file.

        RemotingConfiguration.Configure("client.exe.config");

        ClientActivatedType CAObject = new ClientActivatedType();

        ILease serverLease = (ILease)RemotingServices.GetLifetimeService(CAObject);

        MyClientSponsor sponsor = new MyClientSponsor();

        // Note: If you do not pass an initial time, the first request will

        // be taken from the LeaseTime settings specified in the

        // server.exe.config file.

        serverLease.Register(sponsor);

        // Calls same method on each object.

        Console.WriteLine("Client-activated object: " + CAObject.RemoteMethod());

        Console.WriteLine("Press Enter to end the client application domain.");

        Console.ReadLine();

    }

}

public class MyClientSponsor : MarshalByRefObject, ISponsor

{

    private DateTime lastRenewal;

    public MyClientSponsor()

    {

        lastRenewal = DateTime.Now;

    }

    //必须重写的ISponsor方法

    public TimeSpan Renewal(ILease lease)

    {

        Console.WriteLine("I've been asked to renew the lease.");

        Console.WriteLine("Time since last renewal:" +

(DateTime.Now - lastRenewal).ToString());

        lastRenewal = DateTime.Now;

        return TimeSpan.FromSeconds(20);

    }

}

服务器端代码:

<application>

     <service>

         <activated type="ClientActivatedType, General"/>

     </service>

     <channels>

         <channel port="8080" ref="http">

              <serverProviders>

                   <formatter ref="soap" typeFilterLevel="Full"/>

                   <formatter ref="binary" typeFilterLevel="Full"/>

              </serverProviders>

         </channel>

     </channels>

</application>

public class Server

{

    public static void Main (string[] Args)

    {

        // Loads the configuration file.

        RemotingConfiguration.Configure("server.exe.config");

        Console.WriteLine("The server is listening. Press Enter to exit....");

        Console.ReadLine();

        Console.WriteLine("Recycling memory...");

        GC.Collect();

        GC.WaitForPendingFinalizers();

    }

}

运行起来的结果如下(每个10秒发起者向服务器续约一次):

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值