NIO.md

Java NIO

一、Java NIO 和IO的区别

​ NIO和IO有同样的作用和目的,但是使用方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更高效的方式进行文件的读写操作。

IONIO
面向流(Stream Oriented)面向缓冲区(Buffer Oriented)
阻塞IO(Blocking IO)非阻塞IO(Non Blocking IO)
无选择器选择器(Selectors)

二、NIO的核心之一 — 缓冲区(Buffer)

缓冲区(Buffer):在Java NIO中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据。

(一)、缓冲区的四个核心属性

  1. caoacity :容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。

  2. limit :界限,表示缓冲区可以操作数据的大小。(limit后的数据不能进行读写)

  3. position :位置,表示缓冲区中正在操作数据的位置。

  4. mark :标记,表示记录当前position的位置。可以通过reset()恢复到mark的位置

  5. 标记、位置、限制、容量遵守以下不变式:

    0 <= mark <= position <= limit <= capacity

(二)、几大方法

  1. get()
  2. put()
  3. flip()
  4. rewind()

等等

具体看例子吧:

package com.nio;

import org.junit.Test;

import java.nio.ByteBuffer;

/**
 * Class BufferTest ...
 * @author LiJun
 * Created on 2018/8/14
 *
 * 一、缓冲区(Buffer):在Java NIO中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据。
 *
 * 根据数据类型的不同(boolean除外),提供了相应类型的缓冲区:
 *      ByteBuffer
 *      CharBuffer
 *      ShortBuffer
 *      IntBuffer
 *      LongBuffer
 *      FloatBuffer
 *      DoubleBuffer
 * 上述缓冲区的管理方式几乎一致,通过allocate()方法获取缓冲区
 *
 * 二、缓冲区存取数据的两个核心方法
 *  put()   :存入数据到缓冲区中
 *  get()   :获取缓冲区的数据
 *
 * 三、缓冲区的四个核心属性
 *  caoacity    :容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。
 *  limit   :界限,表示缓冲区可以操作数据的大小。(limit后的数据不能进行读写)
 *  position    :位置,表示缓冲区中正在操作数据的位置。
 *
 *  mark    :标记,表示记录当前position的位置。可以通过reset()恢复到mark的位置
 *
 *  position <= limit <= capacity
 *
 *
 * 四、直接缓冲区和非直接缓冲区
 *  非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中
 *  直接缓冲区:通过allocateDirect()方法直接分配缓冲区,将缓冲区分配到OS的物理内存中,可以提高效率。
 *
 */
public class BufferTest {

    @Test
    public void test2(){
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("abcde".getBytes());
        byteBuffer.flip();
        byte[] bytes = new byte[byteBuffer.limit()];
        byteBuffer.get(bytes, 0, 2);
        System.out.println(new String(bytes, 0, 2));
        System.out.println(byteBuffer.position());
        byteBuffer.mark();
        byteBuffer.get(bytes, 2, 2);
        System.out.println(new String(bytes, 2, 2));
        System.out.println(byteBuffer.position());
        //恢复到mark位置
        byteBuffer.reset();
        System.out.println(byteBuffer.position());

        //判断缓冲区是否还有剩余数据
        if(byteBuffer.hasRemaining()){
            System.out.println(byteBuffer.remaining());
        }

    }

    @Test
    public void test1(){
        //1. 分配一个指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        System.out.println("allocate");
        print(byteBuffer);

        //2.利用put()存入数据到缓冲区
        byteBuffer.put("abcde".getBytes());
        System.out.println("put");
        print(byteBuffer);

        //3. 切换到读取数据的模式
        byteBuffer.flip();
        System.out.println("flip");
        print(byteBuffer);

        //4.利用get()读取数据
        byte[] bytes = new byte[byteBuffer.limit()];
        byteBuffer.get(bytes);
        System.out.println(new String(bytes, 0, bytes.length));
        System.out.println("get");
        print(byteBuffer);

        //5. rewind() 表示可重复读
        byteBuffer.rewind();
        System.out.println("rewind");
        print(byteBuffer);

        //6.clear():清空缓冲区,但是缓冲区数据还在,但处于“被遗忘”状态,我们还能读出其中的数据,但是不知道具体有多少了,读取可能不争取
        byteBuffer.clear();
        System.out.println("clear");
        System.out.println(byteBuffer);

    }

