RMI 用法入门

 

RMI 用法入门


本教程展示使用 Java 远程方法调用 (RMI) 创建经典“Hello World”程序分布式版本的操作步骤。使用本示例时,可能会遇到一些相关的问题。您可以从 RMI 常见问题中找到解答,也可访问 rmi-users 别名电子邮件归档。如果要预订 rmi-users 电子邮件别名,可单击这里

分布式“Hello World!”示例使用 applet 对服务器进行远程方法调用,从中可下载 applet 以检索消息“Hello World!”。当 applet 运行时,客户机浏览器上将显示“Hello World!”。

本教程组织如下:

  1. 编写 Java 源文件及 HTML 文件的步骤
  2. 编译和发布类文件及 HTML 文件的步骤
  3. 启动 RMI 注册服务程序、服务器和 applet 的步骤

本教程所需的文件为:

注意:

要得到本教程中所用的全部源代码,可选择下列一种下载格式:


编写 Java 源文件及 HTML 文件

由于 Java 语言需要类的包全名和该类目录路径之间的映射关系,因此在开始写 Java 代码前应确定包和目录的名称。该映射可以帮助 Java 编译器找到该目录,从中可查找 Java 程序中提及的类文件。对于本教程中的程序,包名为 examples.hello,而源目录为 $HOME/mysrc/examples/hello

要在 Solaris 上为源文件创建目录,可执行如下命令:

mkdir -p $HOME/mysrc/examples/hello

mkdir mysrc
mkdir mysrc/examples
mkdir mysrc/examples/hello

本部分需要完成三项任务:

  1. 将远程类的功能定义为 Java 接口
  2. 编写实现和服务器类
  3. 编写使用远程服务的客户机程序

将远程类的功能定义为 Java 接口

在 Windows 平台上,可转到所选目录下,然后键入:

在 Java 中,远程对象是实现远程接口的类的实例。远程接口声明每个要远程调用的方法。远程接口有如下特征:

  • 远程接口必须声明为 public。如果不这样,除非客户端与远程接口在同一个包内,否则当试图装入实现该远程接口的远程对象时,调用会得到错误结果。
  • 远程接口扩展 java.rmi.Remote 接口。
  • 除了所有应用程序特定的例外之外,每个方法还必须在 throws 子句中声明 java.rmi.RemoteException(或 RemoteException 的父类)。
  • 任何作为参数或返回值(直接或嵌入本地对象)传送的远程对象的数据类型必须声明为远程接口类型(例如,Hello),而不应声明为实现类(HelloImpl)。

    下面是远程接口 examples.hello.Hello 的接口定义。该接口只包含一个方法 sayHello,它将向调用程序返回一个字符串:

    package examples.hello;

    public interface Hello extends Remote {
            String sayHello() throws RemoteException;
    }

    import java.rmi.Remote;
    import java.rmi.RemoteException;

因为远程方法调用失败的原因与本地方法调用失败的原因很不相同(由于网络连接的通讯问题或服务器问题),所以远程方法将通过抛出 java.rmi.RemoteException 报告通讯失败。有关分布式系统中失败和恢复的详细信息,参见分布式计算的说明

编写实现和服务器类

远程对象实现类至少应具备下列条件:

该上下文中的“server”类具有创建远程对象实现实例的 main 方法,并可在 rmiregistry 中将该实例绑定到名字上。包含 main 方法的类可以是实现类自身,也可以完全是另一个类。

本例中,main 方法是 examples.hello.HelloImpl 的一部分。该服务器程序需要:

上述六步中各步的解释跟在 HelloImpl.java 源程序后。

package examples.hello; import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.RMISecurityManager; import java.rmi.server.UnicastRemoteObject; public class HelloImpl extends UnicastRemoteObject      implements Hello {     public HelloImpl() throws RemoteException {        super();        }     public String sayHello() {         return "Hello World!";      }     public static void main(String args[]) {

       // 创建并安装安全管理器
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new RMISecurityManager());
        }
        try {
            HelloImpl obj = new HelloImpl();
            // 将该对象实例与名称“HelloServer”捆绑
            Naming.rebind("//myhost/HelloServer", obj);
            System.out.println("HelloServer bound in registry");
        } catch (Exception e) {
             System.out.println("HelloImpl err: " + e.getMessage());
             e.printStackTrace();
        }
      }
}

