Java进阶之NIO&AIO

一、简单了解各种IO

之前在网络编程的学习中,服务器调用accept方法时会一直等待客户端访问,如果没有客户端访问,服务器程序会一直停留在这个阶段,这种数据传输方式就被称为BIO。

除了BIO之外,还有两种IO,分别是NIO和AIO:

  • BIO:同步阻塞IO
  • NIO:同步非阻塞IO
  • AIO:异步非阻塞IO

举例来说,去银行排队叫号和写作业三种IO不同的执行过程:
在这里插入图片描述

二、NIO

NIO有三个重要的组成部分:BufferChannelSelector

2.1 Buffer

Buffer是缓冲区,本质就是由数组组成的,在NIO中,数据都是要在缓冲区进行操作的。

常见的缓冲区:

  • ByteBuffer(常用)
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

2.1.1 ByteBuffer的创建

ByteBuffer创建三种方式:

  • static ByteBuffer allocate(int capacity):创建一个字节缓冲区并返回,参数是缓冲区的长度(间接缓冲区)
  • static ByteBuffer allocateDirect(int capacity):创建一个字节缓冲区并返回,参数是缓冲区的长度(直接缓冲区)
  • static ByteBuffer wrap(byte[] array):根据字节数组创建字节缓冲区并返回(间接缓冲区)

缓冲区:

  • 间接缓冲区:在Java的内存中创建的缓冲区
  • 直接缓冲区:在系统内存中创建的缓冲区

间接缓冲区的创建和销毁效率比直接缓冲区要高,但是工作效率比直接缓冲区要低。

public class Demo01Buffer {
    public static void main(String[] args) {
        // static ByteBuffer allocate(int capacity):创建一个字节缓冲区并返回,参数是缓冲区的长度(间接缓冲区)
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //将ByteBuffer转成数组,然后借助工具类Arrays.toString输出
        System.out.println(Arrays.toString(buffer.array()));

        //static ByteBuffer allocateDirect(int capacity):创建一个字节缓冲区并返回,参数是缓冲区的长度(直接缓冲区)
        ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);
        System.out.println(buffer2);//java.nio.DirectByteBuffer[pos=0 lim=10 cap=10]

        //static ByteBuffer wrap(byte[] array):根据字节数组创建字节缓冲区并返回(间接缓冲区)
        ByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes());
        System.out.println(Arrays.toString(buffer3.array()));//[104, 101, 108, 108, 111]
    }
}

2.1.2 ByteBuffer的put方法

在ByteBuffer中有一些方法叫做put,可以向缓冲区中添加元素:

  • ByteBuffer put(byte b):向当前位置添加一个字节
  • ByteBuffer put(byte[] src):向当前位置添加一个字节数组
  • ByteBuffer put(byte[] src, int offset, int length):添加字节数组的一部分。参数offset是数组起始索引,参数length是元素个数
public class Demo02BufferPut {
    public static void main(String[] args) {
        //获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //ByteBuffer put(byte b):向当前位置添加一个字节。
        buffer.put((byte) 100);
        buffer.put((byte) 101);
        buffer.put((byte) 102);
        //ByteBuffer put(byte[] src):向当前位置添加一个字节数组。
        byte[] bArr = {90, 91, 92, 93, 94};
        //buffer.put(bArr);

        //ByteBuffer put(byte[] src, int offset, int length):添加字节数组的一部分。参数offset是数组起始索引,参数length是元素个数
        buffer.put(bArr, 1, 3);

        //输出
        System.out.println(Arrays.toString(buffer.array()));

    }
}

2.1.3 ByteBuffer的capacity方法

在ByteBuffer中有一个方法叫做capacity,可以获取到缓冲区的容量:

  • int capacity():返回缓冲区的容量
public class Demo03Capacity {
    public static void main(String[] args) {
        //获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //输出容量
        System.out.println("容量:" + buffer.capacity());
    }
}

2.1.4 ByteBuffer的limit方法

在ByteBuffer中,有一个方法叫做limit,可以对缓冲区进行限制(比如限制缓冲区中只能使用前5个元素)

  • int limit():获取缓冲区的限制
  • Buffer limit(int newLimit):设置缓冲区的限制。 参数表示新的限制,比如参数是5,就表示只能使用5个元素
