Java之IO与NIO

NIO简介

  • 起源

    Java NIO(New IO)可以理解为非阻塞IO(Non Blocking IO),是从java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API

  • 使用方式

    NIO支持面向缓冲区,是基于通道的IO操作,文件读取操作更加高效

NIO与IO区别

面向对象不同

 IO是面向流(Stream Oriented)——流是单向的  

 NIO是面向缓冲区(Buffer Oriented)——缓冲区是双向的

阻塞方式不同

 IO是阻塞IO(Blocking IO)  

 NIO是非阻塞IO(Non Blocking IO)

其他

 NIO引入了Selectors

缓冲区(Buffer)和通道(Channel)

简而言之,Channel负责传输,Buffer负责存储

缓冲区(Buffer)

  • 简介

    一个用于特定基本数据类型的容器,实现对数据进行处理,由java.nio包定义,所有缓冲区都是Buffer抽象类的子类

  • 主要作用

    Java NIO中的Buffer主要用于NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的

  • 实现方式

    1. 缓冲区底层是数组,并根据数据类型不同(boolean除外),提供了相应类型的缓冲区

      ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer

    2. 都是通过allocate()获取缓冲区

  • 四大核心属性

    Buffer缓冲区核心属性例图

  • 直接缓冲区和非直接缓冲区

    直接缓冲区与非直接缓冲区

    可以通过isDirect()判断缓冲区类型

    非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中

    直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中

    1. 可以提高效率(测试发现大概提高10倍);
    2. 但不安全,且写入物理内存中后由操作系统主导控制
    3. 且引用只能有垃圾回收器进行断开引用,造成文件IO操作实际结束,但程序却停顿10秒左右才结束

    直接缓冲区流程图

  • 案例代码

    
        public class BufferTest {
    
            /**
            * 四大核心属性:
            * capacity:容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变
            * limit:界限,表示缓冲区中可以操作数据的大小(limit后数据不能读写)
            * position:位置,表示缓冲区中正在操作数据的位置
            *
            * mark:标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置
            *
            * 0 <= mark <= position <= limit <= capacity
            **/
            @Test
            public void test1(){
                //1.利用allocate() —— 初始化缓冲区
                ByteBuffer buf = ByteBuffer.allocate(1024);
                System.out.println(buf.capacity());
                System.out.println("初始化操作数据位置:" + buf.position());
                System.out.println(buf.limit());
    
    
                //2.利用put() —— 存入数据到缓冲区
                String str = "charles";
                System.out.println("------------------put-------------------------");
                buf.put(str.getBytes());
                System.out.println("写模式buffer操作数据位置:" + buf.position());
    
                //3.利用flip() —— 切换至读取数据模式
                buf.flip();
                System.out.println("------------------flip-------------------------");
                System.out.println("读模式buffer操作数据位置:" + buf.position());
                System.out.println(buf.limit());
    
                //4.利用get() —— 读取缓冲区中的数据
                byte[] bst = new byte[buf.limit()];
                buf.get(bst);//将缓冲区中的数据读到字节数组中
                System.out.println("------------------get-------------------------");
                System.out.println(new String(bst,0,bst.length));
                
                System.out.println(buf.position());//读取结束后缓冲区中操作数据库中的位置为读取数据长度
                System.out.println(buf.limit());
                System.out.println(buf.capacity());
    
                //5.利用rewind() —— 恢复到可重复读模式
                buf.rewind();
                System.out.println("------------------rewind-------------------------");
                System.out.println(buf.position());
                System.out.println(buf.limit());
                System.out.println(buf.capacity());
    
                //6.利用clear() —— 清空缓冲区(缓冲区中数据依然存在,但是处于被遗忘状态,无法获取位置和状态)
                buf.clear();
                System.out.println("-----------------clear--------------------------");
                System.out.println(buf.position());
                System.out.println(buf.limit());
                System.out.println(buf.capacity());
                System.out.println((char)buf.get());//默认读取是ASCII码
            }
    
    
            @Test
            public void test2(){
                String str = "charles";
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                buffer.put(str.getBytes());
                buffer.flip();//切换为读模式
                byte[] dst = new byte[buffer.limit()];
                buffer.get(dst,0,3);
                System.out.println(new String(dst,0,3));
                System.out.println("当前操作数据位置:" + buffer.position());
    
                buffer.mark();//标记
    
                buffer.get(dst,5,2);
                System.out.println(new String(dst,0,7));//思考为什么输出是:cha rle
                System.out.println(buffer.position());
    
                buffer.reset();
                System.out.println(buffer.position());
    
    
                //判断缓冲区是否还有剩余数据
                if (buffer.hasRemaining()) {
                    //获取缓冲区中可以操作的数量
                    System.out.println(buffer.remaining());
                }
    
            }
        }
    
    
    

