BIO(IO),NIO,AIO

目录

# 同步?异步?阻塞?非阻塞?

# 同步IO和异步IO,阻塞IO和非阻塞IO  区别

# 概念

#BIO(IO)

#NIO

#AIO

 

 

同步?异步?阻塞?非阻塞?

同步:用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪。

异步:用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)

阻塞:当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止

非阻塞: 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待

综上所述:

同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。

阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。

同步和异步是目的,阻塞和非阻塞是实现方式。

 

=======================================================================

 

同步IO和异步IO,阻塞IO和非阻塞IO  区别

一个IO操作其实分成了两个步骤:发起IO请求 和 实际的IO操作。 

同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO

阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。 

 

=======================================================================

概念

一、同步阻塞I/O(BIO):jdk1.4以前  面向流

用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。 

服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。

BIO方式架适用于连接数目比较小且固定的构,这种方式对服务端资源要求比较高,并发局限于应用中。

 

二、同步非阻塞I/O(NIO):jdk1.4开始支持  面向缓冲区

用户进程发起一个IO操作以后便可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。

服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。

 

三、异步非阻塞I/O(AIO):jdk1.7开始支持

应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序。

服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。

AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。

 

 

喻:

目标:烧十壶开水。

IO:需要10个人,一个人盯一壶。

BIO:需要1个人,轮流查看几个壶。

AIO:需要1个人兼职,给壶装个开关,水开后提示,水未开时人可以去做任何事。

=======================================================================

BIO(IO)

Socket又称“套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。

Socket和ServerSocket类库位于java.net包中,serverSocket用于服务器端,Socket是建立网络连接时使用的。

在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。

对于一个网络连接来说,套接字是平等的,不因为在服务器端或在客户端而产生不同的级别,

Socket和ServerSocket都是通过SocketImpl类及其子类完成的

缺点:该模型最大的问题就是,当客户端数量增加时,服务端的线程数与客户端并发的数量是 1:1 关系,我们知道在jvm 中线程是非常宝贵的资源,当线程数不断上升时,系统性能将不断下降,可能会出现堆栈溢出,创建线程失败等问题,并最终导致宕机或者僵死,不对外提供服务。

 

网络编程的基本模型Client/Server模型,也就是两个进程直接进行相互通信,其中服务端提供配置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接成功,则双方即可进行通信(网络套接字socket)

 

优化:

采用线程池和任务队列可以实现一种伪异步的IO通信框架。

我们学过连接池的使用和队列的使用,其实就是将客户端的socket封装成一个task任务(实现runnable接口的类)然后投递到线程池中去,配置相应的队列进行实现。

代码演示

package com.io.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class ServerHandler implements Runnable {

    private Socket socket;

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

    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            out = new PrintWriter(this.socket.getOutputStream(), true);
            String body =  null;
            while (true) {
                body =  in.readLine();
                if (body == null) {
                    break;
                }
                System.out.println("Server :"+ body);
                out.println("服务器端回送响的应数据.");
            }
        } catch (IOException e) {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (out != null) {
                out.close();
                out = null;
            }
            if (this.socket != null) {
                try {
                    this.socket.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
                this.socket = null;
            }
        }
    }
}
package com.io.bio;

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