public class Demo04Limit {
    public static void main(String[] args) {
        //获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //输出
        System.out.println("容量:" + buffer.capacity() + ",限制:" + buffer.limit());
        //设置limit
        buffer.limit(2);
        System.out.println("容量:" + buffer.capacity() + ",限制:" + buffer.limit());

        //添加元素
        buffer.put((byte) 100);
        buffer.put((byte) 101);
        //限制了只能使用前两个元素,如果添加第三个,就会报错
        buffer.put((byte) 102);//BufferOverflowException
    }
}

2.1.5 ByteBuffer的position方法

在ByteBuffer中有一个方法叫做Position,可以获取以及设置缓冲区的光标位置

  • int position():获取缓冲区的光标位置
  • Buffer position(int newPosition):设置缓冲区的光标位置,参数表示新设置的位置
public class Demo05Position {
    public static void main(String[] args) {
        //获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //输出缓冲区的信息
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //添加
        buffer.put((byte) 100);
        buffer.put((byte) 101);
        buffer.put((byte) 102);
        //输出缓冲区的信息
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //设置缓冲区的位置
        buffer.position(0);
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        buffer.put((byte) 50);
        buffer.put((byte) 51);
        //输出
        System.out.println(Arrays.toString(buffer.array()));
    }
}

2.1.6 ByteBuffer的mark方法

在ByteBuffer中有一个方法叫做mark,可以设置缓冲区的标记:

  • Buffer mark():设置缓冲区的标记
  • Buffer reset():恢复之前的标记

调用mark方法时position位置是多少,那么调用reset方法后恢复的position就是多少。

public class Deo06Mark {
    public static void main(String[] args) {
        //获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //输出缓冲区的信息
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //添加
        buffer.put((byte) 100);
        buffer.put((byte) 101);
        buffer.put((byte) 102);
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //设置标记
        buffer.mark();
        //添加
        buffer.put((byte) 103);
        buffer.put((byte) 104);
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //调用reset,恢复之间做标记是的位置.
        buffer.reset();
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        buffer.put((byte) 10);
        buffer.put((byte) 11);
        //输出元素
        System.out.println(Arrays.toString(buffer.array()));
    }
}

2.1.7 ByteBuffer中的其他的方法

  • Buffer flip():缩小limit的范围
    a. 将limit设置到position位置。
    b. 将position设置为0
    c. 丢弃标记

  • Buffer clear():还原缓冲区的状态。
    a. 将limit设置到capacity
    b. 将position设置为0
    c. 丢弃标记

public class Demo07OtherMethod {
    public static void main(String[] args) {
        //获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        //输出缓冲区的信息
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //添加
        buffer.put((byte) 100);
        buffer.put((byte) 101);
        buffer.put((byte) 102);
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //缩小limit范围
        buffer.flip();
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
        //还原缓冲区状态
        buffer.clear();
        System.out.println("位置:" + buffer.position() + ", 限制:" + buffer.limit() + ", 容量:" + buffer.capacity());
    }
}

2.2 Channel

Channel表示通道,在NIO中数据的读写都是使用通道完成的,可以将通道看成之前的流,只不过流是单向的,通道是双向的,通道既有读取的方法,也有写的方法。

常见的通道:

  • FileChannel:从文件读取数据的
  • DatagramChannel:读写UDP网络协议数据
  • SocketChannel:读写TCP网络协议数据
  • ServerSocketChannel:可以监听TCP连接

2.2.1 FileChannel复制文件

通过NIO的方式复制文件,如果要对文件读写,需要使用FileChannel

通过文件字节流获取FileChannel:

  • FileChannel getChannel():获取通道

在通道(Channel)中还有用于读写的方法

  • int write(ByteBuffer src):写数据,参数是缓冲区。
  • int read(ByteBuffer dst):读取数据,参数是缓冲区
public class Demo01Channel {
    public static void main(String[] args) throws IOException {
        //创建字节流
        FileInputStream fis = new FileInputStream("d:\\aa.jpg");
        FileOutputStream fos = new FileOutputStream("d:\\bb.jpg");
        //获取通道
        FileChannel inChannel = fis.getChannel();
        FileChannel outChannel = fos.getChannel();
        //先定义缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);//
        //定义变量,表示读取到的字节个数
        int len;
        //开始循环
        while ((len = inChannel.read(buffer)) != -1) {
            //如果条件成立,就表示读取到了数据,那么就进行处理。
            //缩小limit范围(将limit设置到position位置),读取到几个,就让缓冲区中的几个元素是有效的。
            buffer.flip();
            //将读取到的数据写到目的地文件了
            outChannel.write(buffer);
            //重置缓冲区(将position设置为0,将limit设置到capacity,丢弃标记)
            buffer.clear();
        }
        //释放资源
        fos.close();
        fis.close();

    }
}