通道(Channel)

  • 例图

    通道Channel交互示意图

  • 简介

    通道表示打开到IO设备(列如:文件、套接字)的连接。类似于传统的"流",只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。另外,Channel是一个完全独立的处理器,附属于CPU,替代以往的DMA总线,直接负责与IO接口交互

  • 常用实现类

    java.nio.channels.Channel接口
    |–FileChannel(用于本地)
    |–SocketChannnel(用于网络)
    |–ServerSocketChannel(用于网络)
    |–DatagramChannel(用于网络)

    FileChannel不能切换为非阻塞模式,非阻塞模式是针对于网络传输

文件通道(FileChannel)

获取通道方式

  • Java针对支持通道的类提供了getChannel()方法
    本地IO:
    FileInputStream/FileOutputStream
    RandomAccessFile

    网络IO:
        Socket
        ServerSocket
        DatagramSocket
    
  • 在JDK1.7中的NIO.2针对各个通道提供了静态方法open()

  • 在JDK1.7中的NIO.2的Files工具类的newByteChannel()

通道之间的数据传输

transferFrom()
transferTo()

分散与聚集

  • 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中

  • 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中

  • 例图

    ![分散(Scatter)]与聚集(Gather)](https://img-blog.csdnimg.cn/20200229121058690.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjU4NjcyMw==,size_16,color_FFFFFF,t_70)

字符集(Charset)

编码:字符串 ——> 字节数组

解码:字节数组 ——> 字符串

案例代码

  • 案例

    
    
        public class TestChannel {
    
    
            /**
            * @Author charlesYan
            * @Description 使用非直接缓冲区利用通道完成文件的复制
            * @Date 11:26 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void test1(){
                FileInputStream fis = null;
                FileOutputStream fos = null;
                FileChannel inChannel = null;
                FileChannel outChannel = null;
                try {
                    fis = new FileInputStream("D:\\temp\\SynchronizedDemo.java");
                    fos = new FileOutputStream("D:\\temp\\SynchronizedDemo2.java");
    
                    //1.获取通道
                    inChannel = fis.getChannel();
                    outChannel = fos.getChannel();
    
                    //2.分配指定大小的缓冲区
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
                    //3.将通道中的数据存入缓冲区中
                    while (inChannel.read(buffer) != -1){
                        buffer.flip();//切换读取数据的模式
                        //4.将缓冲区中的数据写入通道中
                        outChannel.write(buffer);
                        buffer.clear();//清空缓冲区
    
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (outChannel != null) {
                        try {
                            outChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
    
                    if (inChannel != null) {
                        try {
                            inChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
    
                    }
    
                    if (fos != null) {
                        try {
                            fos.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
    
                    }
    
                    if (fis != null) {
                        try {
                            fis.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
    
                    }
                }
    
            }
    
    
            /**
            * @Author charlesYan
            * @Description 使用直接缓冲区完成文件的复制(内存映射文件)
            * @Date 11:51 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void test2() throws IOException {
    
                FileChannel inChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo.java"), StandardOpenOption.READ);
                //StandardOpenOption.CREATE_NEW若存在,则创建报错
                FileChannel outChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo2.java"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
    
                //内存映射文件(只有ByteBuffer支持)
                MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
                MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
    
                //直接对缓冲区进行数据的读写操作,无需通过Channel操作
                byte[] dst = new byte[inMappedBuf.limit()];
                inMappedBuf.get(dst);
                outMappedBuf.put(dst);
    
                inChannel.close();
                outChannel.close();
    
            }
    
            /**
            * @Author charlesYan
            * @Description 通道之间的数据传输
            * @Date 12:28 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void test3() throws IOException {
    
                FileChannel inChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo.java"), StandardOpenOption.READ);
                //StandardOpenOption.CREATE_NEW若存在,则创建报错
                FileChannel outChannel = FileChannel.open(Paths.get("D:\\temp\\SynchronizedDemo2.java"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
    
        //        inChannel.transferTo(0,inChannel.size(),outChannel);
                outChannel.transferFrom(inChannel,0,inChannel.size());
                
                inChannel.close();
                outChannel.close();
            }
        }
    
    
            /**
            * @Author charlesYan
            * @Description 分散读取
            * @Date 14:25 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void test4() throws IOException {
    
                RandomAccessFile raf = new RandomAccessFile("D:\\temp\\如何将UTC时间转换为CST时间格式.md", "rw");
    
                //1.获取通道
                FileChannel channel = raf.getChannel();
    
                //2.分配指定大小的缓冲区
                ByteBuffer buf1 = ByteBuffer.allocate(100);
                ByteBuffer buf2 = ByteBuffer.allocate(10240);
    
                //3.分散读取
                ByteBuffer[] bufs = {buf1,buf2};
                channel.read(bufs);
    
                for (ByteBuffer byteBuffer : bufs) {
                    byteBuffer.flip();
                }
    
                System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
                System.out.println("-----------------------");
                System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
            }
    
    
            /**
            * @Author charlesYan
            * @Description 聚集写入与分散读取
            * @Date 14:27 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void test5() throws IOException {
    
                RandomAccessFile inRaf = new RandomAccessFile("D:\\temp\\如何将UTC时间转换为CST时间格式.md", "rw");
    
                RandomAccessFile outRaf = new RandomAccessFile("D:\\temp\\如何将UTC时间转换为CST时间格式1.md", "rw");
    
                //获取通道
                FileChannel channel = inRaf.getChannel();
    
                //2.分配指定大小的缓冲区
                ByteBuffer buf1 = ByteBuffer.allocate(100);
                ByteBuffer buf2 = ByteBuffer.allocate(10240);
    
                //3.分散读取
                ByteBuffer[] bufs = {buf1,buf2};
                channel.read(bufs);
    
                for (ByteBuffer byteBuffer : bufs) {
                    byteBuffer.flip();
                }
    
                //获取通道
                FileChannel outChannel = outRaf.getChannel();
                outChannel.write(bufs);
            }
    
    
            /**
            * @Author charlesYan
            * @Description 编码与解码
            * @Date 16:07 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void test6() throws CharacterCodingException {
                Map<String, Charset> map = Charset.availableCharsets();
    
                Charset gbk = Charset.forName("GBK");
                //获取编码器
                CharsetEncoder gbkEncoder = gbk.newEncoder();
                //获取解码器
                CharsetDecoder gbkDecoder = gbk.newDecoder();
    
                //初始化Buffer
                CharBuffer charBuffer = CharBuffer.allocate(2048);
                charBuffer.put("我是谁,我在哪");
                charBuffer.flip();
    
                //编码
                ByteBuffer byteBuffer = gbkEncoder.encode(charBuffer);
                for (int i = 0; i < byteBuffer.limit(); i++) {
                    System.out.println(byteBuffer.get());
    
                }
    
                //解码
                byteBuffer.flip();
                CharBuffer newCharBuffer = gbkDecoder.decode(byteBuffer);
                System.out.println(newCharBuffer.toString());
    
            }
    
    
    

NIO非阻塞式网络通信

  • 阻塞与非阻塞

    IO阻塞原因
    用户从客户端网络发送数据到服务端,先到服务端内核地址空间,服务端判断在内核地址空间中数据是否存在,若没有数据,则服务端该线程就等待;若内核地址空间存在数据,则将数据copy到用户地址空间

    阻塞与非阻塞

选择器(Selector)

SelectionKey

  • 概念

    表示SelectableChannel和Selector之间的注册关系。每次向选择器注册通道时,就会选择一个事件(选择键)。

  • 类型

    选择键包含两个表示为整数值的操作集,操作集的每一位都表示该键的通道所支持的一类可选择操作。

    SelectionKey类型

  • 作用

    选择器会将每一个用于传输数据的通道都注册到自身,从而监控这些通道的状况,当某一个通道的某一个请求的数据完全准备就绪时,选择器才会将该任务分配到服务端一个或多个线程中运行

  • 使用NIO完成网络通信的3个核心

    1. 通道(Channel):负责连接

    2. 缓冲区(Buffer):负责数据的存取

    3. 选择器(Selector):是SelectableChannel的多路复用器。用于监控SelectableChannel的IO状况

通道(Channel)

  • 常用接口

    java.nio.channels.Channel接口:
    |–SelectableChannel
    |–SocketChannel(是一个连接到TCP网络套接字的通道)
    |–ServerSocketChannel
    |–DatagramChannel

        |--Pipe.SinkChannel
        |--Pipe.SourceChannel
    

NIO_阻塞式

  • 案例代码

    
       //阻塞式IO
       public class TestBlockingNIO {
    
            /**
            * @Author charlesYan
            * @Description 服务端接收数据
            * @Date 21:55 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void server() throws IOException {
                //1.获取通道
                ServerSocketChannel serverChannel = ServerSocketChannel.open();
    
                //创建本地FileChannel
                FileChannel fileChannel = FileChannel.open(Paths.get("D:\\temp\\如何将UTC时间转换为CST时间格式2.md"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
    
                //2.绑定连接
                serverChannel.bind(new InetSocketAddress(9898));
    
                //3.获取客户端连接的通道
                SocketChannel clientChannel = serverChannel.accept();
    
                //4.分配指定大小的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
    
                //5.接收客户端的数据
                while (clientChannel.read(buffer) != -1) {
                    buffer.flip();
                    fileChannel.write(buffer);
                    buffer.clear();
                }
    
                //6.发送反馈给客户端
                buffer.put("服务端接收数据成功".getBytes());
                buffer.flip();
                clientChannel.write(buffer);
    
                //7.关闭通道
                clientChannel.close();
                fileChannel.close();
                serverChannel.close();
            }
    
    
            /**
            * @Author charlesYan
            * @Description //客户端发送数据
            * @Date 21:55 2020/2/27
            * @Param []
            * @return void
            **/
            @Test
            public void client() throws IOException {
    
                //1.获取通道
                SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
    
                //本地FileChannel
                FileChannel fileChannel = FileChannel.open(Paths.get("D:\\temp\\如何将UTC时间转换为CST时间格式.md"), StandardOpenOption.READ);
    
                //2.分配指定大小缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
    
                //3.读取本地文件,并发送到客户端
                while (fileChannel.read(buffer) != -1) {
                    buffer.flip();
                    clientChannel.write(buffer);
                    buffer.clear();
                }
    
                //由于服务端不知道客户端数据是否传输结束,进入阻塞模式;可以选择将客户端通道关闭,或者将线程切换为非阻塞模式
                //此处选择将客户端通道关闭
                clientChannel.shutdownOutput();
    
                //4.接收服务端反馈
                int len = 0;
                while ((len = clientChannel.read(buffer)) != -1) {
                    buffer.flip();
                    System.out.println(new String(buffer.array(),0,len));
                    buffer.clear();
                }
    
                //5.关闭通道
                fileChannel.close();
                clientChannel.close();
    
            }
    
    
        }
    
    

NIO_非阻塞式

  • 案例代码

    
        public class TestNonBlockingNIO {
    
    
            /**
            * @Author charlesYan
            * @Description //非阻塞模式客户端
            * @Date 18:39 2020/2/28
            * @Param []
            * @return void
            **/
            @Test
            public void client() throws IOException {
    
                //1.获取客户端通道
                SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8989));
    
                //2.切换为非阻塞模式
                clientChannel.configureBlocking(false);
    
                //3.分配指定大小缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
    
                //4.发送数据给服务端
                Scanner scanner = new Scanner(System.in);
                while (scanner.hasNext()) {
                    String next = scanner.next();
                    //Java1.8日期API
                    LocalDateTime now = LocalDateTime.now();
                    String nowStr = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
                    buffer.put((nowStr + "\n" + next).getBytes());
                    buffer.flip();
                    clientChannel.write(buffer);
                    buffer.clear();
    
                }
    
                //5.关闭通道
                clientChannel.close();
    
            }
    
    
            @Test
            public void server() throws IOException {
    
                //1.获取服务端通道
                ServerSocketChannel serverChannel = ServerSocketChannel.open();
    
                //2.切换为非阻塞模式
                serverChannel.configureBlocking(false);
    
                //3.绑定连接
                serverChannel.bind(new InetSocketAddress(8989));
    
                //4.获取选择器
                Selector selector = Selector.open();
    
                //5.将通道注册到选择器上,并且指定"监听接收事件"
                serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    
                //6.轮询式获取选择器上已经"准备就绪"的事件
                while (selector.select() > 0) {
    
                    //7.获取当前选择器中所有注册的"选择键(已经就绪的监听事件)"
                    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    
                    while (it.hasNext()) {
                        //8.获取准备"就绪"的事件
                        SelectionKey key = it.next();
    
                        //9.判断具体什么事件准备就绪
                        if (key.isAcceptable()) {
                            //10.若"接收就绪"则获取客户端连接
                            SocketChannel clientChannel = serverChannel.accept();
    
                            //11.切换每个客户端通道为非阻塞
                            clientChannel.configureBlocking(false);
    
                            //12.将该通道注册到选择器上
                            clientChannel.register(selector,SelectionKey.OP_READ);
                        }else if(key.isReadable()){
    
                            //13.获取当前选择器上"读就绪"状态的通道
                            SocketChannel clientChannel = (SocketChannel) key.channel();
    
                            //14.读取数据
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            int len = 0;
                            while ((len = clientChannel.read(buffer)) > 0) {
                                buffer.flip();
                                System.out.println((new String(buffer.array(),0,len)));
                                buffer.clear();
                            }
                        }
    
                        //15.取消选择键SelectionKey
                        it.remove();
                    }
                }
    
            }
        }
    
    