实现远程接口
在 Java 语言中,当类声明它实现一个接口时,类和编译器之间即形成一个约定。加入本约定后,该类即承诺为其所实现的接口中声明的每个方法签名提供方法体或定义。接口方法显式上为 publicabstract。这样,如果实现类不履行该约定,则按照定义它将成为 abstract 类。如果未声明该类为 abstract,则编译器将予以指明。

本例中的实现类为 examples.hello.HelloImpl。实现类声明它将实现的远程按口。以下是 HelloImpl 类的声明:

public class HelloImpl extends UnicastRemoteObject 牋牋 implements Hello

为便利起见,实现类可扩展远程对象,如本例中的 java.rmi.server.UnicastRemoteObject。通过扩展 UnicastRemoteObject,可用 HelloImpl 类来创建满足下列条件的远程对象:

  • 使用 RMI 缺省的、基于套接字的通讯传输
  • 始终保存运行状态

完成本教程后,如果需要在客户机请求时激活(可创建)远程对象而不使之始终处于运行状态,可参见远程对象激活教程。也可通过创建自定义 RMI 套接字工厂教程学习如何使用自己的通讯协议(而不是 RMI 缺省使用的 TCP 套接字)。

为远程对象定义构造函数
用于远程对象的构造函数可提供与非远程类构造函数同样的功能:它初始化每个新建类的实例,并且将该类的实例返回调用构造函数的程序。

另外,远程对象实例需要“导出”。导出远程对象后,即可通过在匿名端口上监听对远程对象的到来的调用来接受到来的远程方法请求。当扩展 java.rmi.server.UnicastRemoteObjectjava.rmi.activation.Activatable 时,将依据创建内容自动导出类。

如果选择从 UnicastRemoteObjectActivatable 以外的某个类扩展远程对象,则可通过从类构造函数(或其它适当的初始化方法)调用 UnicastRemoteObject.exportObjectActivatable.exportObject 方法显式地导出该远程对象。

因为对象导出可能会抛出 java.rmi.RemoteException,所以即使别无它用,也必须定义抛出 RemoteException 的构造函数。如果忽略该构造函数,则 javac 将生成如下错误消息:

HelloImpl.java:13: Exception java.rmi.RemoteException must be caught, or it must be declared in the throws clause of this method.


              super();
                   ^
1 error

复习:

  • 实现远程接口
  • 导出对象从而可接受到来的远程对象调用
  • 声明构造函数,至少抛出 java.rmi.RemoteException

以下是 examples.hello.HelloImpl 类的构造函数:

public HelloImpl() throws RemoteException {


        super();
}

注意如下事项:

  • super 方法调用将调用 java.rmi.server.UnicastRemoteObject 的无参数构造函数,用来导出远程对象。
  • 构造函数必须抛出 java.rmi.RemoteException,因为如果通讯资源不可用,则在构造过程中 RMI 导出远程对象的企图就可能会失败。

虽然缺省情况下将调用父类的无参数构造函数 super()(即使被省略),但本例中仍包含了 super() 以进一步说明 Java 虚拟机在构造类之前将构造父类的事实。

为每个远程方法提供实现
远程对象的实现类中包含实现每个远程接口所指定的远程方法的代码。例如,以下是 sayHello 方法的实现。该方法向调用程序返回字符串“Hello World!”:

public String sayHello() throws RemoteException {       return  "Hello World!"; }

远程方法的参数或返回值可以是任何 Java 类型,也包括对象(但前提是这些对象实现接口 java.io.Serializable)。java.langjava.util 中有很多核心 Java 类实现 Serializable 接口。在 RMI 中:

  • 缺省情况下,本地对象将以复制方式传送。这意味着除标记有 statictransient 的数据外,对象的所有数据成员(或域)均将被复制。有关如何改变缺省序列化行为的信息,参见 Java 对象序列化规范
  • 远程对象将以引用方式传送。对远程对象的引用实质上就是对 stub 程序的引用,后者是远程对象的客户机端代理程序。stub 程序在 RMI 规范中有完整的说明。创建 stub 程序的步骤将在本教程的使用 rmic 生成 stub 程序和 skeleton(可选)部分进行说明。

