11_tcp三次握手_四次挥手_多进程Server_多线程Server_tcp状态转换

本文详细讲解TCP三次握手(建立连接)、四次挥手(断开连接)的过程,包括SYN、ACK标志位,滑动窗口机制,以及多进程和多线程在client/server实现中的应用。同时涵盖通信细节和状态转换,以及实用的查看网络状态命令.
摘要由CSDN通过智能技术生成

代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

1. TCP三次握手

TCP: 面向连接的, 安全的, 流式传输协议
    - 面向连接:三次握手、四次挥手
    - 安全的:通信过程中会进行数据校验 -> 丢失之后会重传
    - 流式:双方每次操作的数据量可以不同

在这里插入图片描述

SYN: 发起一个--建立连接的请求
ACK: 确认、同意
FIN: 发起一个--断开连接的请求

如果这些标志位在协议中  == 1 => 请求被发起, 
                    == 0 => 没有发起这个请求

             
seq: 序号, 随机生成:随机数
ack: 确认序号, 确认tcp通信过程中接收的数据的量

在这里插入图片描述

/*
第一次握手: 客户端发起的
    - 将SYN设置为1, 向服务器发起连接请求
    - 生成了随机序号: seq=J
第二次握手:
    - 回复ACK=1, 接受了客户端的连接请求
    - 发送SYN=1, 服务器向客户端发起一个连接请求
    - ack=J+1, J(第一次握手生成的随机序号), 
               +1代表接收了一个字节(连接请求SYN)
    - seq=K, 服务器端生成一个随机序号K
第三次握手:
    - ACK=1, 客户端接受了服务器的连接请求
    - ack=K+1, K(第2次握手生成的随机序号), 
               +1代表接收了一个字节(连接请求SYN)
*/	

2. TCP四次挥手

// 四次挥手的时候, 首先断开连接的一方: 可以是客户端也可以是服务器
// 在程序中如何挥手: close();
// 过程: 假设客户端先断开连接
1. 客户端调用close函数, 在协议中 FIN 被设置为 1, 发送给服务器
2. 服务器接收到客户端断开连接的请求, 同意断开连接, 在tcp协议中 标志位 ACK = 1
3. 服务器端调用close函数, 在协议中 FIN 被设置为 1, 发送给客户端
4. 客户端接收到服务器断开连接的请求, 同意断开连接, 在tcp协议中 标志位 ACK = 1

在这里插入图片描述

3. TCP滑动窗口

1.滑动窗口是 TCP 中用于实现诸如:ACK确认、流量控制、拥塞控制的承载结构;
2.窗口理解为一块缓存就可以了;
3.滑动窗口的内存是在变化的 -> 存储的数据量在变化;
4.通信的双方都有滑动窗口;
服务器
    读缓冲区 -> 内核中的内存
        被填满之后, 阻塞客户端的发送(写缓冲区)
    写缓冲区 -> 内核中的内存
客户端
    读缓冲区 -> 内核中的内存
        被填满之后, 阻塞服务器的发送(写缓冲区)
    写缓冲区 -> 内核中的内存

这个图是一个单向的数据发送:
在这里插入图片描述

# 发送端 -> 图中对应的是写缓冲区
    - 白色的格子: 空闲的没有数据的空间
    - 灰色的格子: 代表已经发送出去的数据
    - 粉色的格子: 还没有发送出去的数据
        - 全部为粉色: 写阻塞
        - 对方的读缓冲区满了, 写被阻塞
# 接收端 -> 图中对应的是读缓冲区
    - 白色的格子: 空闲的没有数据的空间
    - 粉色格子: 接收到的数据, 但是这个数据还没有被处理
        - 写满之后, 会阻塞发送端的发送

在这里插入图片描述

