NIO

本文转载https://blog.csdn.net/weixin_45676630/article/details/105717381

1.Socket模拟通信

通过一个案例来实现Socket模拟通信

Service

public class ServerSocketClass {
    // 启动一个服务端
    public static void main(String[] args) {
        final int DEFAULT_PORT = 8080;
        ServerSocket serverSocket = null;
        // 绑定一个监听端口
        try {
            serverSocket = new ServerSocket(DEFAULT_PORT);
            // 阻塞操作。等待客户端的链接
            Socket socket = serverSocket.accept();
            System.out.println("客户端:"+socket.getPort()+"已连接");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String clientStr = bufferedReader.readLine();// 获得客户端输入的信息
            System.out.println("收到客户端请求消息:"+clientStr);
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("服务端已收到消息\n");
            bufferedWriter.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client

public class ClientSocketClass {

    public static void main(String[] args) {
        final int DEFAULT_PORT = 8080;
        try {
            Socket socket = new Socket("localhost", DEFAULT_PORT);
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("客户端发送消息:client-01\n");
            bufferedWriter.flush();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String serviceStr = bufferedReader.readLine();
            System.out.println("收到服务端消息:"+serviceStr);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

先运行Service,再运行Client

在这里插入图片描述
在这里插入图片描述

2.Socket和ServerSocket

通信流程如下:
在这里插入图片描述

2.1 Socket类

套接字是网络连接的一个端点。套接字使得一个应用可以从网络中读取和写入数据。放在两个不同计算机上的两个应用可以通过连接发送和接受字节流。为了从你的应用发送一条信息到另一个应用,你需要知道另一个应用的IP地址和套接字端口。在Java里边,套接字指的是java.net.Socket类。要创建一个套接字,你可以使用Socket类众多构造方法中的一个。其中一个接收主机名称和端口号:

public Socket (java.lang.String host, int port)

一旦你成功创建了一个Socket类的实例,你可以使用它来发送和接受字节流。要发送字节流,你首先必须调用Socket类的getOutputStream方法来获取一个java.io.OutputStream对象。要发送文本到一个远程应用,你经常要从返回的OutputStream对象中构造一个java.io.PrintWriter对象。要从连接的另一端接受字节流,你可以调用Socket类的getInputStream方法用来返回一个java.io.InputStream对象。

2.2 ServerSocket类

Socket类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构造的套接字,现在,假如你想实施一个服务器应用,例如一个HTTP服务器或者FTP服务器,你需要一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时候会尝试去连接它。为了让你的应用能随时待命,你需要使用java.net.ServerSocket类。这是服务器套接字的实现。
ServerSocket和Socket不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个Socket实例来与客户端进行通信。
要创建一个服务器套接字,你需要使用ServerSocket类提供的四个构造方法中的一个。你需要指定IP地址和服务器套接字将要进行监听的端口号。通常,IP地址将会是127.0.0.1,也就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的IP地址被称为是绑定地址。服务器套接字的另一个重要的属性是backlog,这是服务器套接字开始拒绝传入的请求之前,传入的连接请求的最大队列长度。
其中一个ServerSocket类的构造方法如下所示:

public ServerSocket(int port, int backLog, InetAddress bindingAddress);

对于这个构造方法,绑定地址必须是java.net.InetAddress的一个实例。一种构造InetAddress对象的简单的方法是调用它的静态方法getByName,传入一个包含主机名称的字符串,就像下面的代码一样。

InetAddress.getByName("127.0.0.1");

下面一行代码构造了一个监听的本地机器8080端口的ServerSocket,它的backlog为1。

new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));

一旦你有一个ServerSocket实例,你可以让它在绑定地址和服务器套接字正在监听的端口上等待传入的连接请求。你可以通过调用ServerSocket类的accept方法做到这点。这个方法只会在有连接请求时才会返回,并且返回值是一个Socket类的实例。Socket对象接下去可以发送字节流并从客户端应用中接受字节流,就像前一节"Socket类"解释的那样。

3.网络通信协议分析

3.1 网络分层模型

在这里插入图片描述

3.2 OSI开放式互联网参考模型

3.2.1 第一层:物理层

机械、电子、定时接口通信信道上的原始bit流传输
定义硬件设备标准,用于计算机之间的数据传输,传输bit流。

解决两台物理机之间的通信需求,具体就是机器A往机器B发送bit流,机器B能收到这些比特流,这就是物理层要做的事情。物理层主要定义了物理设备的标准,如网线的类型,光纤的接口类型,各种传输介质的传输速率,主要作用是传输bit流,即我们所谓的0101二进制数据。将他们转换为电流强弱来进行传输,到达目的后再转换为0101的机器码,也就是我们常说的数模转换与模数转换。(数模转换就是将离散的数字量转换为连接变化的模拟量。与数模转换相对应的就是模数转换,模数转换是数模转换的逆过程。)这一层的数据叫做比特,网卡就是工作在这一层的。

3.2.2 第二层:数据链路层

物理寻址,同时将原始比特流转变为逻辑传输线路。
数据帧,对bit数据格式化,校验。目的是保障数据的可靠性。
在传输比特流的过程中,会产生错传,数据传输不完整的可能,因此数据链路层应运而生,数据链路层定义了如何格式化数据以进行传输,以及如何控制对物理介质的访问。这一层通常还提供错误检测和纠正以确保数据传输的可靠性。本层将比特数据组成了帧,其中交换机工作在这一层面。对帧解码,并根据帧中包含的信息把数据发送到正确的接收方,那随着网络节点的不断增加,点对点通信的时候是需要多个节点的,那么如何找到目标节点,如何选择最佳路径便成为了首要需求,此时便有了网络层。

3.2.3 第三层:网络层

控制子网的运行,如逻辑编辑、分组传输、路由选择。
IP寻址,通过IP连接网络上的计算机。
其主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方。网络层通过综合考虑,发送优先权。通过网络拥塞层度,服务质量以及可选路由的花费来决定从一个网络中节点A到另一个节点B的最佳路径。由于网络层处理并智能指导数据传送路由器连接网络各段,所以路由器属于网络层。此层的数据,我们称之为数据包。本层我们需要关注的协议为TCP/IP协议里面的IP协议。随着网络通信需求的进一步扩大,通信过程中需要发送大量的数据,如海量文件传输的,可能需要很长时间,而网络在通信的过程当中,会中断好多次,此时为了保证传输大量文件时的准确性,需要对发出去的数据进行切分,切割为一个一个的段落。

3.2.4 第四层:传输层

接收上一层数据,在必要的时候把数据进行分割,并将这些数据较为网络层,且保证这些数据段有效到达对端。建立主机端对端的链接。
传输层解决了主机间的数据传输,数据的传输可以是不同网络的,并且传输层解决了传输质量的问题,该层称之为OSI模型中最重要的一层。传输协议同时进行流量控制,或是基于可接收方接收数据的快慢程度规定适当的发送数率。除此之外,传输层按照网络能处理的最大尺寸,将较长的数据包进行强制分割,例如:以太坊无法接收大于1500字节的数据包。发送方节点的传输层将数据分割成较小的数据片,同时对每一数据片安排一个序列号,以便数据到达接收方的传输层时,能以正确的顺序重组。该过程即称为排序。传输层中需要我们关注的协议有TCP/IP的TCP协议和UDP协议。

3.2.5 第五层:会话层

不同机器(如windows linux)上的用户之间建立及管理会话。管理不同设备之间的通信

3.2.6 第六层:表示层

对应用层数据编码和数据格式转换,保障不同设备之间的通信。
信息的语法语义以及它们的关联,如加密解密、转换翻译,压缩解压缩。

3.2.7 第七层:应用层

提供应用接口,为用户直接提供各种网络服务

4. 请求发送过程

在这里插入图片描述

5.请求接收过程

在这里插入图片描述

6.深入分析NIO

6.1 阻塞和非阻塞

同步: 客户端发起一个请求,这个请求需要同步等待结果,在结果返回之前,这个客户端一直处于阻塞状态。
阻塞: 同步代表一个通信机制,阻塞指的是在同步机制下结果返回之前客户端的状态。

在这里插入图片描述

异步: 客户端发起一个请求后,不需要一直等待,可以继续其他操作,然后服务端异步返回请求结果。
非阻塞: 异步代表一个通信机制,非阻塞指的是在异步机制下结果返回之前客户端的状态。
在这里插入图片描述

6.2 深入分析5种IO模型

6.2.1 阻塞IO

在这里插入图片描述

6.2.2 非阻塞IO

在这里插入图片描述

6.2.3 IO复用

在这里插入图片描述

6.2.4 信号驱动

在这里插入图片描述

6.2.5异步IO

在这里插入图片描述

6.3 NIO的概述及应用

6.3.1 NIO的新特性

在这里插入图片描述

6.3.2 核心组件

  • 通道(Channel): Java NIO数据来源,可以是网络,也可以是本地磁盘
  • 缓冲区(Buffer):数据读写的中转区,后续会单独讲
  • 选择器(Selectors):异步IO的核心类,可以实现异步非阻塞IO,一个selectors可以管理多个通道Channel

6.3.3 IO和NIO的区别

类型面向操作区域处理数据(字节流&字符流)IO阻塞/非阻塞
Java IO直接面向最初的数据源每次读取时 = 读取所有字节/字符,无缓存;无法前后移动读取流中的数据当一个线程在读/写时:当数据被完全读取/写入完毕前&数据未准备好时,线程不能做其他任务,只能一直等待,直到数据准备好后继续读取/写入,即阻塞。;当线程处于活跃状态时&外部未准备好时,则阻塞。
Java NIO面向缓冲区先将数据读取到缓存区;可在缓冲区前后移动流数据当一个线程向某通道发送请求要求读/写时,当数据被完全读取/写入完毕前&数据未准备好时,线程可以做其他任务(控制其他通道),直到数据准备好后再切换回该通道,继续读取/写入,即选择器(selector)的使用;外部 准备好时才唤醒线程 ,则不会阻塞。

6.3.4 Demo

	public static void main(String[] args) {
        // 实现一个文件复制
        try {
            FileInputStream fis = new FileInputStream(new File("G:/test.txt"));
            FileOutputStream fos = new FileOutputStream(new File("G:/test-10.txt"));

            FileChannel fin = fis.getChannel();
            FileChannel fout = fos.getChannel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);
            fin.read(buffer); // 读取数据到缓冲区
            buffer.flip(); // 表示从读转化为写
            fout.write(buffer);
            buffer.clear(); // 重置缓冲区

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

6.3.4 NIO详解Channel和Buffer

6.3.4.1 什么是Channel

什么是Channel?由java.nio.channels包定义的,Channel表示IO源与目标打开的连接,Channel类似于传统的“流”,只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互。通道主要用于传输数据,从缓冲区的一侧传到另一侧的实体(如文件、套接字…),反之亦然;通道是访问IO服务的导管,通过通道,我们可以以最小的开销来访问操作系统的I/O服务;顺便说下,缓冲区是通道内部发送数据和接收数据的端点。

在这里插入图片描述

6.3.4.2 Channel的实现

  • F i l e C h a n n e l: 从文件中读写数据
  • D a t a g r a m C h a n n e l: 通过U D P协议读写网络中的数据
  • S o c k e t C h a n n e l: 通过T C P协议读写网络中的数据
  • S e r v e r S o c k e t C h a n n e l: 监听一个T C P连接,对于每一个新的客户端连接都会创建一个S o c k e t C h a n n e l。

6.3.4.3 什么是Buffer

b u f f e r是一个对象,它包含了需要写入或者刚读出的数据,最常用的缓冲区类型是 B y t e B u f f e r

在这里插入图片描述

6.3.5 NIO Read Demo

	public static void main(String[] args) {
        try(FileInputStream inputStream = new FileInputStream("G:/test.txt");){
            // 针对本地磁盘的文件进行操作
            FileChannel fileChannel = inputStream.getChannel();
            // 读取数据,分配缓冲区大小 与下面这种写法效果一样
            // byte[] bytes = new byte[1024];
            // ByteBuffer buffer = ByteBuffer.wrap(bytes);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int read = fileChannel.read(buffer);
            System.out.println(new String(buffer.array()));

        }catch (Exception e){
            e.printStackTrace();
        }
    }

6.3.6 NIO Write Demo

	public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("G:/test-nio-write.txt")){
            FileChannel channel = fos.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 往buffer写数据
            buffer.put("NIO Wirte Test Demo".getBytes()); // 往缓冲区写数据
            buffer.flip(); // 读模式转换为写模式
            channel.write(buffer);// 读取buffer数据

        }catch (Exception e){
            e.printStackTrace();
        }
    }

6.3.7 Buffer的本质

缓冲区本质上是一块可以写入数据,以及从中读取数据的内存,实际上也是一个byte[]数据,只是在NIO中被封装成了NIO Buffer对象,并提供了一组方法来访问这个内存块,要理解buffer的工作原理,需要知道几个属性

  • capacity // ByteBuffer.allocate(10);
  • position
  • limit

初始状态下,limit和capacity都是8,而position=0,如果当前是从通道读取数据到缓冲区,那么下一个读取的数据就会存储到0位置。
在这里插入图片描述

第一次读取数据,读了四个字节,position指向4

在这里插入图片描述

第二次读取了两个字节,position指向6

在这里插入图片描述

读完数据之后,调用flip方法切换为写模式。调用之后,如下图:

在这里插入图片描述

数据写出,写完之后,position会不断的移动

在这里插入图片描述

当执行完clear方法,缓冲区又回到了初始的状态。

6.3.8 零拷贝

IO的通信原理:

在这里插入图片描述

IO流程:

  • 内核给磁盘控制器发命令说:我要读磁盘上的某某块磁盘块上的数据
  • 在DMA的控制下,把磁盘上的数据读入到内核缓冲区
  • 内核把数据从内核缓冲区复制到用户缓冲区
byte[] b = new byte[1024];

while((read = inputStream.read(b))>=0){
	total = total + read;
	// TODO
}

mmap原理:
在这里插入图片描述

	public static void main(String[] args) throws IOException {
        FileChannel inChannel = FileChannel.open(Paths.get("G:/test.txt"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("G:/test-mmap.txt"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMap = outChannel.map(FileChannel.MapMode.READ_WRITE,0,inChannel.size());
        byte[] bytes = new byte[inMap.limit()];
        inMap.get(bytes);
        outMap.put(bytes);
        inChannel.close();
        outChannel.close();
    }

IO拷贝流程:

在这里插入图片描述

零拷贝流程:

在这里插入图片描述

Demo:

public class ZeroCopyServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            SocketChannel socketChannel = serverSocketChannel.accept();
            ByteBuffer buffer = ByteBuffer.allocate(2048);
            int r = 0;
            FileChannel fileChannel = new FileOutputStream("G:/02-技术分析之Spring框架的概述_copy.flv").getChannel();
            while (r != -1) {
                r = socketChannel.read(buffer);
                buffer.flip();
                fileChannel.write(buffer);
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {

        }
    }
}
public class ZeroCopyClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            FileChannel fileChannel = new FileInputStream("G:/02-技术分析之Spring框架的概述_.flv").getChannel();
            // tf表示总的字节数
            int position = 0;
            long size = fileChannel.size();
            while (size > 0) {
                long tf = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
                if (tf > 0) {
                    position += tf;
                    size -= tf;
                }
            }
            System.out.println("总的数据传输字节数:" + position);
            socketChannel.close();
            fileChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6.3.9 SocketChannel和ServeSocketChannel

demo:

public class NIOSocketServer01 {
    public static void main(String[] args) {
        try {
            // 可以支持两种模式:阻塞,非阻塞
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //  修改阻塞模式 start
            serverSocketChannel.configureBlocking(false); // 默认是true
            //  修改阻塞模式 end
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));

            while (true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel != null) {
                    // 如果代码进入这个位置,说明有连接进来
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer);
                    System.out.println(new String(buffer.array()));
                    // 再把消息写回客户端
                    Thread.sleep(10000);
                    buffer.flip();
                    socketChannel.write(buffer);
                }else{
                    Thread.sleep(1000);
                    System.out.println("没有客户端连接进来");
                }

            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class NIOSocketClient01 {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false); // 把客户端设置为非阻塞
            // 非阻塞模式下,后面的代码并不一定是等到建立连接之后再往下执行
            socketChannel.connect(new InetSocketAddress("localhost",8080));
            if(socketChannel.isConnectionPending()){
                socketChannel.finishConnect();
            }
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("Hello,I'm SockectChannel Client01".getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            // 读取服务端返回的数据
            buffer.clear();
            int r = socketChannel.read(buffer); //非阻塞模式下,这里不阻塞
            if(r>0) {
                System.out.println("收到服务端的消息:" + new String(buffer.array()));
            }else{
                System.out.println("服务端的数据还未返回");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6.3.10 选择器 Selector

Selector(选择器,多路复用器)是Java NIO中能够检测一到多个NIO通道,是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

在这里插入图片描述

Demo:

public class NIOSelectorServerDemo {

    static Selector selector; // 多路复用器

    public static void main(String[] args) {
        try {
            selector = Selector.open(); // 创建一个多路复用器
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false); // 多路复用器下,这个必须设置为非阻塞
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            // 监听连接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove(); // 避免重复处理
                    if (selectionKey.isAcceptable()) {
                        handleAccept(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        handleRead(selectionKey);
                    }
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleAccept(SelectionKey selectionKey) {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.write(ByteBuffer.wrap("Hello Client , I'm NIO Server With Selector".getBytes()));
            socketChannel.register(selector,SelectionKey.OP_READ);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleRead(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        socketChannel.read(buffer);
        System.out.println("server receive Msg:"+new String(buffer.array()));

    }
}
public class NIOSelectorClientDemo {

    static Selector selector;

    public static void main(String[] args) {
        try {
            selector = Selector.open();
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("localhost", 8080));
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    if (selectionKey.isConnectable()) {
                        handleConnect(selectionKey);
                    } else if (selectionKey.isReadable()) {
                        handleRead(selectionKey);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void handleConnect(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        if (socketChannel.isConnectionPending()){
            socketChannel.finishConnect();
        }
        socketChannel.configureBlocking(false);
        socketChannel.write(ByteBuffer.wrap("Hello Server ,I'm NIO Client".getBytes()));
        socketChannel.register(selector,SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        socketChannel.read(buffer);
        System.out.println("Server receive Msg:"+new String(buffer.array()));
    }
}

总结

附加知识

mmap()

系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。

阻塞方法的优化

可以将阻塞调用过程交给线程处理。如socket的读写,可以将socket交给线程处理。通过线程的run方法来处理socket的读写逻辑。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值