通信协议

http的请求流程

访问目标主机的服务有两种方式:1.通过域名访问。2.通过ip访问。
因为IP不容易记忆,所以使用域名访问代替ip,使得用户更容易记住。

用户访问一个域名,会经过 DNS 解析(DNS(Domain Name System),它和 HTTP 协议一样是位于应用层的协议,主要提供域名到 IP 的解析服务),通过DNS把域名解析成ip返回给客户端,然后客户端通过ip访问目标主机。

加速静态内容访问速度的 CDN
在很多大型网站,会引入 CDN 来加速静态内容的访问, CDN(Content Delivery Network),表示的是内容分发网络。CDN 其实就是一种网络缓存技术,能够把一些相对稳定的资源放到距离最终用户较近的地方,一方面可以节省整个广域网的带宽消耗,另外一方面可以提升用户的访问速度,改进用户体验。我们一般会把静态的文件(图片、脚本、静态页面)放到 CDN 中。如果引入了 CDN,那么解析的流程可能会稍微复杂一点。阿里云就提供了 cdn 的功能。

HTTP协议通信原理

域名被成功解析以后,客户端和服务端之间,是怎么建立连接并且如何通信的呢?
常见的通信协议:tcp和udp这两种通信协议。
http 协议的通信是基于 tcp/ip 协议之上的一个应用层协议,应用层协议除了http还有FTP、DNS、SMTP、Telnet 等

涉及到网络协议,我们一定需要知道 OSI 七层网络模型和 TCP/IP 四层概念模型。

  • OSI七层网络模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
  • TCP/IP四层概念模型:应用层、传输层、网络层、数据链路层

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

请求发起过程中,在tcp/ip四层网络模型中所做的事情

当应用程序用 T C P 传送数据时,数据被送入协议栈中,然后逐个通过每一层直到被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些头信息(有时还要增加尾部信息)
在这里插入图片描述
客户端如何找到目标服务的?
在客户端发起请求的时候,我们会在数据链路层去组装目标机器的MAC地址,目标机器的mac地址怎么得到呢?这里就涉及到一个 ARP 协议,这个协议简单来说就是已知目标机器的ip,需要获得目标机器的 mac 地址。(发送一个广播消息,这个 ip 是谁的,请来认领。认领ip的机器会发送一个mac地址的响应)有了这个目标MAC地址,数据包在链路上广播,MAC的网卡才能发现,这个包是给它的。MAC的网卡把包收进来,然后打开IP包,发现IP地址也是自己的,再打开TCP包,发现端口是自己,也就是80端口,而这个时候这台机器上有一个nginx是监听80端口。于是将请求提交给nginx,nginx 返回一个网页。然后将网页发回请求的机器。然后层层封装,最后到数据链路层。因为来的时候有源MAC地址,返回的时候,源MAC就变成了目标MAC,再返给请求的机器。
为了避免每次都用ARP请求,机器本地也会进行ARP缓存。当然机器会不断地上线下线,IP也可能会变,所以 ARP的MAC地址缓存过一段时间就会过期。

接收端收到数据包以后的处理过程
当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议都要去检查报文首部中的协议标识,以确定接收数据的上层协议。
在这里插入图片描述
为什么有了MAC层还要走IP层呢?
mac地址是唯一的,那理论上在任何两个设备之间,我应该都可以通过mac地址发送数据,为什么还需要ip地址?
mac地址就好像个人的身份证号,人的身份证号和本人户口所在的城市,出生的日期有关,但是和人所在的位置无关,人是会移动的,知道一个人的身份证号,并不能找到它这个人,mac 地址类似,它是和设备的生产者,批次,日期之类的关联起来,知道一个设备的mac,并不能在网络中将数据发送给它,除非它和发送方的在同一个网络内。所以要实现机器之间的通信,我们还需要有ip地址的概念,ip地址表达的是当前机器在网
络中的位置,类似于城市名+道路号+门牌号的概念。通过 ip 层的寻址,我们能知道按何种路径在全世界任意两台Internet上的的机器间传输数据。

TCP/IP的分层管理

CP/IP协议按照层次分为4层:应用层、传输层、网络层、数据链路层。

分层负载
一次http请求过来,一定会从应用层到传输层,完成整个交互。只要是在网络上跑的数据包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。

