BIO、NIO与AIO


一、I/O模型

I/O模型其实就是数据的输入输出模型。通过不同的通道,通信模式或架构进行数据的传输和接收,从而改变通信程序的性能。

UNIX中I/O模型有5种:同步阻塞I/O,同步非阻塞I/O,I/O多路复用,信号驱动I/O和异步I/O。

Java中有3种常用的I/O模型:
1.BIO(Blocking I/O)同步阻塞I/O模型
2.NIO(Non-blocking/New I/O)同步非阻塞I/O模型,也可以看作是I/O多路复用模型
3.AIO(Asynchronous I/O 异步非阻塞I/O模型

二、BIO

1.BIO简介

BIO是同步阻塞I/O模型,服务器实现模式为一个连接一个线程,即客户端有连接请求服务端时服务器就需要启用一个新的线程进行处理,如果这个连接不做任何事情,那就会造成不必要的线程开销。当有多个客户端进行连接时,可以使用线程池改善这种机制。
在这里插入图片描述

2.BIO工作机制

Socket基本工作流程
accept是阻塞的,只有新的连接进来了,accept才会返回,主线程才会继续工作。
read是阻塞的,只有请求消息进来了,read才能返回,子线程才能继续工作。
write是阻塞的,只有客户端把数据读取了,write才会返回,子线程才能继续工作。

在这里插入图片描述
BIO工作机制
BIO通过socket管道进行数据的read/write操作,read和write只能阻塞进行,线程在进行读写IO操作时不能做其他事情。比如调用socket.read时,如果服务器一直没有响应,那线程就会一直等待。

在这里插入图片描述

3.一对一通信代码示例

服务端:只允许一个客户端连接

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;

public class Server {

    public static void main(String[] args) {
        try {
            //1、ServerSock注册9000端口
            ServerSocket serverSocket = new ServerSocket(9000);
            System.out.println("服务端启动了");
            //2、监听客户端Socket的连接请求
            while(true) {
                System.out.println("当前线程信息,线程id=" + Thread.currentThread().getId() + ",线程名称=" + Thread.currentThread().getName());
                System.out.println("等待客户端连接...");
                Socket socket = serverSocket.accept();
                System.out.println("连接到一个客户端");
                //3、通过socket获取输入流
                InputStream in = socket.getInputStream();
                //4、循环读取客户端的发送的数据
                byte[] bytes = new byte[1024];
                int read = in.read(bytes);
                while (read != -1) {
                    System.out.println("客户端说:");
                    System.out.println(new String(bytes, 0, read));
                    read = in.read(bytes);
                }
            }
        } catch (IOException e) {
            if (e instanceof SocketException) {
                System.out.println("一个客户端断开连接...");
            }
        }

    }
}

客户端

import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class Client {

    public static void main(String[] args) {
        try {
            //1、Socket与服务端进行连接
            Socket socket = new Socket("127.0.0.1", 9000);
            System.out.println("开启一个客户端");
            //2、定义一个文本输出流
            PrintWriter pr = new PrintWriter(socket.getOutputStream());
            while(true) {
                //3、获取键盘输入
                System.out.println("请说:");
                Scanner sc = new Scanner(System.in);
                String msg = sc.nextLine();
                //4、输出文本字符
                pr.println(msg);
                pr.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.一对多通信代码示例

服务端:采用线程池实现接收多个客户端

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server2 {

    public static void main(String[] args) {
        try {
            //1、ServerSocket注册一个9000端口
            ServerSocket ss = new ServerSocket(9000);
            System.out.println("服务端已启动");
            //2、定义一个线程池
            ExecutorService threadPool = Executors.newCachedThreadPool();
            while(true) {
                //3、监听客户端请求
                System.out.println("等待客户端连接...");
                Socket socket = ss.accept();
                System.out.println("连接到一个客户端");
                threadPool.execute(new ServerRunnable(socket));
            }
        } catch (IOException e) {
            if (e instanceof SocketException) {
                System.out.println("一个客户端断开连接");
            } else {
                e.printStackTrace();
            }
        }
    }
}
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class ServerRunnable implements Runnable {

    private Socket socket;

    public ServerRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //1、通过socket获取输入流
            System.out.println("当前线程信息,线程id=" + Thread.currentThread().getId() + ",线程名称=" + Thread.currentThread().getName());
            InputStream in = socket.getInputStream();
            //2、循环读取客户端的发送的数据
            byte[] bytes = new byte[1024];
            int read = in.read(bytes);
            while (read != -1) {
                System.out.print("客户端说:");
                System.out.print(new String(bytes, 0, read));
                read = in.read(bytes);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三、NIO

1.NIO简介

NIO是同步非阻塞I/O模型,支持面向缓冲区的,基于通道的I/O操作。与BIO相比,NIO可以将Socket设置成非阻塞模式。

NIO服务器实现模式使一个线程从通道中发送或接收数据,如果通道中没有可用数据时,线程就不会去获取,线程可以去处理其他事情,从而达到非阻塞。线程往通道中写入数据时也是一样,不需要等待完全写入,这个线程可以同时去处理别的事情。

在这里插入图片描述

  • 每一个Channel都会对应一个Buffer
  • 一个线程对应一个Selector,一个Selector对应多个Channel
  • 程序切换到哪个Channel由事件决定,Selector会根据事件在不同Channel上切换
  • Buffer是一块内存,底层是个数组
  • 数据的读取和写入是通过Buffer来完成的。
  • Channel负责传输,Buffer负责数据的读写

2.NIO与BIO的比较

NIOBIO
面向缓冲区(Buffer)面向流(Stream)
非阻塞IO(Non Blocking)阻塞IO(Blocking IO)
选择器(Selectors)
  • BIO以流的方式处理数据,NIO以块的方式处理数据,块I/O效率比流I/O效率高
  • BIO基于字节流和字符流操作,而NIO基于通道和缓冲区操作,数据都是通过通道读/写入缓冲区,选择器会监听到连接请求或数据到达等事件,然后再进行处理,一个选择器可以监听多个通道,因此一个线程就能处理多个客户端的请求。

3.NIO三大核心

NIO三大核心部分:Buffer缓冲区,Channel通道,Selector选择器

1.Buffer

Buffer本质是一块可以写入数据,也可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一系列方法来访问该块内存。Buffer是可以双向的,可以读也可以写。

所有缓冲区都是Buffer抽象类的子类。JAVA NIO中的Buffer主要用于与NIO的交互,数据从通道读入缓冲区,从缓冲区写入通道。

Buffer底层是一个数组,可以保存多个相同类型的数据。

常用的Buffer子类有

  1. ByteBuffer
  2. CharBuffer
  3. ShortBuffer
  4. IntBuffer
  5. LongBuffer
  6. FloatBuffer
  7. DoubleBuffer

Buffer的基本属性

  • 容量(capacity):作为一个内存块,Buffer具有一定的固定大小,也称为容量,缓冲区的容量不能为负的并且创建后不能更改。

  • 限制(limit):表示缓冲区中可以操作数据的大小(limit后数据不能进行读写)缓冲区的限制不能为负,并且不能大于其容量。写入模式,限制等于buffer的容量。读取模式,limit等于写入的数据量。

  • 位置(position):下一个要读取或者写入的数据索引。缓冲区的位置不能为负,并且不能大于其限制。

  • 标记(mark)于重置(reset):标记是一个索引,通过Buffer中的mark()方法可以指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。

  • 标记,位置,限制,容量遵循以下不变公式:0 <= mark <= position <= limit <= capacity

import java.nio.ByteBuffer;

/**
 * Buffer三个重要属性
 *  capacity:表示的是这个缓冲区包含元素的个数,容量不可改变也不可是负数
 *  limit:表示的是缓冲区中第一个不可读或写的元素的索引,它不可是负数并且 <= capacity
 *  position:表示的是缓冲区中下一个元素可读或可写的元素的索引,它不可是负数并且 <= limit
 * Buffer只能单向读或者单向写,进行读操作时,需要调用flip()方法,此时position=0,limit=Buffer实际写入的容量值
 */
public class BufferTest {

    public static void main(String[] args) {
        //buffer只能单向读或者单向写,不能同时读写操作,

        //是否使用直接内存
        ByteBuffer bf1 = ByteBuffer.allocate(10);
        System.out.println(bf1.isDirect()); //false
        ByteBuffer bf2 = ByteBuffer.allocateDirect(10);
        System.out.println(bf2.isDirect()); //true

        System.out.println("---------------------------------------------------");

        //初始创建时position=0,limit=容量,capacity=容量
        ByteBuffer bf3 = ByteBuffer.allocate(10);
        System.out.println(bf3.position()); //0
        System.out.println(bf3.limit()); //10
        System.out.println(bf3.capacity()); //10

        System.out.println("---------------------------------------------------");

        //往buffer加数据后position=数据长度,limit=容量,capacity=容量
        bf3.put("asdfgh".getBytes());
        System.out.println(bf3.position()); //6
        System.out.println(bf3.limit()); //10
        System.out.println(bf3.capacity()); //10

        System.out.println("---------------------------------------------------");

        //转换为读模式,position=0,limit=数据长度,capacity=容量,缓冲区的数据不变
        bf3.flip();
        System.out.println(bf3.position()); //0
        System.out.println(bf3.limit()); //6
        System.out.println(bf3.capacity()); //10

        System.out.println("---------------------------------------------------");

        //直接再写入数据,会从第一位开始覆盖,此时limit为6,能进行写的长度最多也是6
        System.out.println(new String(bf3.array(), 0, bf3.limit())); //asdfgh
        bf3.put("1234".getBytes());
        System.out.println(new String(bf3.array(), 0, bf3.limit())); //1234gh
        System.out.println(bf3.position()); //4
        System.out.println(bf3.limit()); //6
        System.out.println(bf3.capacity()); //10

        System.out.println("---------------------------------------------------");

        //缓冲区清空,position=0,limit=容量,capacity=容量, 缓冲区中其实还是有数据
        bf3.clear();
        System.out.println(bf3.position()); //0
        System.out.println(bf3.limit()); //10
        System.out.println(bf3.capacity()); //10
        System.out.println(new String(bf3.array(), 0, bf3.remaining())); //1234gh

        System.out.println("---------------------------------------------------");

        //clear后重新写入数据,会覆盖原来的
        bf3.put("1234567890".getBytes());
        System.out.println(bf3.position()); //10
        System.out.println(bf3.limit()); //10
        System.out.println(bf3.capacity()); //10
        bf3.flip();
        System.out.println(new String(bf3.array(), 0, bf3.remaining())); //1234567890

        System.out.println("---------------------------------------------------");

        ByteBuffer bf4 = ByteBuffer.allocate(10);
        bf4.put("123456".getBytes());
        System.out.println(bf4.position()); //6
        System.out.println(bf4.limit()); //10
        System.out.println(bf4.capacity()); //10

        System.out.println("---------------------------------------------------");

        //mark(),mark=position
        bf4.mark();
        System.out.println(bf4.mark()); //6

        bf4.put("123".getBytes());
        System.out.println(bf4.position()); //9
        System.out.println(bf4.limit()); //10
        System.out.println(bf4.capacity()); //10

        System.out.println("---------------------------------------------------");

        //reset(),position=mark
        bf4.reset();
        System.out.println(bf4.position()); //6
        System.out.println(bf4.limit()); //10
        System.out.println(bf4.capacity()); //10

        System.out.println("---------------------------------------------------");
    }
}

2.Channel

  1. Channel可以认为是进行传输Buffer的,Channel本身并不能直接访问数据,而是将Buffer传递给接收方,让接收方来操作Buffer进行读取数据。
  2. Channel在NIO中是一个接口
    public interface Channel extends Closeable {}
    

常用的Channel实现类

  1. FileChannel:用于读取,写入,映射和操作文件的通道
  2. DatagramChannel:通过UDP读写网络中的数据通道
  3. SocketChannel:通过TCP读写网络中的数据
  4. ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。

3.Selector

  • 选择器是NIO的一个组件,可以检查多个NIO通道,并通过轮询监听哪些通道已经准备好连接,读,写等事件。
  • Selector是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,即保证了IO的非阻塞。
  • 通过Selector监听,保证了只有真正在连接/读写事件发生时,才会进行读写操作,这样大大减少了系统开销,并且不需要为每个连接都创建线程,不需要去维护多个线程。避免了多线程之间上下文的切换带来的系统开销

Selector选择器的监听类型

  1. 可读:SelectionKey.OP_READ
  2. 可写:SelectionKey.OP_WRITE
  3. 连接:SelectionKey.OP_CONNECT
  4. 接收:SelectionKey.OP_ACCEPT

4.代码示例

服务端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;

public class Server {

    public static void main(String[] args) {
        try {
            //创建一个通道
            ServerSocketChannel channel = ServerSocketChannel.open();
            //通道设置为非阻塞模式
            channel.configureBlocking(false);
            //绑定一个端口
            channel.bind(new InetSocketAddress(9000));
            //创建一个选择器
            Selector selector = Selector.open();
            //将通道注册选择器中,并对接收进行监听(这里监听的是服务端通道)
            channel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务端已启动");

            //轮询监听选择器事件
            while (true) {
                //选择器开始监听
                selector.select();
                //返回选择器的结果集
                Set<SelectionKey> keys =selector.selectedKeys();
                //遍历结果集,根据事件进行处理
                Iterator<SelectionKey> ite = keys.iterator();
                while (ite.hasNext()) {
                    SelectionKey key = ite.next();
                    //获取到后从结果集中移除
                    ite.remove();
                    //判断是否有效
                    if (key.isValid()) {
                        if (key.isAcceptable()) {
                            //如果为accept状态,执行accept操作
                            //获取服务端通道
                            ServerSocketChannel sc = (ServerSocketChannel) key.channel();
                            //获取客户端通道
                            SocketChannel c = sc.accept();
                            //判断是否有客户端连接,非阻塞模式下,如果没有连接accept会返回null
                            if (Objects.nonNull(c)) {
                                System.out.println(Thread.currentThread().getName() + "一个客户端已连接");
                                //设置客户端通道为非阻塞模式
                                c.configureBlocking(false);
                                //将客户端通道注册到选择中,监听事件为读(监听的是客户端通道)
                                c.register(selector, SelectionKey.OP_READ);
                            }
                        }
                        if (key.isReadable()) {
                            //如果为read状态,执行read操作
                            //获取客户端通道
                            SocketChannel c = (SocketChannel) key.channel();
                            //定义一个缓冲区用于存储数据
                            ByteBuffer bf = ByteBuffer.allocate(1024);
                            //通道为非阻塞模式下,如果没有数据时,read会返回-1
                            if (c.read(bf) == -1) {
                                //不存在数据,关闭客户端通道,停止该选择器
                                System.out.println(Thread.currentThread().getName() + "一个客户端断开连接");
                                c.close();
                                key.cancel();
                            }
                            //将缓冲区切换到读模式
                            bf.flip();
                            //从通道中读取数据
                            String msg = new String(bf.array(), 0, bf.remaining());
                            System.out.println(Thread.currentThread().getName() + "接收客户端消息:" + msg);
                            //清空缓冲区
                            bf.clear();
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class Client {

    public static void main(String[] args) {
        try {
            //创建一个客户端通道
            SocketChannel channel = SocketChannel.open();
            //客户端通道连接服务端通道
            channel.connect(new InetSocketAddress("127.0.0.1", 9000));
            //将通道设置为非阻塞模式
            channel.configureBlocking(false);
            //定义一个缓冲区
            ByteBuffer bf = ByteBuffer.allocate(1024);
            Scanner sc = new Scanner(System.in);
            System.out.println("请说:");
            while(sc.hasNext()) {
                String msg = sc.nextLine();
                //将控制台信息写入缓冲区
                bf.put(msg.getBytes());
                //将缓冲区设置为读模式
                bf.flip();
                //通道将缓冲区写入
                channel.write(bf);
                //清空缓冲区
                bf.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四、AIO

1.AIO简介

AIO是异步非阻塞模型,从JDK1.7版本开始支持AIO,AIO模型需要操作系统的支持,AIO最大的特性是异步功能。

AIO服务端在创建时,需要创建一个accept的异步回调操作;当有客户端连接时,会执行accept的异步回调,并创建一个read的异步回调操作,当客户端有数据发送过来时,会执行read回调操作。

AIO的read和write都是异步的,对于读操作,当有流可读时,操作系统会通知应用程序进行读。

2.AIO代码示例

服务端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;

public class Server {

    public static void main(String[] args) {
        try {
            //1、创建服务端通道
            AsynchronousServerSocketChannel channel = AsynchronousServerSocketChannel.open();
            //2、绑定端口号
            channel.bind(new InetSocketAddress(9000));
            System.out.println("服务端启动");
            //3、接收客户端的链接,异步操作
            //第一个参数是要附加到I/O操作的对象,可以为null;第二个参数是一个连接结果处理器,当有连接完成后,会自动调用该处理器CompletionHandler来执行后面的内容,该处理器是一个泛型接口,第一个泛型类型是AsynchronousSocketChannel类,第二个泛型类型是前面提到的附加对象。
            channel.accept(null, new AcceptComplationHandler(channel));

            //因为accept是异步操作,防止当前程序直接结束
            while(true) {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * 接收accept的回调
 */
public class AcceptComplationHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {

    private AsynchronousServerSocketChannel channel;

    public AcceptComplationHandler(AsynchronousServerSocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void completed(AsynchronousSocketChannel result, Object attachment) {
        System.out.println("有一个客户端连接, ThreadName=" + Thread.currentThread().getName());
        //1、创建一个buffer
        ByteBuffer bf = ByteBuffer.allocate(1024);
        //2、创建异步接收数据
        result.read(bf, bf, new ReadCompletionHandler(result));
        //3、重复接收客户端消息,添加此行可以使多个客户端连接
        channel.accept(null, new AcceptComplationHandler(channel));
    }

    @Override
    public void failed(Throwable exc, Object attachment) {
        //客户端连接失败,继续接收下一个客户端连接
        channel.accept(null, this);
    }
}
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {

    private final AsynchronousSocketChannel channel;

    public ReadCompletionHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        System.out.println("读取客户端数据,ThreadName=" + Thread.currentThread().getName());
        if (result == -1) {
            //没有数据进行读取
            System.out.println(Thread.currentThread().getName() + "客户端关闭连接");
            try {
                //关闭当前通道
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }
        //将缓冲区设置为读模式
        attachment.flip();
        //读取缓冲区中的内容,转换为字符串
        String msg = new String(attachment.array(), 0, result);
        System.out.println(Thread.currentThread().getName() + "客户端说:" + msg);
        //清除缓冲区
        attachment.clear();
        //继续读取下一个报文,添加此行才可以多次读取数据
        channel.read(attachment, attachment, new ReadCompletionHandler(channel));
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.out.println(Thread.currentThread().getName() + "客户端读取失败断开连接");
        try {
            //关闭当前通道
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.Scanner;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class Client {
    public static void main(String[] args) {
        try {
            //创建一个通道
            AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
            System.out.println("客户端启动");
            //连接到指定的服务器,并获取结果
            Future<Void> connectFuture = channel.connect(new InetSocketAddress("127.0.0.1", 9000));
            //通过get方法来操作会阻塞,直到连接成功
            connectFuture.get();
            System.out.println("客户端连接服务端成功");
            //创建一个缓冲区
            ByteBuffer bf = ByteBuffer.allocate(1024);
            //读取控制台输入数据
            Scanner scanner = new Scanner(System.in);
            System.out.print("请说:");
            while (scanner.hasNext()) {
                String msg = scanner.nextLine();
                //将数据写入缓冲区
                bf.put(msg.getBytes());
                //将缓冲区转化为读模式
                bf.flip();
                //将缓冲区的内容写入通道中,直到成功
                channel.write(bf).get();
                //清空缓冲区
                bf.clear();
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

五、总结

1.BIO,NIO,AIO的区别

  • BIO 同步并阻塞,服务器一个连接就必须要要有一个线程,如果连接不做任何事,就会产生不必要的线程开销,可以使用多线程改善。
  • NIO 同步非阻塞,服务器一个请求一个线程,当有多个连接时,会注册到多路复用器中,多路复用器采用轮询的方式,当监听到需要处理时,再开启一个线程去处理。
  • AIO 异步非阻塞,服务器一个有效请求一个线程,客户端的请求会依靠操作系统先处理完,处理完后再通知服务器去启动线程进行处理。

2.BIO,NIO,AIO的使用场景

  • BIO 适用于连接数目比较小且固定的架构,对服务器要求比较高,并发局限于应用中。JDK1.4以前就支持。
  • NIO 适用于连接数目多,连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中。Netty网络框架就是使用NIO。JDK1.4开始支持。
  • AIO 适用于连接数目多,连接比较长(重操作)的架构,比如相册服务器,调用OS参与并发操作。JDK1.7开始支持。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值