NIO_DatagramChannel

  • 概念
    Java NIO中的DatagramChannel是一个能收发UDP包的通道

  • 案例代码

    
        @Test
        public void send() throws IOException {
    
            //1.创建客户端通道
            DatagramChannel clientChannel = DatagramChannel.open();
    
    
            //2.创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
    
            //3.输入数据
            Scanner in = new Scanner(System.in);
            while (in.hasNext()) {
                String str = in.next();
                buffer.put((new Date().toString() + "\n" + str.getBytes()).getBytes());
                buffer.flip();
                //4.发送数据
                clientChannel.send(buffer,new InetSocketAddress("127.0.0.1",8080));
                buffer.clear();
            }
    
    
            //5.关闭客户端通道
            clientChannel.close();
        }
    
    
        @Test
        public void receive() throws IOException {
    
            //1.打开服务端通道
            DatagramChannel serverChannel = DatagramChannel.open();
    
            //2.切换为非阻塞模式
            serverChannel.configureBlocking(false);
    
            //3.绑定端口号
            serverChannel.bind(new InetSocketAddress(8080));
    
            //4.创建选择器
            Selector selector = Selector.open();
    
            //5.将服务端通道注册到选择器
            serverChannel.register(selector, SelectionKey.OP_READ);
    
            //6.轮询选择器已经准备就绪事件
            while (selector.select() > 0) {
    
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    
                while (it.hasNext()) {
    
                    SelectionKey sk = it.next();
                    if (sk.isReadable()) {
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        serverChannel.receive(buffer);
                        buffer.flip();
                        System.out.println(new String(buffer.array(),0,buffer.limit()));
                        buffer.clear();
                    }
                }
                //7.取消选择键SelectionKey
                it.remove();
            }
        }
    
    

管道(Pipe)

  • 概念
    Java NIO管道是2个线程之间的单向数据连接。

  • 作用
    Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取

  • 案例代码

    
        @Test
        public void pipe() throws IOException {
            //1.获取管道
            Pipe pipe = Pipe.open();
    
            //2.将缓冲区中的数据写入管道
            ByteBuffer buffer = ByteBuffer.allocate(1024);
    
            Pipe.SinkChannel sink = pipe.sink();
            buffer.put("通过单向管道发送数据".getBytes());
            buffer.flip();
            sink.write(buffer);
    
            //3.读取缓冲区中的数据
            Pipe.SourceChannel source = pipe.source();
            buffer.flip();
            int len = source.read(buffer);
            System.out.println(new String(buffer.array(),0,len));
    
            source.close();
            sink.close();
    
        }
    
    

参考链接

https://www.cnblogs.com/aspirant/p/8630283.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值