二层负载均衡
二层负载是针对MAC,负载均衡服务器对外依然提供一个 VIP(虚 IP),集群中不同的机器采用相同 IP 地址,但是机器的MAC地址不一样。当负载均衡服务器接受到请求之后,通过改写报文的目标MAC地址的方式将请求转发到目标机器实现负载均衡

二层负载均衡会通过一个虚拟MAC地址接收请求,然后再分配到真实的MAC地址。

三层负载均衡
三层负载是针对 IP,和二层负载均衡类似,负载均衡服务器对外依然提供一个 VIP(虚 IP),但是集群中不同的机器采用不同的 IP 地址。当负载均衡服务器接受到请求之后,根据不同的负载均衡算法,通过 IP 将请求转发至不同的真实服务器。

三层负载均衡会通过一个虚拟 IP 地址接收请求,然后再分配到真实的 IP 地址。

四层负载均衡
四层负载均衡工作在 OSI 模型的传输层,由于在传输层,只有 TCP/UDP 协议,这两种协议中除了包含源 IP、目标 IP 以外,还包含源端口号及目的端口号。四层负载均衡服务器在接受到客户端请求后,以后通过修改数据包的地址信息(IP+端口号)将流量转发到应用服务器。

四层通过虚拟 IP + 端口接收请求,然后再分配到真实的服务器。

七层负载均衡
七层负载均衡工作在 OSI 模型的应用层,应用层协议较多,常用 http、radius、dns 等。七层负载就可以基于这些协议来负载。这些应用层协议中会包含很多有意义的内容。比如同一个Web 服务器的负载均衡,除了根据 IP 加端口进行负载外,还可根据七层的 URL、浏览器类别来决定是否要进行负载均衡。

七层通过虚拟的 URL 或主机名接收请求,然后再分配到真实的服务器。

TCP/IP通信协议原理

我们通过 TCP 协议在两台电脑建立网络连接之前要先发数据包进行沟通,沟通后再建立连接,然后才是信息的传输。而 UDP协议就类似于我们的校园广播,广播内容已经通过广播站播放出去了,你能不能听到,那就与广播站无关了。

TCP 握手协议
TCP 消息的可靠性首先来自于有效的连接建立,所以在数据进行传输前,需要通过三次
握手建立一个连接,所谓的三次握手,就是在建立 TCP 链接时,需要客户端和服务端总共发送 3 个包来确认连接的建立,在 socket 编程中,这个过程由客户端执行 connect 来触发。
在这里插入图片描述

  1. 第一次握手(SYN=1, seq=x):
    客户端发送一个TCP的SYN标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号X ,保存在包头的序列号(Sequence Number)字段里 。发送完毕后,客户端进入SYN_SEND 状态。
  2. 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
    服务器发回确认包(ACK) 应答 。 即SYN标志位 和ACK标志位均为1。服务器端选择自己ISN序列号,放到Seq域里,同时将确认序号
    (Acknowledgement Number)设置为客户的ISN加 1,即 X+1。发送完毕后,服务器端进入
    SYN_RCVD 状态。
  3. 第三次握手(ACK=1 ,seq=x+1,ACKnum=y+1):
    客户端再次发送确认包(ACK),SYN 标志位为 0,ACK 标志位为 1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据包发放完毕后 , 客户端进入ESTABLISHED 状态,当服务器端接收到这个包时,也进 入ESTABLISHED 状态,TCP握手结束。

SYN攻击
在三次握手过程中,Server 发送 SYN-ACK 之后,收到 Client 的 ACK 之前的 TCP 连接称为半连接(half-open connect),此时 Server 处于 SYN_RCVD 状态,当收到 ACK 后,Server转入 ESTABLISHED 状态。SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向Server 不断地发送 SYN 包,Server 回复确认包,并等待 Client 的确认,由于源地址是不存在的,因此,Server 需要不断重发直至超时,这些伪造的 SYN 包将产时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN 攻击时一种典型的 DDOS 攻击,检测 SYN 攻击的方式非常简单,即当 Server 上有大量半连接状态且源 IP 地址是随机的,则可以断定遭到 SYN 攻击了。