2.2.2 RandomAccessFile复制文件

上面复制文件的案例直接使用FileChannel结合ByteBuffer实现channel读写,但并不能提高文件的读写效率。

ByteBuffer有个子类:MappedByteBuffer,可以创建一个“直接缓冲区”,并可以将文件直接映射至内存,可以提高大文件的读写效率。

RandomAccessFile类(是一个可以设置读写模式的IO流类)

  • RandomAccessFile(String name, String mode): 第一个参数是字符串的文件路径,第二个参数是模式(举例:"r"表示只读;"rw"表示读写)

RandomAccessFile其他的方法:

  • FileChannel getChannel():获取通道

FileChannel获取MappedByteBuffer方法

  • MappedByteBuffer map(FileChannel.MapMode mode, long position, long size):获取直接缓冲区
    参数mode:表示模式
    参数position:表示起始位置
    参数size:映射的大小

注意:通过RandomAccessFile复制文件的方式文件不能超过2G。

public class Demo02FastCopy {
    public static void main(String[] args) throws IOException {
        //创建RandomAccessFile对象
        //创建的RandomAccessFile,绑定了源文件,模式只读
        RandomAccessFile source = new RandomAccessFile("d:\\aa.rar", "r");
        //创建的RandomAccessFile,绑定了目的地文件,模式读写
        RandomAccessFile target = new RandomAccessFile("d:\\bb.rar", "rw");
        //获取通道
        FileChannel inChannel = source.getChannel();
        FileChannel outChannel = target.getChannel();
        //记录时间
        long start = System.currentTimeMillis();
        //获取源文件大小
        long size = inChannel.size();
        //获取MappedByteBuffer缓冲区
        MappedByteBuffer mbbi = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
        MappedByteBuffer mbbo = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, size);
        //遍历mbbi,将每一个字节都放入到mbbo缓冲区中.
        for (int i = 0; i < size; i++) {
            //获取到mbbi中索引为i的字节
            byte b = mbbi.get();
            //将获取到的放入到mbbo中
            mbbo.put(b);
        }
        //记录时间
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        //释放资源
        target.close();
        source.close();
    }
}

2.2.3 SocketChannel客户端

SocketChannel表示客户端通道,我们可以使用该类中表示TCP协议的客户端。

获取SocketChannel

  • static SocketChannel open():获取SocketChannel

SocketChannel方法:

  • boolean connect(SocketAddress remote):连接服务器,参数是目标服务器的IP地址以及端口号
public class Demo01Client {
    public static void main(String[] args) throws IOException {
        //获取SocketChannel对象
        SocketChannel socketChannel = SocketChannel.open();
        //连接服务器
        socketChannel.connect(new InetSocketAddress("localhost", 8888));
        //给服务器发送数据
        //将要发送的数据封装到缓冲区中
        ByteBuffer buffer = ByteBuffer.wrap("你好".getBytes());
        //将数据发送给服务器
        socketChannel.write(buffer);
        //接收服务器回复过来的数据
        //创建一个长度是1024的缓冲区,用来接收服务器回复过来的数据
        ByteBuffer buffer2 = ByteBuffer.allocate(1024);
        //接收服务器的数据
        socketChannel.read(buffer2);
        //缩小limit限制
        buffer2.flip();
        //将缓冲区的内容转成字符串输出。
        System.out.println(new String(buffer2.array(), 0, buffer2.limit()));
        //释放资源
        socketChannel.close();
    }
}

2.2.4 SocketChannel服务端

ServerSocketChannel表示服务端通道,我们可以使用该类中表示TCP协议的服务端。

获取ServerSocketChannel

  • static ServerSocketChannel open():获取ServerSocketChannel

ServerSocketChannel方法:

  • static ServerSocketChannel open():获取服务器通道
  • ServerSocketChannel bind(SocketAddress local):给服务器绑定端口号
  • SocketChannel accept():监听客户端请求