public class Server {
    public static void main(String[] args) throws IOException {
        final int port = 8088;
        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("服务器端口号:" + port);
            Socket socket;
            while (true){
                //进行阻塞
                socket = server.accept();
                //新建一个线程执行客户端的任务
                new Thread(new ServerHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(server != null){
                System.out.println("The time server close");
                server.close();
                server = null;
            }
        }
    }
}
package com.io.bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Client {
    public static void main(String[] args) {
        final int port = 8088;

        Socket socket = null;
        BufferedReader reader = null;
        PrintWriter writer = null;

        try {
            socket = new Socket("127.0.0.1", port);
        } catch (IOException e) {
            System.out.println("客户端初始化失败");
        }

        try {
            writer = new PrintWriter(socket.getOutputStream(),true);
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        } catch (IOException e) {
            System.out.println("输入输出流获取失败");
        }

        writer.println("客户端向服务器端发送的请求");

        try {
            String readLine = reader.readLine();
            System.out.println("Client: " + readLine);
        } catch (IOException e) {
            System.out.println("读取失败");
        }finally {
            if(writer != null){
                writer.close();
                writer = null;
            }
            if(reader != null){
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                reader = null;
            }
        }
    }
}

 

=======================================================================

NIO

NIO 叫做 new IO,也叫做 Non-block IO , 即非阻塞IO

传统的IO流(inputstream,outputstream)都是单向的管道,要么去读,要么去写。

有一个server端,有n个client端。古老的socket编程,是每个客户端直接向server端发起一个套接字,建立一个tcp连接。

NIO是在传统的tcp之上进行一个抽象。不是client端和server端直接进行连接,而是,把client的通道注册到server端。

在NIO中没有Socket和ServerSocket的概念。

在NIO中服务端要实例化出一个ServerSocketChannel(把ServerSocket进行了一个抽象),客户端使用SocketChannel。

在Server端会创建一个Selector(多路复用器)。所有客户端的SocketChannel都要注册到Selector。

Selector的机制是使用一个线程,去轮询所有注册到这个服务器端的SocketChannel,根据通道的状态,执行相关的操作。
 

概念

(1)Buffer-缓冲区

(2)Channel(管道。通道)

(3)Selector(选择器,多路复用器)

Buffer

是一个对象,它包含一些要写入或者要读取的数据。在NIO类库中加入Buffer对象,体现了新库与原IO的一个重要的区别。在面向流的IO中,可以将数据直接写入或读取到Stream对象中。在NIO库中,所有的数据都是用缓冲区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组,这个数组为缓冲区提供了数据的访问读写等操作属性,如位置,容量,上限等概念,参考API文档。Buffer类型:我们最常用的就是ByteBuffer,实际上每一种java基本类型都对应了一种缓冲区(除了Boolean类型)

ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBUffer、FloatBuffer、DoubleBUffer

 

使用:

flip()  将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。

get() 读取数据

rewind() 将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素

mark() 可以标记Buffer中的一个特定position。之后可以通过调用。

reset() 恢复到Buffer.mark()标记时的position。

clear() 清空整个缓冲区。position将被设回0,limit被设置成 capacity的值

compact() 只会清除已经读过的数据;任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

   将position设到最后一个未读元素正后面,limit被设置成 capacity的值。

这里写图片描述

通道(Channel)

它就像自来水管道一样,网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动(一个流必须是inputStream或者outputStream的子类),而通道可以用于读,写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。

事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们使用的SocketChannel和ServerSockerChannel都是SelectableChannel的子类

 

使用

FileChannel:用于读取、写入、映射和操作文件的通道。

DatagramChannel:通过 UDP 读写网络中的数据通道。

SocketChannel:通过 TCP 读写网络中的数据。

ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

AbstractInterruptibleChannel

AbstractSelectableChannel

Pipe.SinkChannel

Pipe.SourceChannel

SelectableChannel

这里写图片描述

多路复用器(seletor)

他是NIO编程的基础,非常重要,多路复用器提供选择已经就绪的任务的能力。

Selector线程就类似一个管理者(Master),管理了成千上万个管道,然后轮询哪个管道的数据已经准备好,通知CPU执行IO的读取或写入操作。

Selector模式:当IO事件(管理)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector选择器是以轮询的方式进行查找注册的所有IO事件(管道)

如果某个通道发生了读写操作,这个通道就处于就绪状态,select就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道里读或写数据,写道我们的数据缓冲区中)。

 

每个管道都会对选择器进行注册不同的事件状态,以便选择器查找。

SelectionKey.OP_CONNECT         连接状态

SelectionKey.OP_ACCEPT            阻塞状态

SelectionKey.OP_READ                可读状态

SelectionKey.OP_WRITE               可写状态

创建NIO服务器的主要步骤

    (1)打开ServerSocketChannel,监听客户端连接

    (2)绑定监听端口,设置连接为非阻塞模式

    (3)创建Reactor线程,创建多路复用器并启动线程

     (4)将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件

     (5)Selector轮询准备就绪的key

     (6)Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,简历物理链路

     (7)设置客户端链路为非阻塞模式

     (8)将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息

     (9)异步读取客户端消息到缓冲区

     (10)对Buffer编解码,处理半包消息,将解码成功的消息封装成Task

     (11)将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端