fast sender -> 客户端
slow recv   -> 服务器
# mss: 最大的数据段大小 Maximum Segment Size  -> 一条数据的最大长度
# win: 滑动窗口
1. 发送SYN请求和服务器建立连接, 客户端的自己的滑动窗口大小4K(缓存4k数据), 
   客户端发送最大字节数1460,0是客户端生成的随机序号
2. 接受客户端连接请求, 发送SYN请求和客户端建立连接, 服务器的自己的滑动窗口
   大小6K(缓存6k数据), 服务器发送最大字节数1K, 8000是服务器生成的随机序号
3. 接受服务器连接请求,  客户端的自己的滑动窗口大小4K
4. 4-9步客户端不停的给服务器发送数据, 每次发送1k
   服务器的滑动窗口为6k, 接收了6k数据, 满了, 因此将发送端阻塞
5. 第10步: 客户端发送给服务器的6145个字节全部被接收, 服务器滑动窗口可用缓存为2k
6. 第11步: 客户端发送给服务器的6145个字节全部被接收, 服务器滑动窗口可用缓存为4k
7. 第12步: 客户端由给服务器发送了1k数据
8. 第13步: 客户端由给服务器发送了1k数据, 并且主动和服务器断开连接, 发送FIN
9. 14-16: 服务器接受了客户端断开连接的请求, 一直在处理滑动窗口缓存中的数据
10. 17步: 第三次挥手
11. 18步: 第四次挥手

4. TCP通信并发

在这里插入图片描述

1.1多进程 -> client/server 实现

// 多进程思路:父进程、子进程
/*
父进程: 一直不停的等待并接受客户端的连接
	- accept
	- 成功建立连接之后, fork()
子进程: 通信
	- read , write
*/ 
/*
多进程程序(有血缘关系)特点:
-地址空间被拷贝
	- 所有的数据被复制, 父子进程互不影响
	- 文件描述符表是共享的(父进程拷贝给子进程的那部分)
*/
// 服务器端的程序
int main()
{
    // 1. 创建监听的套接字
    int lfd = socket(af_int, sock_stream, 0);
    // 2. 绑定IP和端口
    bind(lfd, localaddr, sizeof(struct sockaddr));
    // 3. 设置监听
    listen(lfd, 128);
    while(1)
    {
        // 一直检测有没有客户端连接, 如果有建立连接
        int cfd = accept(lfd, cliaddr, &len);
        // 成功建立了连接, 创建子进程 -> 通信
        pid_t pid = fork();
        if(pid > 0)
        {
            // 父进程利用信号捕捉的回调函数, 回收资源pcb
        }
        else if(pid == 0)
        {
            // 子进程, 通信
            while(1)
            {
                read(cfd, buf, sizeof(buf));
                write(cfd, data, strlen(data));
                //跳出循环的条件
                ...
            }
        }
    }
}

02client.c

// 02client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
    //1.创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        exit(0);
    }
    //2.连接服务器
    struct sockaddr_in serverAddr; //服务器的相关数据
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(9999);
    inet_pton(AF_INET, "192.168.184.132", &serverAddr.sin_addr.s_addr);
    int ret = connect(fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if (ret == -1)
    {
        perror("connect");
        exit(0);
    }
    //3.通信
    while (1)
    {
        //3.1.发送数据
        char *p = "你好服务器...";
        write(fd, p, strlen(p) + 1);

        //3.2.接受数据
        char buf[1024];
        int len = read(fd, buf, sizeof(buf));
        if (len > 0)
        {
            printf("recv data:%s\n", buf);
        }
        else if (len == 0)
        {
            printf("server disconnect...\n");
            break;
        }
        else
        {
            perror("read");
            exit(0);
        }
        sleep(1);
    }
    close(fd);
    return 0;
}

02multi_process_server.c

// 02multi_process_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