类可定义远程接口中未指定的方法,但这些方法只能在虚拟机运行服务器时调用,而不能进行远程调用

创建和安装安全管理器
服务器的 main 方法首先需要创建和安装安全管理器:可以是 RMISecurityManager,也可以自己定义。例如:

if (System.getSecurityManager() == null) {


    System.setSecurityManager(new RMISecurityManager());
}

只有安全管理器处于运行状态下,才能保证所装载的类不执行对其禁用的操作。如果没有指定安全管理器,则除在本地 CLASSPATH 中可找到的类以外,RMI 客户机或服务器不允许装载任何其它类。

创建一个或多个远程对象的实例
服务器的 main 方法需要创建一个或多个用于提供服务的远程对象实现的实例。例如:

HelloImpl obj = new HelloImpl();

构造函数将导出远程对象。这意味着一旦创建,远程对象就准备好了接受到来的调用。

注册远程对象
对于可调用远程对象方法的调用程序(客户机、peer 或 applet ),必须首先获得对远程对象的引用。

为完成自举过程,RMI 系统提供了远程对象注册服务程序,允许以“//host/objectname” 形式将 URL 格式的名字绑到远程对象上。这里的 objectname 可以是任何字符串名。

RMI 注册服务程序是简单服务器方名字服务器,允许远程客户机取得对远程对象的引用。它通常用于定位第一个 RMI 客户机需要对话的远程对象。而该对象反过来则提供对面向特定应用程序的支持以查找其它对象。

例如,可将另一个远程方法调用的参数或返回值作为所获得的引用。有关如何进行本操作的讨论,参见将工厂模型应用于 RMI

一旦远程对象在服务器上注册,调用程序就可通过名字查询该对象,获得远程对象引用,而后远程调用该对象的方法。

例如,下列代码将名字“HelloServer”绑定到对远程对象的引用上:

Naming.rebind("//myhost/HelloServer", obj);

请注意有关 rebind 方法调用的下列参数:

  • 第一个参数是 URL 格式的 java.lang.String,表示远程对象的位置和名字。
    • 需要将 myhost 的值更改为服务器名或 IP 地址。否则,如果在 URL 中省略,则主机缺省值为当前主机,而且在 URL 中无需指定协议(例如“HelloServer”)。
    • 在 URL 中,可以选择提供端口号:例如“//myhost:1234/HelloServer”。端口缺省值为 1099。除非服务器在缺省 1099 端口上创建注册服务程序,否则需要指定端口号。
  • 第二个参数为从中调用远程方法的对象实现引用。
  • RMI 运行时将用对远程对象 stub 程序的引用代替由 obj 参数指定的实际远程对象引用。远程实现对象(如 HelloImpl 实例)将始终不离开创建它们的虚拟机。因此,当客户机在服务器的远程对象注册服务程序中执行查找时,将返回包含该实现的 stub 程序的对象。

出于安全原因,应用程序只能绑定或取消绑定在同一主机上运行的注册服务程序。这可防止客户机删除或覆盖服务器远程注册服务程序的任何项。但是,用户仍可从任何主机上进行查找。

远程对象的实现类需要:

编写使用远程服务的客户机程序