TCP 四次挥手协议
四次挥手表示 TCP 断开连接的时候,需要客户端和服务端总共发送 4 个包以确认连接的断开;客户端或服务器均可主动发起挥手动作(因为 TCP 是一个全双工协议),在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

  • 单工:数据传输只支持数据在一个方向上传输
  • 半双工:数据传输允许数据在两个方向上传输,但是在某一时刻,只允许在一个方向上传输,实际上有点像切换方向的单工通信
  • 全双工:数据通信允许数据同时在两个方向上传输,因此全双工是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。

在这里插入图片描述

  1. 第一次挥手(FIN=1,seq=x):
    假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为 1 的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。发送完毕后,客户端进入 FIN_WAIT_1 状态。
  2. 第二次挥手(ACK=1,ACKnum=x+1):
    服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
  3. 第三次挥手(FIN=1,seq=w):
    服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为 1。发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个 ACK。
  4. 第四次挥手(ACK=1,ACKnum=w+1):
    客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT 状态,等待可能出现的要求重传的 ACK 包。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入CLOSED状态。

为什么连接的时候是三次握手,关闭的时候却是四次握手?
三次握手是因为因为当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送
SYN+ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的。但是关闭连接时,当 Server 端收到 FIN 报文时,很可能并不会立即关闭 SOCKET(因为可能还有消息没处理完),所以只能先回复一个 ACK 报文,告诉 Client 端,“你发的 FIN 报文我收到了”。只有等到我 Server 端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。故需要四步握手

为什么TIME_WAIT状态需要经过 2MSL(最大报文段生存时间)才能返回到CLOSE状态?
虽然按道理,四个报文都发送完毕,我们可以直接进入 CLOSE 状态了,但是我们必须假
象网络是不可靠的,有可以最后一个 ACK 丢失。所以 TIME_WAIT 状态就是用来重发可能丢失的 ACK 报文。

Demo

tcp连接建立以后,就可以基于这个连接通道来发送和接受消息了,TCP、UDP 都是在基于Socket 概念上为某类应用场景而扩展出的传输协议,socket 是一种抽象层,应用程序通过它来发送和接收数据,就像应用程序打开一个文件句柄,把数据读写到磁盘上一样。使用 socket 可以把应用程序添加到网络中,并与处于同一个网络中的其他应用程序进行通信。不同类型的 Socket 与不同类型的底层协议簇有关联。主要的 socket 类型为流套接字(stream socket)和数据报文套接字(datagram socket)。 stream socket 把 TCP作为端对端协议(底层使用 IP 协议),提供一个可信赖的字节流服务。数据报文套接字(datagram socket)使用 UDP 协议(底层同样使用 IP 协议)提供了一种“尽力而为”的数据报文服务。
在这里插入图片描述

  1. demo1:实现一个简单的从客户端发送一个消息到服务端的功能
    Server:
public class ServerDemo1 {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=null;
        BufferedReader in=null;
        try {
            serverSocket = new ServerSocket(8080);
            Socket socket = serverSocket.accept();
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println(in.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (in != null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null){
                serverSocket.close();
            }
        }
    }
}

Client:

public class ClientDemo1 {
    public static void main(String[] args)
    {
        Socket socket=null;
        PrintWriter out=null;
        try {
            socket = new Socket("127.0.0.1",8080);
            out = new PrintWriter(socket.getOutputStream(),true);
            out.println("Hello, dillon");
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(out!=null){
                out.close();
            }
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  1. demo2:实现双向通信对话功能
    Server:
public class ServerDemo2 {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket=null;
            serverSocket = new ServerSocket(8080);
            Socket socket = null;
            try {
                socket = serverSocket.accept();
            }catch (IOException e) {
                System.out.println("Error." + e);
            }
            String line;
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter os = new PrintWriter(socket.getOutputStream());
            BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Client:" + is.readLine());
            line = sin.readLine();
            while (!line.equals("bye")){
                os.println(line);
                os.flush();
                System.out.println("Server:" + line);
                System.out.println("Client:" + is.readLine());
                line = sin.readLine();
            }
            os.close();
            is.close();
            socket.close();
            serverSocket.close();
        } catch (IOException e) {
            System.out.println("Error." + e);
        }
    }
}

在这里插入图片描述

Client:

public class ClientDemo2 {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1",8080);
            BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter os = new PrintWriter(socket.getOutputStream());
            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line;
            line = sin.readLine();
            while (!line.equals("bye")){
                os.println(line);
                os.flush();
                System.out.println("Client:" + line);
                System.out.println("Server:" + is.readLine());
                line = sin.readLine();
            }
            os.close();
            is.close();
            socket.close();
        } catch (IOException e) {
            System.out.println("Error." + e);
        }
    }
}

