JAVA NIO 介绍

使用JavaSocket API编写一个简单的TCP Echo Server。其阻塞式IO的处理方式虽然简单,但每个客户端都需要一个单独的Thread来处理,当服务器需要同时处理大量客户端时,这种做法不再可行。使用NIO API可以让一个或有限的几个Thread同时处理连接到服务器上的所有客户端。

NIO API允许一个线程通过Selector对象同时监控多个SelectableChannel来处理多路IONIO应用程序一般按下图所示工作:

 

Figure 1

 

 

Figure 1 所示,Client一直在循环地进行select操作,每次select()返回以后,通过selectedKeys()可以得到需要处理的SelectableChannel并对其一一处理。 这样做虽然简单但也有个问题,当有不同类型的SelectableChannel需要做不同的IO处理时,在图中Client的代码就需要判断channel的类型然后再作相应的操作,这往往意味着一连串的if else。更糟糕的是,每增加一种新的channel,不但需要增加相应的处理代码,还需要对这一串if else进行维护。(在本文的这个例子中,我们有ServerSocketChannelSocketChannel这两种channel需要分别被处理。)

 

如果考虑将channel及其需要的IO处理进行封装,抽象出一个统一的接口,就可以解决这一问题。在Listing 1中的NioSession就是这个接口。

 

NioSessionchannel()方法返回其封装的SelectableChannel对象,interestOps()返回用于这个channel注册的interestOpsregistered()是当SelectableChannel被注册后调用的回调函数,通过这个回调函数,NioSession可以得到channel注册后的SelectionKeyprocess()函数则是NioSession接口的核心,这个方法抽象了封装的SelectableChannel所需的IO处理逻辑。

Listing 1:

复制代码
public   interface  NioSession {

    
public  SelectableChannel channel();

    
public   int  interestOps();


    
public   void  registered(SelectionKey key);

    
public   void  process();   



}
复制代码

NioSession一起工作的是NioWorker这个类(Listing 2),它是NioSession的调用者,封装了一个Selector对象和Figure 1中循环select操作的逻辑。理解这个类可以帮助我们了解该如何使用NioSession这个接口。NioWorker实现了Runnable接口,循环select操作的逻辑就在run()方法中。在NioWorker – NioSession这个框架中,NioSessionchannel注册的时候会被作为attachment送入register函数,这样,在每次select()操作的循环中,对于selectedKeys()中的每一个SelectionKey,我们都可以通过attachment拿到其相对应的NioSession然后调用其process()方法。 每次select()循环还有一个任务,就是将通过add()方法加入到这个NioWorkerNioSession注册到Selector上。在Listing 2的代码中可以看出,NioSession中的channel()被取出并注册在Selector上,注册所需的interestOpsNioSession中取出,NioSession本身则作为attachment送入register()函数。注册成功后,NioSessionregistered()回调函数会被调用。 NioWorkeradd()方法的作用是将一个NioSession加入到该NioWorker中,并wakeup当前的select操作,这样在下一次的select()调用之前,这个NioSession会被注册。stop()方法则是让一个正在run()NioWorker停止。closeAllChannels()会关闭当前注册的所有channel,这个方法可在NioWorker不再使用时用来释放IO资源。 Listing 2:

 

复制代码
public   class  NioWorker  implements  Runnable {
    
public  NioWorker(Selector sel) {

       _sel 
=  sel;
  _added 
=   new  HashSet(); 

    }

    
public   void  run() {
       
try  {

           
try  {

              
while  (_run) {

                  _sel.select();

                  Set selected 
=  _sel.selectedKeys();

                  
for  (Iterator itr  =  selected.iterator(); itr.hasNext();) {

                     SelectionKey key 
=  (SelectionKey) itr.next();

                   NioSession s 
=  (NioSession) key.attachment();

                     s.process();

                     itr.remove();

                  }

                  
synchronized  (_added) {

                     
for  (Iterator itr  =  _added.iterator(); itr.hasNext();) {

                         NioSession s 
=  (NioSession) itr.next();

                         SelectionKey key 
=  s.channel().register(_sel, s.interestOps(), s);

                         s.registered(key);

                         itr.remove();

                     }

                  }

              }


           } 
finally  {



              _sel.close();



           }



       } 
catch  (IOException ex) {



           
throw   new  Error(ex);



       }



    }

    
public   void  add(NioSession s) {

       
synchronized  (_added) {



           _added.add(s);



       }



       _sel.wakeup();



    }



    



    
public   synchronized   void  stop() {



       _run 
=   false ;



       _sel.wakeup();



    }


    
public   void  closeAllChannels() {



       
for  (Iterator itr  =  _sel.keys().iterator(); itr.hasNext();) {



           SelectionKey key 
=  (SelectionKey) itr.next();



           
try  {         



              key.channel().close();



           } 
catch  (IOException ex) {}



       }



    }



    
protected  Selector _sel  =   null ;



    
protected  Collection _added  =   null ;



    
protected   volatile   boolean  _run  =   true ;



}
复制代码

 