因为应答消息的发送,SocketChannel也是异步非阻塞的,所以不能保证一次能吧需要发送的数据发送完,此时就会出现写半包的问题。我们需要注册写操作,不断轮询Selector将没有发送完的消息发送完毕,然后通过Buffer的hasRemain()方法判断消息是否发送完成。

 

代码演示

package com.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class Server implements Runnable {
    //多路复用器(管理所有的通道)
    private Selector seletor;
    //2 建立缓冲区
    private ByteBuffer readBuf = ByteBuffer.allocate(1024);
    //3
    private ByteBuffer writeBuf = ByteBuffer.allocate(1024);

    public Server(int port) {
        try {
            //步骤一: 打开服务器通道(ServerSocketChannel),用于监听客户端的连接,它是所有客户端连接的副管道
            ServerSocketChannel socketChannel = ServerSocketChannel.open();

            //步骤二: 绑定监听端口,设置连接为非阻塞模式
            socketChannel.bind(new InetSocketAddress(port));
            socketChannel.configureBlocking(false);

            //步骤三: 创建多路复用器(Selector)
            this.seletor = Selector.open();

            //步骤四: 把服务器通道(ServerSocketChannel)注册到多路复用器(Selector)上,并且监听阻塞(ACCEPT)事件
            socketChannel.register(this.seletor, SelectionKey.OP_ACCEPT);

            System.out.println("Server start, port :" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //步骤五:多路复用器在线程run方法的无线循环体内轮询准备就绪的key
        while (true) {
            try {
                //1 必须要让多路复用器开始监听
                this.seletor.select();
                //2 返回多路复用器已经选择的结果集
                Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
                //3 进行遍历
                while (keys.hasNext()) {
                    //4 获取一个选择的元素
                    SelectionKey key = keys.next();
                    //5 直接从容器中移除就可以了
                    keys.remove();
                    //6 如果是有效的
                    if (key.isValid()) {
                        //7 如果为阻塞状态
                        if (key.isAcceptable()) {
                            //步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理连接
                            //步骤七:设置客户端链路为非阻塞模式 3
                            //步骤八:将新接入的客户端连接注册到的多路复用器(Selector)上,监听读操作,读取客户端发送的网络消息 4
                            this.accept(key);
                        }
                        //8 如果为可读状态
                        if (key.isReadable()) {
                            //步骤九:异步读取客户端请求消息到缓冲区
                            this.read(key);
                        }
                        //9 写数据
                        if (key.isWritable()) {
                            this.write(key);
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void write(SelectionKey key) {
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        try {
            ssc.register(this.seletor, SelectionKey.OP_WRITE);
        } catch (ClosedChannelException e) {
            e.printStackTrace();
        }
    }

    private void read(SelectionKey key) {
        try {
            //1 清空缓冲区旧的数据
            this.readBuf.clear();
            //2 获取之前注册的socket通道对象
            SocketChannel sc = (SocketChannel) key.channel();
            //3 读取数据
            int count = sc.read(this.readBuf);
            //4 如果没有数据
            if (count == -1) {
                key.channel().close();
                key.cancel();
                return;
            }
            //5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
            this.readBuf.flip();
            //6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
            byte[] bytes = new byte[this.readBuf.remaining()];
            //7 接收缓冲区数据
            this.readBuf.get(bytes);
            //8 打印结果
            String body = new String(bytes).trim();
            // 9.可以写回给客户端数据
            System.out.println("Server : " + body);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void accept(SelectionKey key) {
        try {
            //1 获取服务通道
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            //2 执行阻塞方法
            SocketChannel sc = ssc.accept();
            //3 设置为非阻塞模式
            sc.configureBlocking(false);
            sc.socket().setReuseAddress(true);
            //4 注册到多路复用器上,并设置读取标识
            sc.register(this.seletor, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(new Server(8081)).start();
    }
}

 

package com.io.nio;

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

public class Client {

    //需要一个Selector
    public static void main(String[] args) {
        //创建连接的地址
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8081);
        //声明连接通道
        SocketChannel sc = null;
        //建立缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        try {
            //打开通道
            sc = SocketChannel.open();
            //进行连接
            sc.connect(address);
            while(true){
                //定义一个字节数组,然后使用系统录入功能:
                byte[] bytes = new byte[1024];
                System.in.read(bytes);
                buf.put(bytes);//把数据放到缓冲区中
                buf.flip();//对缓冲区进行复位
                sc.write(buf);//写出数据
                buf.clear();//清空缓冲区数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(sc != null){
                try {
                    sc.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

推荐NIO底层讲解网址:

深入浅出NIO之Channel、Buffer

深入浅出NIO之Selector实现原理

 

=======================================================================

AIO

package com.io.aio;

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;

public class ServerHandler implements CompletionHandler<AsynchronousSocketChannel, Server> {

    @Override
    public void completed(AsynchronousSocketChannel asc, Server attachment) {
        //当有下一个客户端接入的时候 直接调用Server的accept方法,这样反复执行下去,保证多个客户端都可以阻塞(没有递归上限)
        // 1.7以后AIO才实现了异步非阻塞
        attachment.assc.accept(attachment, this);
        read(asc);
    }

    private void read(final AsynchronousSocketChannel asc) {
        //读取数据
        ByteBuffer buf = ByteBuffer.allocate(1024);
        asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer resultSize, ByteBuffer attachment) {
                //进行读取之后,重置标识位
                attachment.flip();
                //获得读取的字节数
                System.out.println("Server -> " + "收到客户端的数据长度为:" + resultSize);
                //获取读取的数据
                String resultData = new String(attachment.array()).trim();
                System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData);
                String response = "服务器响应, 收到了客户端发来的数据: " + resultData;
                write(asc, response);
            }
            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                exc.printStackTrace();
            }
        });
    }

    private void write(AsynchronousSocketChannel asc, String response) {
        try {
            ByteBuffer buf = ByteBuffer.allocate(1024);
            buf.put(response.getBytes());
            buf.flip();
            asc.write(buf).get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void failed(Throwable exc, Server attachment) {
        exc.printStackTrace();
    }
}
package com.io.aio;

import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server{
    //线程池
    private ExecutorService executorService;
    //线程组
    private AsynchronousChannelGroup threadGroup;
    //服务器通道
    public AsynchronousServerSocketChannel assc;

    public Server(int port){
        try {
            //创建一个缓存池
            executorService = Executors.newCachedThreadPool();
            //创建线程组
            threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
            //创建服务器通道
            assc = AsynchronousServerSocketChannel.open(threadGroup);
            //进行绑定
            assc.bind(new InetSocketAddress(port));
            System.out.println("server start , port : " + port);
            //进行阻塞
            assc.accept(this, new ServerHandler());
            //一直阻塞 不让服务器停止
            Thread.sleep(Integer.MAX_VALUE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server(8082);
    }
}
package com.io.aio;

import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;

public class Client implements Runnable{

    private AsynchronousSocketChannel asc ;

    public Client() throws Exception {
        asc = AsynchronousSocketChannel.open();
    }

    public void connect(){
        asc.connect(new InetSocketAddress("127.0.0.1", 8082));
    }

    public void write(String request){
        try {
            asc.write(ByteBuffer.wrap(request.getBytes())).get();
            read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void read() {
        ByteBuffer buf = ByteBuffer.allocate(1024);
        try {
            asc.read(buf).get();
            buf.flip();
            byte[] respByte = new byte[buf.remaining()];
            buf.get(respByte);
            System.out.println(new String(respByte,"utf-8").trim());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while(true){

        }
    }

    public static void main(String[] args) throws Exception {
        Client c1 = new Client();
        c1.connect();
        Client c2 = new Client();
        c2.connect();
        Client c3 = new Client();
        c3.connect();
        new Thread(c1, "c1").start();
        new Thread(c2, "c2").start();
        new Thread(c3, "c3").start();
        Thread.sleep(1000);
        c1.write("c1 aaa");
        c2.write("c2 bbbb");
        c3.write("c3 ccccc");
    }
}

=======================================================================

参考地址

BIO、NIO、AIO的深度解析于区别

尚硅谷java学习笔记——NIO(New IO)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值