在这里插入图片描述
在前面的代码中,通过socket.accept 去接收一个客户端请求,accept 是一个阻塞的方法,意味着 TCP 服务器一次只能处理一个客户端请求,当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然在连接建立后可以向服务端发送数据,但是在服务端处理完之前的请求之前,却不会对新的客户端做出响应,这种类型的服务器称为“迭代服务器”。迭代服务器是按照顺序处理客户端请求,也就是服务端必须要处理完前一个请求才能对下一个客户端的请求进行响应。

  1. demo3:优化demo2,使其可以同时处理多个连接请求。
    Server:
public class ServerDemo3 {
    static ExecutorService executorService = Executors.newFixedThreadPool(20);

    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(8080);
            while (true){
                Socket socket = serverSocket.accept();
                System.out.println("服务端链接建立成功");
                executorService.execute(new SocketServer(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (serverSocket != null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class SocketServer implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        BufferedReader bufferedReader = null;
        BufferedWriter bufferedWriter = null;
        try {
            bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String s = null;
            s = bufferedReader.readLine();
            System.out.println("接收到客户端的信息:" + s);
            bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("我收到了信息\n");
            bufferedWriter.flush();

            bufferedReader.close();
            bufferedWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

Client:

public class ClientDemo3 {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("localhost" , 8080);
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("我是客户端33333发送了一条消息\n");
            bufferedWriter.flush();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String s= bufferedReader.readLine();
            System.out.println("服务端返回的数据:" + s);
            bufferedWriter.close();
            bufferedReader.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

虽然优化了 IO 的处理方式,但是,不管是线程池还是单个线程,线程本身的处理个数是有限制的,对于操作系统来说,如果线程数太多会造成 CPU 上下文切换的开销。因此这种方式不能解决根本问题。

解决方式把 BIO 变成 NIO

  1. demo4:BIO 变成 NIO
    Server:
public class NewIOServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);//设置为非阻塞
            serverSocketChannel.socket().bind(new InetSocketAddress(8080));
            while (true){
                SocketChannel socketChannel = serverSocketChannel.accept();
                if (socketChannel != null){
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    socketChannel.read(byteBuffer);
                    System.out.println(new String(byteBuffer.array()));
//                    写回数据
                    byteBuffer.flip();
                    socketChannel.write(byteBuffer);
                }else {
                    Thread.sleep(1000);
                    System.out.println("连接未就绪~!");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client:

public class NewClient {
    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 byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put("Hello ,I am Client".getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
//            读取数据
            byteBuffer.clear();
            int i = socketChannel.read(byteBuffer);
            if (i > 0 ){
                System.out.println("收到服务端的数据:" + new String(byteBuffer.array()));
            }else {
                System.out.println("没有收到数据");
            }

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

非阻塞仍然需要进程不断的轮询重试

  1. demo5:多路复用
    Server:
public class NewIOServer {
    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> selectionKeySet = selector.selectedKeys();
                Iterator<SelectionKey> iterable = selectionKeySet.iterator();
                while (iterable.hasNext()){
                    SelectionKey key = iterable.next();
                    iterable.remove();
                    if (key.isAcceptable()){//连接事件
                        handleAccept(key);
                    }else if (key.isReadable()){//读的就绪事件
                        handleRead(key);
                    }
                }
            }
        } 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 am NIO Server".getBytes()));
            socketChannel.register(selector,SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private static void handleRead(SelectionKey selectionKey){
        SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            socketChannel.read(byteBuffer);
            System.out.println("Server receive msg:" + new String(byteBuffer.array()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client:

public class NewClient {
    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> selectionKeySet = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeySet.iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isConnectable()){//连接事件
                        handleConnect(key);
                    }else if (key.isReadable()){
                        handleRead(key);
                    }
                }
            }
        } catch (IOException 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 am Nio Client".getBytes()));
        socketChannel.register(selector,SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey selectionKey) throws IOException {
        SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.out.println("client receive msg:" + new String(byteBuffer.array()));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值