Echo Server这个例子中,我们需要一个ServerSocketChannel来接受新的TCP连接,对于每个TCP连接,我们还需要一个SocketChannel来处理这个TCP连接上的IO操作。把这两种channel和上面的NioWorker – NioSession结构整合在一起,可以得到NioServerSessionNioEchoSession这两个类,它们分别封装了ServerSocketChannelSocketChannel及其对应的IO操作。下面这个UML类图描述了这4个类的关系:

 

Figure 2

 

可以看到NioWorkerNioSession对新加入的两个类没有任何依赖性,NioServerSessionNioEchoSession通过实现NioSession这个接口为系统加入了新的功能。这样的一个体系架构符合了Open-Close原则,新的功能可以通过实现NioSession被加入而无需对原有的模块进行修改,这体现了面向对象设计的强大威力。 NioServerSession的实现(Listing 3)相对比较简单,其封装了一个ServerSocketChannel以及从这个channel上接受新的TCP连接的逻辑。NioServerSession还需要一个NioWorker的引用,这样每接受一个新的TCP连接,NioServerSession就为其创建一个NioEchoSession的对象,并将这个对象加入到NioWorker中。 Listing 3:

 

 

复制代码
public   class  NioServerSession  implements  NioSession {
    
public  NioServerSession(ServerSocketChannel channel, NioWorker worker) {

       _channel 
=  channel;

       _worker 
=  worker;


    }

    
public   void  registered(SelectionKey key) {}

    
public   void  process() {

       
try  {

           SocketChannel c 
=  _channel.accept();



           
if  (c  !=   null ) {

              c.configureBlocking(
false );

              NioEchoSession s 
=   new  NioEchoSession(c);

              _worker.add(s);
           }

       } 
catch  (IOException ex) {

           
throw   new  Error(ex);
       }
    }

  
public  SelectableChannel channel() {
       
return  _channel;

    }

    
public   int  interestOps(){

       
return  SelectionKey.OP_ACCEPT;

    } 

    
protected  ServerSocketChannel _channel;

    
protected  NioWorker _worker;

}
复制代码

NioEchoSession的行为要复杂一些,NioEchoSession会先从TCP连接中读取数据,再将这些数据用同一个连接写回去,并重复这个步骤直到客户端把连接关闭为止。我们可以把“Reading”和“Writing”看作NioEchoSession的两个状态,这样可以用一个有限状态机来描述它的行为,如下图所示:

 

Figure 3

 

接下来的工作就是如何实现这个有限状态机了。在这个例子中,我们使用State模式来实现它。下面这张UML类图描述了NioEchoSession的设计细节。

 

Figure 4

 

NioEchoSession所处的状态由EchoState这个抽象类来表现,其两个子类分别对应了“Reading”和“Writing”这两个状态。NioEchoSession会将process()interestOps()这两个方法delegateEchoState来处理,这样,当NioEchoSession处于不同的状态时,就会有不同的行为。 Listing 4EchoState的实现。EchoState定义了process()interestOps()这两个抽象的方法来让子类实现。NioEchoSession中的process()方法会被delegate到其当前EchoStateprocess()方法,NioEchoSession本身也会作为一个描述context的参数被送入EchoStateprocess()方法中。EchoState定义的interestOps()方法则会在NioEchoSession注册和转变State的时候被用到。

 

EchoState还定义了两个静态的方法来返回预先创建好的ReadStateWriteState,这样做的好处是可以避免在NioEchoSession转换state的时候创建一些不必要的对象从而影响性能。然而,这样做要求state类必须是无状态的,状态需要保存在context类,也就是NioEchoSession中。

 