为了取得字符串“Hello World!”,分布式“Hello World”示例的 applet 方将远程调用 sayHello 方法。该字符串将在 applet 运行时显示出来。以下是 applet 的代码:


  1. package examples.hello;

    import java.applet.Applet;
    import java.awt.Graphics;
    import java.rmi.Naming;
    import java.rmi.RemoteException;

    public class HelloApplet extends Applet {

        String message = "blank";
       
       //“obj”是一个标识符,我们将用它指向
       // 实现“Hello”接口的
       // 远程对象
       Hello obj = null;

       public void init() {
           try {
               obj = (Hello)Naming.lookup("//" +
                            getCodeBase().getHost() + "/HelloServer");
               message = obj.sayHello();
           } catch (Exception e) {
               System.out.println("HelloApplet exception: " +
                                       e.getMessage());
               e.printStackTrace();
          }
      }

         public void paint(Graphics g) {
             g.drawString(message, 25, 50);
         }
      }

  1. 首先,applet 从服务器主机的 rmiregistry 取得对远程对象实现(名为“HelloServer”)的引用。如同 Naming.rebind 方法,Naming.lookup 方法使用 URL 格式的 java.lang.String。本例中,applet 通过 getCodeBase 方法和 getHost 方法构造 URL 字符串。Naming.lookup 负责如下任务:
    • 利用作为参数提供给 Naming.lookup 的“hostname”和端口号来构造注册服务程序 stub 程序实例(联系服务器的注册服务程序)
    • 利用 URL 名字(“HelloServer”),使用注册服务程序 stub 程序调用注册服务程序上的远程 lookup 方法。
      • 注册服务程序向该名字返回 HelloImpl_Stub 实例
      • 接收远程对象实现 (HelloImpl) stub 程序实例,并从 CLASSPATH 或 stub 程序的 codebase 装载该 stub 类 (examples.hello.HelloImpl_Stub)
    • Naming.lookup 将 stub 程序返回调用程序 (HelloApplet)

     

  2. applet 调用服务器远程对象上的远程 sayHello 方法。
    • RMI 序列化并返回应答字符串“Hello World!”
    • RMI 对该字符串进行序列化恢复并将其存储到名为 message 的变量中。
  3. applet 调用 paint 方法,将字符串“Hello World!”显示到 applet 的绘制区域中。

所构造的 URL 字符串将作为参数传递给

以下是引用 Hello World applet 的网页 HTML 代码:


<HTML>
<title>Hello World</title>
<center> <h1>Hello World</h1> </center>

The message from the HelloServer is:
<p>
<applet codebase="myclasses/"
        code="examples.hello.HelloApplet"
        width=500 height=120>
</applet>
</HTML>

注意事项如下:

  • 在要下载类的计算机上需要有 HTTP 服务器处于运行状态。
  • 本例中的 codebase 在网页进行自装载的目录下指定了一个目录。我们建议使用这种相对路径。例如,如果 applet 的 HTML 所引用的 codebase 目录(其中有 applet 的类文件)在 HTML 目录的上一层,则可使用相对路径“../”。
  • applet 的 code 属性指定 applet 的包全名。本例中即 examples.hello.HelloApplet

    code="examples.hello.HelloApplet"

Naming.lookup 方法。它必须包括服务器的“hostname”。否则,applet 的查询对象将缺省为客户机。同时,由于 applet 不能访问本地系统(被限制为只能与 applet 的主机通讯),所以 AppletSecurityManager 将抛出异常

编译并发布类文件和 HTML 文件

“Hello World”示例的源代码现已完成。$HOME/mysrc/examples/hello 目录下有四个文件:

  • Hello.java,其中包含 Hello 远程接口的源代码。
  • HelloImpl.java,它是远程对象实现 HelloImpl 的源代码,“Hello World”applet 的服务器。
  • HelloApplet.java,它是 applet 的源代码。
  • hello.html,它是引用“Hello World”applet 的网页。

本部分将编译 .java 源文件以创建 .class 文件。而后将运行 rmic 编译器以创建 stub 程序和 skeleton。stub 程序是远程对象的客户机方代理程序,用来将 RMI 调用转发给服务器方分配器 (dispatcher),后者再将该调用转发给实际的远程对象实现。

当使用 javacrmic 编译器时,必须指定最终类文件所应驻留的位置。对于 applet ,所有文件都应在 applet 的 codebase 目录之下。本例中,该目录为 $HOME/public_html/myclasses

某些 Web 服务器允许通过 "http://host/~username/" 形式的 HTTP URL 访问用户的 public_html 目录。如果 Web 服务器不支持该规则,则可使用形如“file://home/username/public_html”的文件 URL。