public class Demo02Server {
    public static void main(String[] args) throws IOException {
        //获取服务器通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //将服务器设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //给服务器绑定端口
        serverSocketChannel.bind(new InetSocketAddress(8888));
        while (true) {
            //监听客户端请求
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel == null) {
                System.out.println("还没有客户端来");
            } else {
                System.out.println("有客户端来连接了");
                //获取客户端发送过来的数据
                //创建ByteBuffer缓冲区,用来保存读取到的数据
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //通过通道接收数据
                socketChannel.read(buffer);
                //缩小limit范围
                buffer.flip();
                //输出读取到的内容
                System.out.println(new String(buffer.array(), 0, buffer.limit()));


                //给客户端回复数据
                //获取缓冲区,里面保存要回复的数据
                ByteBuffer buffer2 = ByteBuffer.wrap("收到".getBytes());
                //使用通道将数据写给客户端
                socketChannel.write(buffer2);
                //释放资源
                socketChannel.close();
            }
        }

    }
}

2.3 Selector选择器

选择器Selector是NIO中的重要技术之一,它与SelectorChannel联合使用实现了非阻塞的多路复用。

2.3.1 多路复用

"多路"是指:服务器同时监听多个"端口"的情况,每个端口都要监听多个客户端的连接。
在这里插入图片描述
如果不使用多路复用,服务端需要开很多线程处理每个端口的请求,如果在高并发环境下会造成系统性能下降。
在这里插入图片描述
使用多路复用,只需要一个线程就可以处理多个通道,降低内存占用率,减少CPU切换时间,在高并发、高频段环境下非常有优势。

Selector选择器可以实现多路复用的效果。我们可以使用一个Selector监听三个服务器的状态,哪个服务器有客户端来请求了,那么我们就可以让哪个服务器去处理客户端的请求。

获取Selector选择器:

  • static Selector open():获取一个选择器

将通道注册到选择器:

  • channel.configureBlocking(false):将通道设置为非阻塞。
  • channel.register(selector,SelectionKey.OP_ACCEPT):参数selector表示选择器;SelectionKey.OP_ACCEPT表示监听服务器接受就绪事件

Selector选择器中的方法:

  • Set<SelectionKey> keys():获取已经注册到选择器的通道(编号)并放入到Set集合中返回。 SelectionKey可以理解为通道的编号
  • Set<SelectionKey> selectedKeys(): 获取已经连接的通道(编号)并放入到Set集合中返回
  • int select():调用select方法后,程序会等着,一直到有客户端来连接
public class Demo01Selector {
    public static void main(String[] args) throws IOException {
        //创建三个服务器,并将三个服务器设置为非阻塞
        ServerSocketChannel serverSocketChannelOne = ServerSocketChannel.open();
        serverSocketChannelOne.bind(new InetSocketAddress(7777));
        serverSocketChannelOne.configureBlocking(false);//设置为非阻塞

        ServerSocketChannel serverSocketChannelTwo = ServerSocketChannel.open();
        serverSocketChannelTwo.bind(new InetSocketAddress(8888));
        serverSocketChannelTwo.configureBlocking(false);//设置为非阻塞

        ServerSocketChannel serverSocketChannelThree = ServerSocketChannel.open();
        serverSocketChannelThree.bind(new InetSocketAddress(9999));
        serverSocketChannelThree.configureBlocking(false);//设置为非阻塞

        //获取Selector选择器
        Selector selector = Selector.open();

        //让上面三个服务器通道注册到Selector选择器上
        serverSocketChannelOne.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChannelTwo.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChannelThree.register(selector, SelectionKey.OP_ACCEPT);

        //死循环,让程序一直执行(选择器一直监听服务器通道的状态)
        while (true) {
            //调用选择器的select方法,等着客户端来连接服务器
            selector.select();
            //如果程序向下执行,表示有客户端来连接了。就获取已经连接的服务器通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //获取迭代器
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            //遍历
            while (iterator.hasNext()) {
                //获取遍历到的元素
                SelectionKey selectionKey = iterator.next();
                //通过selectionKey获取到通道
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                //让服务器监听客户端请求
                SocketChannel socketChannel = serverSocketChannel.accept();
                //获取缓冲区,用来保存接收到的数据
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //进行读取
                socketChannel.read(buffer);
                //缩小缓冲区的limit范围
                buffer.flip();
                //输出读取到的内容
                System.out.println(new String(buffer.array(), 0, buffer.limit()));
                //客户端通道关闭
                socketChannel.close();
                //如果某个服务器处理完了客户端请求,那么就从集合中删除。
                iterator.remove();//删除遍历的元素
            }
        }
    }
}
public class Demo02Client {
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                //获取SocketChannel
                SocketChannel socketChannel = SocketChannel.open();
                //连接服务器
                socketChannel.connect(new InetSocketAddress("localhost", 7777));
                //准备缓冲区,保存要发送的数据
                ByteBuffer buffer = ByteBuffer.wrap("我要连接7777".getBytes());
                //将数据发给服务器
                socketChannel.write(buffer);
                //释放资源
                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                //获取SocketChannel
                SocketChannel socketChannel = SocketChannel.open();
                //连接服务器
                socketChannel.connect(new InetSocketAddress("localhost", 8888));
                //准备缓冲区,保存要发送的数据
                ByteBuffer buffer = ByteBuffer.wrap("我要连接8888".getBytes());
                //将数据发给服务器
                socketChannel.write(buffer);
                //释放资源
                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                //获取SocketChannel
                SocketChannel socketChannel = SocketChannel.open();
                //连接服务器
                socketChannel.connect(new InetSocketAddress("localhost", 9999));
                //准备缓冲区,保存要发送的数据
                ByteBuffer buffer = ByteBuffer.wrap("我要连接9999".getBytes());
                //将数据发给服务器
                socketChannel.write(buffer);
                //释放资源
                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

三、AIO

AIO有关的通道