Listing 4:

 

复制代码
public   abstract   class  EchoState {

    
public   abstract   void  process(NioEchoSession s)  throws  IOException;

    
public   abstract   int  interestOps();

    
public   static  EchoState readState() {

       
return  _read;

    }

    
public   static  EchoState writeState() {
       
return  _write;
    }

    
protected   static  EchoState _read  =   new  ReadState();
    
protected   static  EchoState _write  =   new  WriteState();
}
复制代码

 

Listing 5NioEchoSession的实现。NioEchoSession包含有一个SocketChannel,这个channel注册后得到的SelectionKey,一个用于存放数据的ByteBuffer和一个记录当前stateEchoState对象。在初始化时,EchoState被初始化为一个ReadStateNioEchoSessionprocess()方法和interestOps()方法都delegate到当前的EchoState中。其setState()方法用于切换当前state,在切换state后,NioEchoSession会通过SelectionKey更新注册的interestOpsclose()方法用于关闭这个NioEchoSession对象。

 

Listing 5:

 

 

复制代码
public   class  NioEchoSession  implements  NioSession {


    
public  NioEchoSession(SocketChannel c) {



       _channel 
=  c;



       _buf 
=  ByteBuffer.allocate( 128 );



       _state 
=  EchoState.readState();



    }

    
public   void  registered(SelectionKey key) {



       _key 
=  key;



    }

    
public   void  process() {

       
try  {

           _state.process(
this );

       } 
catch  (IOException ex) {

           close();

           
throw   new  Error(ex);
       }
    }

    
public  SelectableChannel channel() {

       
return  _channel;

    }

    
public   int  interestOps() {

       
return  _state.interestOps();

    }


    
public   void  setState(EchoState state) {
     _state 
=  state;

       _key.interestOps(interestOps());
    }

    
public   void  close() {

       
try  {

      _channel.close();

       } 
catch  (IOException ex) {

           
throw   new  Error(ex);

       }

    }

    
protected  SocketChannel _channel  =   null ;

    
protected  SelectionKey _key;

    
protected  ByteBuffer _buf  =   null ;

    
protected  EchoState _state  =   null ;

}
复制代码

 

Listing 6Listing 7分别是ReadStateWriteState的实现。ReadStateprocess()中会先从NioEchoSessionchannel中读取数据,如果未能读到数据,NioEchoSession会继续留在ReadState;如果读取出错,NioEchoSession会被关闭;如果读取成功,NioEchoSession会被切换到WriteStateWriteState则负责将NioEchoSession中已经读取的数据写回到channel中,全部写完后,NioEchoSession会被切换回ReadState

 

 

 

 

 

 

Listing 6:

 

 

 

public class ReadState extends EchoState {

 

 

   

 

 

    public void process(NioEchoSession s)

 

 

       throws IOException

 

 

    {

 

 

       SocketChannel channel = s._channel;

 

 

       ByteBuffer buf = s._buf;

 

 

       int count = channel.read(buf);

 

 

 

 

       if (count == 0) {

 

 

           return;

 

 

       }

 

 

 

 

       if (count == -1) {

 

 

           s.close();

 

 

           return;

 

 

       }

 

 

 

 

       buf.flip();

 

 

       s.setState(EchoState.writeState());

 

 

    }

 

 

   

 

 

    public int interestOps() {

 

 

       return SelectionKey.OP_READ;

 

 

    }

 

 

}

 

 

 

 

Listing 7:

 

 

 

public class WriteState extends EchoState {

 

 

   

 

 

    public void process(NioEchoSession s)

 

 

       throws IOException

 

 

    {

 

 

       SocketChannel channel = s._channel;

 

 

       ByteBuffer buf = s._buf;

 

 

       channel.write(buf);

 

 

       if (buf.remaining() == 0) {

 

 

           buf.clear();

 

 

           s.setState(EchoState.readState());

 

 

       }

 

 

    }

 

 

   

 

 

    public int interestOps() {

 

 

       return SelectionKey.OP_WRITE;

 

 

    }

 

 

}

 

 

 

 