本部分有四项任务:

  1. 编译 Java 源文件
  2. 使用 rmic 生成 stub 程序和 skeleton
  3. 将 HTML 文件移动到发布目录
  4. 为运行时设置路径
  1. 编译 Java 源文件

    在尝试编译前,应确保发布目录 $HOME/public_html/myclasses 和开发目录 $HOME/mysrc/examples/hello 都可通过开发计算机上的本地 CLASSPATH 进行访问。

    要编译 Java 源文件,请运行 javac 命令:

    javac -d $HOME/public_html/myclasses        Hello.java HelloImpl.java HelloApplet.java

    该命令在 $HOME/public_html/myclasses 目录下创建目录 examples/hello(如果它不存在)。而后将文件 Hello.classHelloImpl.classHelloApplet.class 写入该目录。它们分别代表远程接口、实现和 applet。有关 javac 选项的说明,参见 Solaris javac 手册网页Win32 javac 手册网页

    使用 rmic 生成 skeleton 和/或 stub 程序

    要创建 stub 程序和 skeleton 文件,应以包含远程对象实现的已编译类包全名运行 rmic 编译器(例如 my.package.MyImpl)。rmic 命令采用一个或多个类名为参数,生成 MyImpl_Skel.classMyImpl_Stub.class 形式的类文件。

    在 JDK 1.2 中,缺省情况下 rmic 将在选中 -vcompat 标志的情况下运行。所生成的 stub 程序和 skeleton 支持访问:

      1. 1.1 客户机的 Unicast(而不是 Activatable)远程对象
      2. 1.2 客户机的各种远程对象。

如果只需支持 1.2 客户机,则可通过 -v1.2 选项运行 rmic。有关 rmic 选项的说明,参见 Solaris rmic 手册网页Win32 rmic 手册网页

例如,要为“HelloImpl”远程对象实现创建 stub 程序和 skeleton,可运行 rmic 如下:

rmic -d $HOME/public_html/myclasses examples.hello.HelloImpl

-d 选项指示放置已编译 stub 程序和 skeleton 类文件的根目录。这样,上述命令将在目录 $HOME/public_html/myclasses/examples/hello 中创建下列文件:

      • HelloImpl_Stub.class
      • HelloImpl_Skel.class


所生成的 stub 类所实现的远程接口设置正好与远程对象本身相同。这意味着客户机可使用 Java 语言的内置运算符进行强制类型转换和拼写检查。同时也表示 Java 远程对象真正支持面向对象的多态性。

将 HTML 文件移动到发布目录

要使引用 applet 的网页对客户机可见,必须将 hello.html 文件从开发目录移至 applet 的 codebase 目录。例如:

mv $HOME/mysrc/examples/hello/hello.html $HOME/public_html/

为运行时设置路径

确保当运行 HelloImpl 服务器时,通过服务器的本地 CLASSPATH 可访问 $HOME/public_html/codebase 目录。


启动 RMI 注册服务程序、服务器和 applet