//7.2.回调函数--回收子进程资源
void callback(int num)
{
    while (1)
    {
        pid_t pid = waitpid(-1, NULL, WNOHANG);
        if (pid == 0 || pid == -1)
        {
            //还有子进程 或者 回收完毕
            break;
        }
        printf("回收的子进程PID:%d\n", num);
    }
}
int main()
{
    //1.创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("socket");
        exit(0);
    }
    //2.绑定本地IP和端口
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;   //地址族协议
    server_addr.sin_port = htons(9999); //端口
    inet_pton(fd, "192.168.184.132", &server_addr.sin_addr.s_addr);
    int ret = bind(fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (ret == -1)
    {
        perror("bind");
        exit(0);
    }
    //3.设置监听
    ret = listen(fd, 128);
    if (ret == -1)
    {
        perror("listen");
        exit(0);
    }
    //7.1.注册信号捕捉
    //当子进程退出时,会给父进程发送SIGCHLD信号
    //我们捕捉,然后回收子进程资源
    struct sigaction act;
    act.sa_flags = 0; // 使用自定义回调函数
    sigemptyset(&act.sa_mask);
    act.sa_handler = callback; //自定义的回调函数
    sigaction(SIGCHLD, &act, NULL);

    while (1)
    {
        //主进程:等待客户端连接,并进行处理
        //4.阻塞等待客户端连接,并且接受建立连接
        struct sockaddr_in client_addr;
        int len1 = sizeof(client_addr);

        printf("等待客户端连接....\n");

        int cfd = accept(fd, (struct sockaddr *)&client_addr, &len1);
        if (cfd == -1)
        {
            if (errno == EINTR)
            {   //如果是信号导致的函数调用失败,就continue
                // 即:重新调用accept函数
                continue;
            }
            perror("accept");
            exit(0);
        }
        //5.如果连接成功,创建子进程
        pid_t pid = fork();
        if (pid == 0)
        {
            //子进程部分:通信
            //6.通信
            while (1)
            { //6.1从父进程复制过来的父进程用来监听连接请求的文件描述符,没用处
                close(fd);
                //6.2.接收数据部分
                char buf[24];
                int len = read(cfd, buf, sizeof(buf));
                if (len > 0)
                { //客户端IP
                    char clientIP[17];
                    inet_ntop(AF_INET,
                              &client_addr.sin_addr.s_addr,
                              clientIP,
                              sizeof(clientIP));
                    //客户端端口
                    unsigned short cliPORT = ntohs(client_addr.sin_port);
                    printf("recv data:%s,client IP:%s,Port:%d\n", buf, clientIP, cliPORT);
                    //6.3.发送数据
                    char msg[1024];
                    sprintf(msg, "你好客户端--%s:%d\n", clientIP, cliPORT);
                    write(cfd, msg, strlen(msg) + 1);
                }
                else if (len == 0)
                {
                    printf("client disconnect...\n");
                    break;
                }
                else
                {
                    perror("read");
                    break;
                }
            }
            close(cfd); //关闭子进程文件描述符
            exit(0);    //子进程退出
        }
        else if (pid > 0)
        {
            //父进程代码
            //父进程只负责监听连接请求,不负责通信
            //父进程中用来通信的文件描述符--没用处
            //所以:关闭父进程里的cfd
            close(cfd);
        }
    }
    //当有的子进程都关闭后,就关闭父进程监听的文件描述符
    close(fd);
    return 0;
}

1.2多线程 -> client/server 实现

// 多线程思路
/*
多线程: 主线程, 子线程
-主线程: 1个, 不停的接受客户端请求, 建立新连接 -> 创建一个子线程
-子线程: 接收数据、 发送数据
*/
/*
多线程程序线程之间:
- 共享用户区, 除了栈
	- 全局数据区 -> 全局, 静态变量
	- 堆区 -> malloc/new
- 每个线程都有自己的栈内存
*/
// 定义结构体, 存储要传送的数据
struct Info
{
    int cfd;	// 通信的文件描述符
    struct sockaddr_in addr;	// 客户端的IP和端口信息
};

// 定义全局变量
struct Info info[1024];