NioEchoServer(Listing 8)被用来启动和关闭一个TCP Echo Server,这个类实现了Runnable接口,调用其run()方法就启动了Echo Server。其shutdown()方法被用来关闭这个Echo Server,注意shutdown()run()finally block中的同步代码确保了只有当Echo Server被关闭后,shutdown()方法才会返回。

 

 

 

 

 

 

Listing 8:

 

 

 

public class NioEchoServer implements Runnable {

 

 

   

 

 

    public void run() {

 

 

       try {

 

 

           ServerSocketChannel serv = ServerSocketChannel.open();

 

 

           try {

 

 

              serv.socket().bind(new InetSocketAddress(7));

 

 

              serv.configureBlocking(false);

 

 

              _worker = new NioWorker(Selector.open());

 

 

              NioServerSession s = new NioServerSession(serv, _worker);

 

 

              _worker.add(s);

 

 

              _worker.run();

 

 

           } finally {

 

 

              _worker.closeAllChannels();

 

 

              synchronized (this) {

 

 

                  notify();

 

 

              }

 

 

           }

 

 

       } catch (IOException ex) {

 

 

           throw new Error(ex);

 

 

       }

 

 

    }

 

 

   

 

 

    public synchronized void shutdown() {

 

 

       _worker.stop();

 

 

       try {

 

 

           wait();

 

 

       } catch (InterruptedException ex) {

 

 

           throw new Error(ex);

 

 

       }

 

 

    }

 

 

   

 

 

    protected NioWorker _worker = null;

 

 

}

 

 

 

 

最后,通过一个简单的main()函数(Listing 9),我们就可以运行这个Echo Server了。

 

 

 

 

 

 

Listing 9:

 

 

 

    public static void main(String [] args) {

 

 

       new NioEchoServer().run();

 

 

    }

 

 

 

 

我们可以通过telnet程序来检验这个程序的运行状况:

 

 

 

1. 打开一个命令行,输入 telnet localhost 7 来运行一个telnet程序并连接到Echo Server上。

 

 

 

2. telnet程序中输入字符,可以看到输入的字符被显示在屏幕上。(这是因为Echo Server将收到的字符写回到客户端)

 

 

 

3. 多打开几个telnet程序进行测试,可以看到Echo Server能通过NIO API用一个Thread服务多个客户端。


本文转自http://www.cnblogs.com/daidu/archive/2009/11/06/1597264.html

转载于:https://my.oschina.net/ccdvote/blog/105563