本部分要完成三项任务:

    1. 启动 RMI 注册服务程序
    2. 启动服务器
    3. 运行 applet

    启动 RMI 注册服务程序

    RMI 注册服务程序是简单的服务器方名字服务器,允许远程客户机取得对远程对象的引用。通常它只用于定位应用程序需要对话的第一个远程对象。而后该对象将反过来提供应用程序特定的支持以查找其它对象。

    注意:在启动 rmiregistry 之前,必须确保运行 registry 的 shell 或窗口没有设置 CLASSPATH,同时也应确保其 CLASSPATH 不包括到任何要下载到客户机的类的路径,包括远程对象实现类的 stub 程序

    如果启动 rmiregistry,而且它在其 CLASSPATH 中找到 stub 类,它就将忽略服务器的 java.rmi.server.codebase 属性。这样,客户机将不能为远程对象下载 stub 程序代码

    要在服务器上启动注册服务程序,可执行 rmiregistry 命令。本命令没有输出,通常在后台运行。有关 rmiregistry 的详细信息,参见 Solaris rmiregistry 手册网页Win32 rmiregistry 手册网页

    例如,在 Solaris 上:

    rmiregistry &

    而在 Windows 95 或 Windows NT 上:

    start rmiregistry

    (如果无法启动,则使用 javaw

    注册服务程序的缺省运行端口为 1099。要在其它端口上启动注册服务程序,可利用命令行指定端口号。例如,要在 Windows NT 系统上从端口 2001 启动注册服务程序:

    start rmiregistry 2001

    如果注册服务程序是在缺省端口以外的端口运行,则需要在名称中指定端口号。当调用注册服务程序时,该名称将传给 java.rmi.Naming 类基于 URL 的方法。例如在 Hello World 示例中,如果注册服务程序在端口 2001 上运行,则将 HelloServer 的 URL 绑定到远程对象引用所需的调用为:

    Naming.rebind("//myhost:2001/HelloServer", obj);

    如果需要修改远程接口或使用远程对象实现中的已修改/附加的远程接口,则必须停机并重新启动注册服务程序。否则,注册服务程序中绑定的对象类型引用将与已修改的类不匹配

    启动服务器

    当启动服务器时,必须指定 java.rmi.server.codebase 属性,从而可动态地将 stub 类下载到注册服务程序及客户机上。运行服务器,将 codebase 属性设置为 stub 实现的位置。因为实现只可引用单个目录,故应确保任何其它可能需要下载的类也已安装到 java.rmi.server.codebase 所引用的目录中。

    有关每个 java.rmi.server 属性的说明,单击这里。要了解所有可用的 java.rmi.activation 属性,单击这里。有关 java 选项的说明,参见 Solaris java 手册网页Win32 java 手册网页。如果在运行示例代码时遇到问题,参见 RMI 和序列化常见问题

    注意:只有当 stub 类在本地尚不可用服务器上已将 java.rmi.server.codebase 属性设置为类文件所在位置时,才可将该类动态下载到客户机的虚拟机上

    在同一命令行需要执行四项操作:“java”命令,后面跟两项属性,即一对 name=value(对于 codebase 属性,注意从“-D”到最后的“/”都没有空格),随后是服务器程序的全包名。在“java”后、两项属性与“examples”(在浏览器或纸上将它作为文本浏览时,很难发现它)之间,应有一个空格。下列命令显示如何启动 HelloImpl 服务器,同时指定 java.rmi.server.codebasejava.security.policy 属性:

    java -Djava.rmi.server.codebase=http://myhost/~myusrname/myclasses/       -Djava.security.policy=$HOME/mysrc/policy ?examples.hello.HelloImpl


    为在系统上运行本代码,需要将策略文件的位置改为示例源代码所安装的系统目录位置。注意:在本例中,为简单起见,我们使用一个任何人随时随地都享有全局权限的策略文件。请不要在生成环境下使用本策略文件。有关如何使用 java.security.policy 文件扩大权限的详细信息,参见下列文件:

    1. http://java.sun.com/products/jdk/1.2/docs/guide/security/PolicyFiles.html


      http://java.sun.com/products/jdk/1.2/docs/guide/security/permissions.html

    codebase 属性将解析为 URL,所以它必须具备“http://aHost/somesource/”、“file:/myDirectory/location/”或某些操作系统所需要的“file:///myDirectory/location/”形式(“file”后有三条斜杠)。

    请注意,其中每个样本 URL 字符串都有“/”。该斜杠是 java.rmi.server.codebase 属性设置 URL 所必需的,这样实现即可正确解析(查找)类的定义。

    如果在 codebase 属性中忽略了斜杠,或在源文件处无法定位类文件(它们不可真正用于下载),或者拼错了属性名,就会抛出 java.lang.ClassNotFoundException。当试图将远程对象绑定到 rmiregistry 时,或当第一个客户机试图访问该对象的 stub 程序时,将抛出本异常。如果发生后一种情况,则也会有另一个问题,因为 rmiregistry 会在其 CLASSPATH 中查找 stub 程序

    输出形式应如下所示:

    HelloServer bound in registry

    运行 applet

    只要注册服务程序和服务器处于运行状态,即可运行 applet。可通过加载其网页到浏览器或 appletviewer 来运行 applet,如下所示:

    appletviewer http://myhost/~myusrname/hello.html &

    运行 appletviewer 之后,显示器上的输出内容与下图相似:





  1. 版权所有 © 1996, 1997, 1998 Sun Microsystems, Inc. 保留所有权利。

本教程下面的部分中,术语“远程对象实现”、“对象实现”和“实现”可互换使用,它们均指实现远程接口的类 examples.hello.HelloImpl
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值