  • AsynchronousSocketChannel:TCP中的客户端异步通道
  • AsynchronousServerSocketChannel:TCP中的服务器异步通道
  • AsynchronousFileChannel:文件操作的异步通道
  • AsynchronousDatagramChannel:UDP通信异步通道
public class Demo01Server {
    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
        //获取一个异步服务器通道
        AsynchronousServerSocketChannel asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
        //绑定端口号
        asynchronousServerSocketChannel.bind(new InetSocketAddress(8888));
        //监听客户端的请求
        Future<AsynchronousSocketChannel> accept = asynchronousServerSocketChannel.accept();
        //调用get方法,获取服务器监听到的客户端通道
        AsynchronousSocketChannel asynchronousSocketChannel = accept.get();
        //创建ByteBuffer缓冲区,用来接收读取到的数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //调用read方法进行读取
        Future<Integer> readFuture = asynchronousSocketChannel.read(buffer);
        //判断如果read方法没有读取结束,那么就去干一些其他事情
        if (!readFuture.isDone()) {
            Thread.sleep(1000);
        }
        //缩小缓冲区limit限制
        buffer.flip();
        //输出读取到的结果
        System.out.println(new String(buffer.array(), 0, buffer.limit()));
        //释放资源
        asynchronousSocketChannel.close();
        asynchronousServerSocketChannel.close();
    }
}
/*
    AsynchronousSocketChannel:TCP中的客户端异步通道
 */
public class Demo02Client {
    public static void main(String[] args) throws IOException, InterruptedException {
        //获取客户端异步通道
        AsynchronousSocketChannel asynchronousSocketChannel = AsynchronousSocketChannel.open();
        //连接服务器
        Future<Void> future = asynchronousSocketChannel.connect(new InetSocketAddress("localhost", 8888));
        //判断如果连接没有建立成功,就做一些其他事情
        if(!future.isDone()) {
            Thread.sleep(1000);
        }
        //让客户端给服务器发送数据
        ByteBuffer buffer = ByteBuffer.wrap("你好".getBytes());
        //调用方法,发送数据
        asynchronousSocketChannel.write(buffer);
        //释放资源
        asynchronousSocketChannel.close();
    }
}

四、同步异步&阻塞非阻塞

同步和异步(线程通信的机制)

  • 同步:线程在完成某个功能时,得到结果之后才能做后面的事情【立即得到结果】
  • 异步:线程在完成功能的时候,不用得到结果也可以做后面的事情【不会立即得到结果】

阻塞和非阻塞(线程的状态)

  • 阻塞:线程在执行任务时,会挂起。
  • 非阻塞:线程在执行任务时,不会挂起,可以继续执行其他任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值