java进行客户端的applet (小程序)开发的技术已广为使用,而用java进行服务器端的servlet(服务器小程序)开发则尚需揭开其神秘的面纱,本书正是基于这样的目的编写的。全书从java服务器的体系结构、开发工具和管理工具、编程技术、安全机制等四个方面全面介绍java服务器的升友技术。通过阅读本书,读者不仅能够知道用java服务器体系结构开发servlet与用传统cgi编写程序的好处,而且还能够掌握编写java servlet的各种技术和技巧。    本书适用于所有对java和web感兴趣的读者使用和参考。 第1部分 java服务器基础[/font] [/font] 第1章 java计算的体系结构[/font] [/font] 1.1 传统的计算模式[/font] 1.1.1 集中式计算模式[/font] 1.1.2 胖客户机/服务器计算模式[/font] 1.1.3 瘦客户机/服务器计算模式[/font] 1.1.4 java desktop与x终端[/font] 1.2 java计算[/font] 1.3 小结[/font] [/font] 第2章 java服务器与servlet[/font] [/font] 2.1 java server的结构[/font] 2.1.1 服务[/font] 2.1.2 服务器[/font] 2.1.3 服务构架[/font] 2.1.4 服务器处理构架[/font] 2.1.5 http服务[/font] .2.1.6 核心servlet[/font] 2 1.7 acl[/font] 2.2 servlet及其功能[/font] 2.2.1 什么是servlet[/font] 2.2.2 servlet与applet的区别[/font] 2.2.3 servlet的应用[/font] 2.3 servlet与cgi的比较[/font] 2.3.1 cgi[/font] 2.3.2 servlet的优点[/font] 2.4 运行servlet[/font] 2.5 小结[/font] [/font] 第2部分 java服务器的开发工具和管理工具[/font] [/font] 第3章 使用java server与servlet开发工具[/font] [/font] 3.1 java web server的安装与启动[/font] 3.1.1 在windows 95或windows nt上安装[/font] 3.1.2 在unix上安装[/font] 3.1.3 java server环境变量的设置[/font] 3.1.4 启动java server[/font] 3.1.5 在端口80上启动java server[/font] 3.1.6 结束java serve的运行[/font] 3.1.7 删除java server[/font] 3.1.8 显示缺省主页[/font] 3.1.9 使用随机文档[/font] 3.2 servlet的加载和调用[/font] 3.2.1 加载servlet[/font] 3.2.2 servlet的标识[/font] 3.2.3 调用servlet[/font] 3.3 jsdk的安装与使用[/font] 3.3.1 servletrunner[/font] 3.3.2 在netscape服务器上安装jsdk[/font] 3.3.3 在apache服务器上安装[/font] 3.4 小结[/font] [/font] 第4章 java web server的管理[/font] [/font] 4.1 进入管理工具[/font] 4.2 设置[/font] 4.2.1 设置web service[/font] 4.2.2 设置proxy service[/font] 4.2.3 设置管理服务[/font] 4.3 监视[/font] 4.3.1 记录输出[/font] 4.3.2 记录统计[/font] 4.3.3 资源的使用[/font] 4.4 安全控制[/font] 4.4.1 用户管理[/font] 4.4.2 用户组[/font] 4.4.3 访问控制表[/font] 4.4.4 资源保护[/font] 4.5 servlet管理[/font] 4.5.1 增加servlet[/font] 4.5.2 设置servlet属性[/font] 4.5.3 修改servlet属性[/font] 4.5.4 删除servlet[/font] 4.6 小结[/font] [/font] 第3部分 java服务器编程[/font] [/font] 第5章 servlet包介绍[/font] [/font] 5.1 javax.servlet包[/font] 5.1.1 接口servlet[/font] 5.1.2 接口servletconfig[/font] 5.1.3 接口servletcontext[/font] 5.1.4 接口servletrequest[/font] 5.1.5 接口servletresponse[/font] 5.1.6 类genericservlet[/font] 5.1.7 类servletinputstream[/font] 5.1.8 类servletoutputstream[/font] 5.2 javax.servlet.http包[/font] 5.2.1 接口httpservletrequest[/font] 5.2.2 接口httpservletresponse[/font] 5.2.3 类httpservlet[/font] 5.2.4 类httputils[/font] 5.3 sun.servlet包介绍[/font] 5.3.1 接口servletconnection[/font] 5.3.2 类servletloader[/font] 5.4 小结[/font] [/font] 第6章 sun.servlet.http包介绍[/font] [/font] 6.1 类cookie[/font] 6.2 类httpdate[/font] 6.3 类httpinputstream[/font] 6.4 类httpoutputstream[/font] 6.5 类httprequest[/font] 6.6 类httpresponse[/font] 6.7 类httpserver[/font] 6.8 类httpserverhandler[/font] 6.9 类httpservletconfig[/font] 6.10 类messagebytes[/font] 6.11 类messagestring[/font] 6.12 类mimeheaderfield[/font] 6.13 类mimeheaders[/font] 6.14 小结[/font] [/font] 第7章 genericservlet编程[/font] [/font] 7.1 servlet编程的一个小例子[/font] 7.1.1 genericservlet在jsdk中的地位[/font] 7.1.2 从hello world学习genericservlet编程[/font] 7.1.3 servlet的两个生命周期函数[/font] 7.2 servlet中的对象[/font] 7.2.1 servlet中的请求和应答对象[/font] 7.2.2 selvlet中的servletconfig对象[/font] 7.3 servlet输出html文件[/font] 7.4 小结[/font] [/font] 第8章 httpservlet编程介绍[/font] [/font] 8.1 http简介[/font] 8.1.1 http协议基本概念及其特点[/font] 8.1.2 http协议的请求和应答[/font] 8.1.3 http协议的信息处理方法[/font] 8.2 httpservlet编程入门[/font] 8.2.1 一个简单例子[/font] 8.2.2 httpservlet的常用对象介绍[/font] 8.3 cookie编程介绍[/font] 8.3.1 cookie简介[/font] 8.3.2 一个cookie例子程序[/font] 8.4 小结[/font] [/font] 第9章 servlet高级编程[/font] [/font] 9.1 servlet与form[/font] 9.1.1 form简介[/font] 9.1.2 一个完整的form[/font] 9.2 编写一个shtml文本[/font] 9.3 异常处理[/font] 9.4 servlet与applet的通信[/font] 9.5 小结[/font] [/font] 第10章 聊天室[/font] [/font] 10.1 “聊天室”客户程序[/font] 10.2 “聊天室”服务器程序[/font] 10.3 “聊天”[/font] 10.4 小结[/font] [/font] 第11章 rmi与servlet[/font] [/font] 11.1 rmi概述[/font] 11.2 如何编写rmi[/font] 11.2.1 定义一个远程接口[/font] 11.2.2 实现远程接口[/font] 11.2.3 写一个applet[/font] 11.2.4 写html文本[/font] 11.2.5 编译和执行[/font] 11.3 rmi与servlet[/font] 11.3.1 定义远程接口[/font] 11.3.2 实现远程接口[/font] 11.3.3 调用rmi的servlet[/font] 11.3.4 调用servlet的applet[/font] 11.3.5 三个超文本文件[/font] 11.3.6 编译和执行[/font] 11.4 小结[/font] [/font] 第12章 servlet与jdbc[/font] [/font] 12.1 jdbc简介[/font] 12.1.1 实现jdbc[/font] 12.1.2 jdbc类细节[/font] 12.2 jdbc与servlet结合[/font] 12.2.1 基本过程[/font] 12.2.2 servlet的同步[/font] 12.2.3 应用举例[/font] 12.3 小结[/font] [/font] 第13章 开发服务[/font] [/font] 13.1 服务[/font] 13.2 实现服务的核心[/font] 13.3 安装服务[/font] 13.3.1 创建属性文件[/font] 13.3.2 创建jamfile[/font] 13.4 创建日志[/font] 13.4.1 日志文件类型[/font] 13.4.2 在自己的服务中加入日志文件[/font] 13.5 服务的参数管理[/font] 13.5.1 系统参数[/font] 13.5.2 增加新的参数[/font] 13.5.3 创建存放参数的域[/font] 13.5.4 实现访问方法[/font] 13.5.5 构造器[/font] 13.5.6 update方法[/font] 13.6 使用realm来创建安全服务[/font] 13.6.1 查找realm[/font] 13.6.2 设置realm[/font] 13.6.3 收集用户数据的对象[/font] 13.6.4 给服务增加身份验证代码[/font] 13.7 小结[/font] [/font] 第14章 pagecompile[/font] [/font] 14.1 生成动态主页[/font] 14.1.1 创建第一个主页[/font] 14.1.2 从html文档到java源文件[/font] 14.2 语法[/font] 14.2.1 声明变量[/font] 14.2.2 条件语句[/font] 14.2.3 循环语句[/font] 14.2.4 注释[/font] 14.2.5 使用backquotes[/font] 14.2.6 使用servlet标记[/font] 14.2.7 输出java表达式[/font] 14.3 使用java类[/font] 14.3.1 重用java类[/font] 14.3.2 创建自己的类[/font] 14.3.3 访问request和response对象[/font] 14.4 servlet初始化参数[/font] 14.5 pagecompile中使用的标记[/font] 14.6 小结[/font] [/font] 第4部分 java服务器安全机制[/font] [/font] 第15章 java web server的安全机制[/font] [/font] 15.1 安全综述[/font] 15.1.1 常见的攻击形式[/font] 15.1.2 java web server的安全机制[/font] 15.1.3 其他特点[/font] 15.1.4 unix上的独有特点[/font] 15.2 web realm机制[/font] 15.2.1 realm模型[/font] 15.2.2 用户和授权[/font] 15.2.3 用户组[/font] 15.3 访问控制表(acl)[/font] 15 3.1 访问控制表结构[/font] 15.3.2 计算权限的规则[/font] 15.3.3 例子[/font] 15.3.4 用法举例[/font] 15.4 ssl机制[/font] 15.4.1 什么是ssl[/font] 15.4.2 建立安全的web服务[/font] 15.4.3 使用认证许可[/font] 15.5 沙盒[/font] 15.6 小结[/font]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值