    private void print(ByteBuffer byteBuffer ){
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.capacity());
    }
}

三、NIO的核心之二 — 通道(Channel)

​ Channel表示IO和目标节点打开的连接,Channel类似于传统的“流”,但是通道本身不能直接访问数据,Channel只能和Buffer进行交互。

(一)、通道的主要实现类

  • java.nio.channels.Channel接口

    • | – FileChannel
    • | – SocketChannel
    • | – ServerSocketChannel
    • | – DatagramChannel

(二)获取通道

  1. Java 针对支持通道的类提供了getChannel()方法

    本地IO:

     FileInputStream/FileOutputStream
    

    RandomAccessFile
    网络IO:
    Socket
    ServerSocket
    DatagramSocket

  2. 在JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法open()

  3. 在JDK 1.7 中的 NIO.2 的Files工具类的newByteChannel()方法

(三)、通道之间的数据传输

  1. transferFrom()
  2. transferTo()

(四)、分散(Scatter)与聚集(Gather)

  • 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中(按照缓冲区的顺序,从Channel中嘟嘟的数据依次将Buffer填满)
  • 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中(按照缓冲区的顺序,写入position和limit之间的数据写入Channel)
package com.nio;

import org.junit.Test;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;

/**
 * Class ChannelTest ...
 *
 * @author LiJun
 * Created on 2018/8/14
 * 一、通道(Channel):用于源节点和目标节点之间的连接。在Java NIO中负责缓冲区数据的传输。Channel本身不存数据,因此需要配合缓冲区进行传输
 * 二、通道的主要实现类
 *      java.nio.channels.Channel接口
 *          | -- FileChannel
 *          | -- SocketChannel
 *          | -- ServerSocketChannel
 *          | -- DatagramChannel
 * 三、获取通道
 *  1. Java 针对支持通道的类提供了getChannel()方法
 *      本地IO:
 *          FileInputStream/FileOutputStream
 *          RandomAccessFile
 *      网络IO:
 *          Socket
 *          ServerSocket
 *          DatagramSocket
 * <p>
 *  2. 在JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法open()
 *  3. 在JDK 1.7 中的 NIO.2 的Files工具类的newByteChannel()方法
 * <p>
 * 四、通道之间的数据传输
 *  transferFrom()
 *  transferTo()
 * <p>
 * 五、分散(Scatter)与聚集(Gather)
 *  分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中(按照缓冲区的顺序,从Channel中嘟嘟的数据依次将Buffer填满)
 *  聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中(按照缓冲区的顺序,写入position和limit之间的数据写入Channel)
 * <p>
 * 六、字符集
 *  编码:字符串 -> 字节数据
 *  解码:字节数组 -> 字符串
 */
public class ChannelTest {