// 子线程对应的处理函数
void* callback(void* arg)
{
    //接受传进来的参数
    ...
    //传进来的数组元素,怎么置空
    ...
    // 通信 -> 客户端
    while(1)
    {
        // 接收数据
        read(cfd, buf, sizeof(buf));
        // 打印客户端的IP和端口
        // 发送数据
    }
}

// 服务器端的程序
int main()
{
    // 1. 创建监听的套接字
    int lfd = socket(af_int, sock_stream, 0);
    // 2. 绑定IP和端口
    bind(lfd, localaddr, sizeof(struct sockaddr));
    // 3. 设置监听
    listen(lfd, 128);
    
    while(1)
    {
        // 一直检测有没有客户端连接, 如果有建立连接
        int cfd = accept(lfd, cliaddr, &len);
      // 成功建立了连接, 创建子线程 -> 通信
		// 从数组中找一个没有被使用的元素
        struct Info *pinfo = info[x];
        pinfo->cfd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);
        pthread_create(&tid, NULL, callback, pinfo);
}

03client.c

// 01server.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>

int main()
{
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == lfd)
    {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in serverStruct;
    serverStruct.sin_family = AF_INET;
    serverStruct.sin_port = htons(8888);
    inet_pton(AF_INET, "192.168.184.134", &serverStruct.sin_addr.s_addr);
    int ret = connect(lfd, (struct sockaddr *)&serverStruct, sizeof(serverStruct));
    if (-1 == ret)
    {
        perror("accept");
        exit(-1);
    }

    char buf[1024];
    char buf2[] = "你好,服务端...";

    while (1)
    {
        write(lfd, buf2, strlen(buf2) + 1);

        int len = read(lfd, buf, sizeof(buf));
        if (len > 0)
        {
            printf("redc buf: %s\n", buf);
            sleep(1);
        }
        else if (len == 0)
        {
            printf("server disconnect...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }

    close(lfd);
    return 0;
}

03multi_pthread_server.c

// 01server.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <pthread.h>

typedef struct Info
{
    int cfd;
    struct sockaddr_in clientStruct;
} Info;
Info info[128];

void *callback(void *arg)
{
    Info *pInfo = (Info *)arg;

    while (1)
    {
        char buf[1024];
        int len = read(pInfo->cfd, buf, sizeof(buf));
        if (len > 0)
        {
            char clientIP[17];
            inet_ntop(AF_INET,
                      &pInfo->clientStruct.sin_addr.s_addr,
                      clientIP,
                      sizeof(clientIP));
            //客户端端口
            unsigned short cliPORT = ntohs(pInfo->clientStruct.sin_port);
            printf("recv data:%s,client IP:%s,Port:%d\n", buf, clientIP, cliPORT);

            char buf2[] = "你好,客户端...";
            write(pInfo->cfd, buf2, strlen(buf2) + 1);
        }
        else if (len == 0)
        {
            printf("client disconnect...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    close(pInfo->cfd);
    pInfo->cfd = -1;

    return NULL;
}

int main()
{
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == lfd)
    {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in serverStruct;
    serverStruct.sin_family = AF_INET;
    serverStruct.sin_port = htons(8888);
    inet_pton(AF_INET, "192.168.184.134", &serverStruct.sin_addr.s_addr);
    int ret = bind(lfd, (struct sockaddr *)&serverStruct, sizeof(struct sockaddr_in));
    if (-1 == ret)
    {
        perror("bind");
        exit(-1);
    }

    ret = listen(lfd, 5);
    if (-1 == ret)
    {
        perror("listen");
        exit(-1);
    }

    int lenInfo = sizeof(info) / sizeof(info[0]);
    for (int i = 0; i < lenInfo; ++i)
    {
        memset(&info[i].clientStruct, 0, sizeof(struct sockaddr_in));
        info[i].cfd = -1;
    }

    while (1)
    {
        struct sockaddr_in clientStruct;
        unsigned int len = sizeof(clientStruct);
        printf("等待客户端连接....\n");
        int cfd = accept(lfd, (struct sockaddr *)&clientStruct, &len);
        if (-1 == cfd)
        {
            if (errno == EINTR)
            {
                continue;
            }
            perror("accept");
            exit(-1);
        }

        struct Info *pInfo;
        for (int i = 0; i < lenInfo; ++i)
        {
            if (info[i].cfd == -1)
            {
                pInfo = &info[i];
                break;
            }
        }
        pInfo->cfd = cfd;
        memcpy(&pInfo->clientStruct, &clientStruct, sizeof(clientStruct));

        pthread_t tid;
        pthread_create(&tid, NULL, callback, (void *)pInfo);
        pthread_detach(tid);
    }
    close(lfd);
    return 0;
}

2.通信细节(伪代码)

// 服务器端
// 出现阻塞的情况
- accept
	有客户端连接解除阻塞
- read
	1.双方保持连接时:对应的读缓冲区(内核)有数据, 解除阻塞, 否则一直阻塞  
	2.通信的另一方断开了连接时:read返回0,接收数据的一方不阻塞
- write
	写缓冲区满了(内核), 阻塞, 否则不阻塞
while(1)
    {
        // 接收数据
        char buf[24];
        int len = read(cfd, buf, sizeof(buf));
        if(len > 0)
        {
            // 接收到了对方的数据
            printf("recv buf: %s\n", buf);
        }
        else if(len == 0)
        {
            printf("client disconnect ....\n");
            break;
        }
        else 
        {
            perror("read");
            break;
        }
        // 发送数据
        char *p = "你好, 客户端...\n";
        write(cfd, p, strlen(p)+1);
    }
// 阻塞函数在阻塞过程中突然被信号中断, 改变了原来的行为->去处理信号(信号优先级高)
// 当信号处理完毕之后, 再回到中断的位置之后,accept就不能再阻塞了, 因此返回-1
// 解决方案: 
//    当函数由于信号被中断, 只需要知道errno被设置为多少, 我们就可以做对应的处理
// 	  :errno == EINTR
while(1)
{
    ...
    ...
    int cfd = accept(fd, (struct sockaddr*)&cliaddr, &len);
    if(cfd == -1)
    {
        if(errno == EINTR)
        {
            // 重新调用accept
            continue;//!!!!!
        }
        perror("accept");
        exit(0);
    }
    ...
    ...
}

5.TCP状态转换总结

在这里插入图片描述

/*
三次握手:	连接过程
	还没有握手之前, 服务器端调用listen()函数, 状态: LISTEN
	第一次:
		客户端: 调用connect()函数, 发送了连接请求, 状态变成了: SYN_SNET
		服务器收到客户端的连接请求: 服务器状态 LISTEN -> SYN_RCVD
	第二次:
		服务器同意客户端连接,回复ack, 并且向客户发起连接请求
		客户端: 收到服务器的ack, 单向连接建立, 客户端状态: SYN_SNET -> ESTABLISHED
	第三次握手:
		服务器收到客户端的ack, 单向连接建立, 服务器状态: SYN_RCVD -> ESTABLISHED
		
四次挥手:	断开连接的过程
	第一次挥手:
		主动断开连接的一方: 调用close()函数, 状态变化: ESTABLISHED -> FIN_WAIT_1
		被动断开连接的一方: 没有调用close()函数, 只是收到了FIN, 
						 状态变化: ESTABLISHED -> CLOSE_WAIT
	第二次挥手:
		被动断开连接的一方: 回复一个ack, 状态仍为: CLOSE_WAIT
		主动断开连接的一方: 收到ack, 状态变化: FIN_WAIT_1 -> FIN_WAIT_2	
	第三次挥手:
		被动断开连接的一方: 调用close()函数, 发送FIN给对方, 状态: CLOSE_WAIT -> LAST_ACK
		主动断开连接的一方: 状态变化: FIN_WAIT_2 -> TIME_WAIT
	第四次挥手:
		主动断开连接的一方: 回复ack, 状态没变: TIME_WAIT
		被动断开连接的一方: 收到ACK,进程退出...
		
通信过程中: 状态是不变的, 状态: ESTABLISHED, 通信过程中, 必须是这种状态
*/

在这里插入图片描述

- 2MSL(Maximum Segment Lifetime)
  - 主动断开连接的一方最后的状态 -> TIME_WAIT
- 报文时间长度: 默认是2分钟, 实际是30- 因此主动断开连接的一方, 最后会等待1分钟(2msl), 之后才退出

当TCP连接主动关闭方接收到被动关闭方发送的FIN和最终的ACK后,连接的主动关闭方必须处于TIME_WAIT状态并持续2MSL时间。

这样就能够让TCP连接的主动关闭方在它发送的ACK丢失的情况下重新发送最终的ACK。

主动关闭方重新发送的最终ACK并不是因为被动关闭方重传了ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的FIN。事实上,被动关闭方总是重传FIN直到它收到一个最终的ACK。

半关闭

当TCP链接中A向B发送 FIN 请求关闭,另一端B回应ACK之后,并没有立即发送 FIN 给A, A方处于半连接状态(半开关),此时A可以接收B发送的数据,但是A已经不能再向B发送数据。

// tcp通信过程中, 服务器和客户端建立的是双向连接
// 	如果一方的连接断开(调用了close()函数), 这种状态就叫半关闭
// 	特点: 调用close()函数的一方, 只能接收数据, 不能发送数据
// 		在close之前如果dup/dup2-> 文件描述符复制了, 原来的关闭了, 复制出的fd还能双向通信

// 半关闭函数
#include <sys/socket.h>
int shutdown(int sockfd, int how);
	参数: 
		- sockfd: 要操作的文件描述符
		- how: 操作方式
			- SHUT_RD: 关闭读
			- SHUT_WR: 关闭写
			- SHUT_RDWR: 关闭读写	

1.查看网络相关信息的命令

$ netstat
	○ 参数:
		-a (all)显示所有选项,默认不显示LISTEN相关
		-p 显示建立相关链接的程序名
		-n 拒绝显示别名,能显示数字的全部转化成数字。
		-l 仅列出有在 Listen (监听) 的服务状态
		-t (tcp)仅显示tcp相关选项
		-u (udp)仅显示udp相关选项
robin@OS:~$ netstat -apn|grep 9999
  (Not all processes could be identified, non-owned process info
   will not be shown, you would have to be root to see it all.)
  tcp        0      0 0.0.0.0:9999            0.0.0.0:*               LISTEN      1401/server     
  robin@OS:~$ netstat -apn|grep 9999
  (Not all processes could be identified, non-owned process info
   will not be shown, you would have to be root to see it all.)
  tcp        0      0 0.0.0.0:9999            0.0.0.0:*               LISTEN      1401/server     
  tcp        0      0 127.0.0.1:37030         127.0.0.1:9999          ESTABLISHED 1422/client     
  tcp        0      0 127.0.0.1:9999          127.0.0.1:37030         ESTABLISHED 1401/server     
  robin@OS:~$ netstat -apn|grep 9999
  (Not all processes could be identified, non-owned process info
   will not be shown, you would have to be root to see it all.)
  tcp        1      0 127.0.0.1:37030         127.0.0.1:9999          CLOSE_WAIT  1422/client     
  tcp        0      0 127.0.0.1:9999          127.0.0.1:37030         FIN_WAIT2   -               
  robin@OS:~$ netstat -apn|grep 9999
  (Not all processes could be identified, non-owned process info
   will not be shown, you would have to be root to see it all.)
  tcp        0      0 127.0.0.1:9999          127.0.0.1:37030         TIME_WAIT   - 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值