    /**
     * 1. 利用通道完成文件的复制
     */
    @Test
    public void test1() {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            fileInputStream = new FileInputStream("D://1.txt");
            fileOutputStream = new FileOutputStream("D://3.txt");

            //获取通道
            inChannel = fileInputStream.getChannel();
            outChannel = fileOutputStream.getChannel();

            //分配指定大小缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

            //将通道的数据存入缓冲区中
            while (inChannel.read(byteBuffer) != -1) {
                //切换成读数据的模式
                byteBuffer.flip();
                //将缓冲区的数据写入通道中
                outChannel.write(byteBuffer);
                byteBuffer.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 (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 2. 利用直接缓冲区完成文件的复制(内存映射文件,异常直接抛出,但是正确的处理异常方式跟文件异常处理一样
     */
    @Test
    public void test2() throws IOException {

        FileChannel in = FileChannel.open(Paths.get("F://nio.zip"), StandardOpenOption.READ);
        FileChannel out = FileChannel.open(Paths.get("E://nio2.zip"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
        //内存映射文件  这个只有ByteBuffer支持
        MappedByteBuffer inMap = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size());
        MappedByteBuffer outMap = out.map(FileChannel.MapMode.READ_WRITE, 0, in.size());

        //直接对缓冲区进行数据的读写操作
        byte[] bytes = new byte[inMap.limit()];
        inMap.get(bytes);
        outMap.put(bytes);

        in.close();
        out.close();

    }

    /**
     * 利用直接缓冲区进行通道之间的数据传输
     *
     * @throws IOException
     */

    @Test
    public void test3() throws IOException {
        FileChannel in = FileChannel.open(Paths.get("D://1.txt"), StandardOpenOption.READ);
        FileChannel out = FileChannel.open(Paths.get("D://5.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
        //in.transferTo(0, in.size(), out);
        out.transferFrom(in, 0, in.size());

        in.close();
        out.close();

    }

    /**
     * 分散和聚集
     */
    @Test
    public void test4() throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("D://1.txt", "rw");

        //获取通道
        FileChannel channel = randomAccessFile.getChannel();

        //分配指定大小的缓冲区
        ByteBuffer byteBuffer1 = ByteBuffer.allocate(100);
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);

        //分散读取
        ByteBuffer[] byteBuffers = {byteBuffer1, byteBuffer2};

        channel.read(byteBuffers);

        for (ByteBuffer byteBuffer : byteBuffers) {
            byteBuffer.flip();
        }

        System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
        System.out.println("----------");
        System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));


        //聚集写入
        RandomAccessFile randomAccessFile1 = new RandomAccessFile("D://2.txt", "rw");
        FileChannel channel1 = randomAccessFile1.getChannel();
        channel1.write(byteBuffers);

    }

    /**
     * 字符集
     */
    @Test
    public void test5() {

        // 查查字符集有哪些内容
        SortedMap<String, Charset> stringCharsetSortedMap = Charset.availableCharsets();
        Set<Map.Entry<String, Charset>> entries = stringCharsetSortedMap.entrySet();
        for (Map.Entry<String, Charset> entry : entries) {
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }

    }

    @Test
    public void test6() throws CharacterCodingException {
        Charset charset1 = Charset.forName("UTF-8");
        //获取编码器
        CharsetEncoder charsetEncoder = charset1.newEncoder();
        //获取解码器
        CharsetDecoder charsetDecoder = charset1.newDecoder();

        CharBuffer charBuffer = CharBuffer.allocate(1024);


        charBuffer.put("字符集测试");
        charBuffer.flip();

        //编码
        ByteBuffer encode = charsetEncoder.encode(charBuffer);

        //解码
        encode.flip();
        CharBuffer decode = charsetDecoder.decode(encode);
        System.out.println(decode.toString());
    }
}

四、选择器(Selector)

选择器(Selector) 是 SelectableChannle 对象的多路复用器, Selector 可 以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。 Selector 是非阻塞 IO 的核心。

  • 选择器的创建

    Selector selector = Selector.open()

  • 向选择器注册通道: SelectableChannel.register(Selector sel, int ops)

  • 当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器 对通道的监听事件,需要通过第二个参数 ops 指定。 即SelectionKey :

    • 可以监听的事件类型(可使用 SelectionKey 的四个常量表示):

      • 读 : SelectionKey.OP_READ (1)
      • 写 : SelectionKey.OP_WRITE (4)
      • 连接 : SelectionKey.OP_CONNECT (8)
      • 接收 : SelectionKey.OP_ACCEPT (16)
    • 若注册时不止监听一个事件,则可以使用“位或”操作符连接。

      例如:serverSocketChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);

    SelectionKey: 表示 SelectableChannel 和 Selector 之间的注册关系。每次向 选择器注册通道时就会选择一个事件(选择键)。 选择键包含两个表示为整 数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。

    其中的方法有:

    方 法描 述
    int interestOps()获取感兴趣事件集合
    int readyOps()获取通道已经准备就绪的操作的集合
    SelectableChannel channel()获取注册通道
    Selector selector()返回选择器
    boolean isReadable()检测 Channal 中读事件是否就绪
    boolean isWritable()检测 Channal 中写事件是否就绪
    boolean isConnectable()检测 Channel 中连接是否就绪
    boolean isAcceptable()检测 Channel 中接收是否就绪

Selector的常用方法:

方 法描 述
Set keys()所有的 SelectionKey 集合。代表注册在该Selector上的Channel
selectedKeys()被选择的 SelectionKey 集合。返回此Selector的已选择键集
int select()监控所有注册的Channel,当它们中间有需要处理的 IO 操作时, 该方法返回,并将对应得的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量。
int select(long timeout)可以设置超时时长的 select() 操作
int selectNow()执行一个立即返回的 select() 操作,该方法不会阻塞线程
Selector wakeup()使一个还未返回的 select() 方法立即返回
void close()关闭该选择器

五、使用NIO完成网络通信的三个核心

(一)、使用NIO完成网络通信的三个核心

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

    java.nio.channel.Channel结构
    |–SelectableChannel
    |–SocketChannel

    ​ |–ServerSocketChannel

    ​ |–DatagramChannel

    ​ |–Pipe.SinkChannel

    ​ |–Pipe.SourceChannel

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

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

一个阻塞式NIO的例子:

package com.nio;

import org.junit.Test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * Class BlockingNIOTest2 ...
 *
 * @author LiJun
 * Created on 2018/8/15
 */
public class BlockingNIOTest2 {

    /**
     * 客户端
     */
    @Test
    public void client() throws IOException {
        SocketChannel socketChannel = SocketChannel.open(
                new InetSocketAddress("127.0.0.1", 9898));

        FileChannel inChannel = FileChannel.open(
                Paths.get("D://1.txt"), StandardOpenOption.READ);

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (inChannel.read(byteBuffer) != -1) {
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
        }

        socketChannel.shutdownOutput();

        // 接收服务端的反馈
        int len = 0;
        while((len = socketChannel.read(byteBuffer)) != -1){
            byteBuffer.flip();
            System.out.println(new String(byteBuffer.array(), 0, len));
            byteBuffer.clear();
        }

        inChannel.close();
        socketChannel.close();
    }

    /***
     * 服务端
     */
    @Test
    public void server() throws IOException {
        // 获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        FileChannel channel = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        // 绑定连接
        serverSocketChannel.bind(new InetSocketAddress(9898));
        // 获取客户端连接的通道
        SocketChannel accept = serverSocketChannel.accept();

        // 分配指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 接收客户端,并保存到本地
        while (accept.read(byteBuffer) != -1) {
            byteBuffer.flip();
            channel.write(byteBuffer);
            byteBuffer.clear();
        }
        byteBuffer.put("成功啦!".getBytes());
        byteBuffer.flip();
        accept.write(byteBuffer);
        // 关闭
        channel.close();
        serverSocketChannel.close();
        accept.close();
    }
}

重点来了(非阻塞式NIO):

注:非阻塞式重点在于将通道切换成非阻塞式模式,即调用其configureBlocking()方法,设置值为false; 其次声明选择器,并将通道注册到选择器中,然后指定监听事件

  1. 基于TCP的非阻塞式NIO:

    package com.nio;
    
    import org.junit.Test;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.*;
    import java.nio.file.Paths;
    import java.nio.file.StandardOpenOption;
    import java.util.Iterator;
    
    /**
     * Class NonBlockingNIOTest ...
     *
     * @author LiJun
     * Created on 2018/8/15
     */
    public class NonBlockingNIOTest {
    
        /**
         * 客户端
         */
        @Test
        public void client() throws IOException {
            // 获取通道
            SocketChannel socketChannel = SocketChannel.open(
                    new InetSocketAddress("127.0.0.1", 9898));
            // 切换成非阻塞模式
            socketChannel.configureBlocking(false);
    
    
            FileChannel inChannel = FileChannel.open(
                    Paths.get("D://1.txt"), StandardOpenOption.READ);
    
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            while (inChannel.read(byteBuffer) != -1) {
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                byteBuffer.clear();
            }
    
    
            inChannel.close();
            socketChannel.close();
        }
    
        /***
         * 服务端
         */
        @Test
        public void server() throws IOException {
            // 获取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 切换成非阻塞模式
            serverSocketChannel.configureBlocking(false);
            FileChannel channel = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            // 绑定连接
            serverSocketChannel.bind(new InetSocketAddress(9898));
            
            //获取选择器
            Selector open = Selector.open();
            // 将通道注册到选择器上,并且指定“监听事件”
            serverSocketChannel.register(open, SelectionKey.OP_ACCEPT);
            
            // 轮询式的获取选择器上已经"准备就绪"的时间
            while(open.select() > 0){
                // 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
                Iterator<SelectionKey> iterator = open.selectedKeys().iterator();
                // 迭代获取
                while (iterator.hasNext()){
                    // 获取准备“就绪”的事件
                    SelectionKey next = iterator.next();
                    // 判断具体是什么事件准备就绪
                    if(next.isAcceptable()){
                        // 若接收就绪,获取客户端连接
                        SocketChannel accept = serverSocketChannel.accept();
                        // 切换成非阻塞模式
                        accept.configureBlocking(false);
                        // 将通道注册到选择器上
                        accept.register(open, SelectionKey.OP_READ);
                    }else if(next.isReadable()){
                        // 获取当前选择器上的读就绪的通道
                        SocketChannel channel1 = (SocketChannel) next.channel();
                        // 读取数据
                        int len = 0;
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        while((len=channel1.read(byteBuffer)) > 0){
                            byteBuffer.flip();
                            System.out.println(new String(byteBuffer.array(), 0, len));
                            byteBuffer.clear();
                        }
                    }
                    // 取消选择键 SelectionKey
                    iterator.remove();
                }
            }
            // 关闭
            channel.close();
            serverSocketChannel.close();
        }
    }
    
  2. 基于UDP的非阻塞NIO

    package com.nio;
    
    import org.junit.Test;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.DatagramChannel;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.util.Date;
    import java.util.Iterator;
    import java.util.Scanner;
    
    /**
     * Class NonBlockingNIO2Test ...
     *
     * @author LiJun
     * Created on 2018/8/15
     */
    public class NonBlockingNIO2Test {
    
        @Test
        public void send() throws IOException {
            DatagramChannel dc = DatagramChannel.open();
    
            dc.configureBlocking(false);
    
            ByteBuffer buf = ByteBuffer.allocate(1024);
    
            Scanner scan = new Scanner(System.in);
    
            while(scan.hasNext()){
                String str = scan.next();
                buf.put((new Date().toString() + ":\n" + str).getBytes());
                buf.flip();
                dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
                buf.clear();
            }
    
            dc.close();
        }
    
        @Test
        public void receive() throws IOException{
            DatagramChannel dc = DatagramChannel.open();
    
            dc.configureBlocking(false);
    
            dc.bind(new InetSocketAddress(9898));
    
            Selector selector = Selector.open();
    
            dc.register(selector, SelectionKey.OP_READ);
    
            while(selector.select() > 0){
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    
                while(it.hasNext()){
                    SelectionKey sk = it.next();
    
                    if(sk.isReadable()){
                        ByteBuffer buf = ByteBuffer.allocate(1024);
    
                        dc.receive(buf);
                        buf.flip();
                        System.out.println(new String(buf.array(), 0, buf.limit()));
                        buf.clear();
                    }
                }
    
                it.remove();
            }
        }
    
    }
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值