Linux——FTP云盘项目

项目介绍:

这是一个基于 C 语言实现的简易 FTP 云盘项目,采用经典的客户端 - 服务器(C/S)架构,通过网络套接字(Socket)进行通信,实现了基本的文件传输与目录操作功能。以下是对该项目的详细介绍:

一、整体架构与通信方式

  • C/S 架构:项目分为客户端和服务器端两部分。服务器端负责监听网络端口,接收客户端连接请求并处理客户端命令;客户端负责与服务器建立连接,向服务器发送操作命令(如文件上传、下载、目录切换等)。
  • 网络通信:基于 TCP 协议,使用 socket 系列函数(如 socketconnectbindlistenaccept 等)实现网络连接。服务器端通过 bind 绑定指定 IP 和端口,listen 监听连接,accept 接受客户端连接;客户端通过 connect 连接到服务器。

二、核心功能模块

  1. 命令解析与分发(change 和 choosecmd 函数)
    • change 函数:分析客户端发送的命令字符串(如 "lls""ps""cd""get""put" 等),返回不同的标识值(如 123 等),用于区分命令类型。
    • choosecmd 函数:根据 change 函数返回的标识值,通过 switch-case 分支逻辑,执行对应的操作(如调用系统命令、处理文件传输、切换目录等)。
  2. 文件上传(putmessage 函数)
    • 服务器端通过 read 函数从客户端连接套接字读取数据,然后使用 open 函数创建(或打开)文件,再通过 write 函数将数据写入文件,实现文件上传功能。
  3. 文件下载与目录操作
    • 文件下载:当处理 get 命令时,先检查文件是否存在(access 函数),若存在则打开文件,读取文件内容并通过 write 函数发送给客户端。
    • 目录切换(cd 命令):通过 chdir 函数实现服务器端目录切换。
  4. 特殊命令处理
    • lls 命令:通过 popen 函数执行类似本地终端 ls 的命令(列出目录内容),并将结果返回给客户端。
    • ps 命令:直接调用 system("ps") 执行系统命令(可能用于查看进程,不过在 FTP 场景下此命令关联性稍弱,可能是测试或扩展功能)。

三、关键代码流程

  • 服务器端
    1. 创建套接字 s_fd,绑定地址(s_addr)并监听端口。
    2. 循环调用 accept 接受客户端连接,得到 c_fd
    3. 使用 fork 创建子进程,子进程中循环读取客户端命令(Readbuf),调用 choosecmd 处理命令,实现与客户端的交互。

函数介绍

socket

socket函数是用于创建网络套接字的函数,在网络编程中起着至关重要的作用。

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明

  • domain:指定套接字的协议族。常见的取值有AF_INET(用于 IPv4 协议)、AF_INET6(用于 IPv6 协议)、AF_UNIX(用于本地进程间通信)等。
  • type:指定套接字的类型。常见的类型有SOCK_STREAM(流套接字,用于 TCP 协议,提供可靠的、面向连接的数据传输)、SOCK_DGRAM(数据报套接字,用于 UDP 协议,提供无连接的、不可靠的数据传输)、SOCK_RAW(原始套接字,允许对底层网络协议进行直接访问,通常用于高级网络编程,如实现自定义协议等)。
  • protocol:指定使用的协议。通常设置为 0,由系统根据domaintype自动选择合适的协议。例如,当domainAF_INETtypeSOCK_STREAM时,系统会自动选择 TCP 协议;当domainAF_INETtypeSOCK_DGRAM时,系统会自动选择 UDP 协议。

返回值

  • 成功时,返回一个标识新创建套接字的文件描述符(非负整数)。后续对套接字的操作(如绑定地址、监听连接、发送和接收数据等)都通过这个文件描述符来进行。
  • 失败时,返回 -1,并设置errno变量以指示错误原因。常见的错误原因包括EACCES(权限不足)、EAFNOSUPPORT(不支持指定的地址族)、EPROTONOSUPPORT(不支持指定的协议)等。

struct sockaddr_in 是在 C 语言网络编程里用于表示 IPv4 地址信息的结构体,它在<netinet/in.h>头文件中被定义。此结构体在 TCP 和 UDP 编程里十分关键,可用于存储和传递 IP 地址与端口号等信息。

#include <netinet/in.h>

struct sockaddr_in {
    short            sin_family;   // 地址族,一般为 AF_INET,表示 IPv4
    unsigned short   sin_port;     // 端口号,使用网络字节序
    struct in_addr   sin_addr;     // IPv4 地址结构体
    char             sin_zero[8];  // 填充字节,使 struct sockaddr_in 和 struct sockaddr 长度相同
};

struct in_addr {
    unsigned long s_addr;  // 32 位 IPv4 地址,使用网络字节序
};

各成员解释

  1. sin_family:该成员表明地址族,对于 IPv4 地址,通常设置为 AF_INET。此值告知系统使用的是哪种地址格式。
  2. sin_port:这是端口号,需以网络字节序存储。在 C 语言中,可使用 htons 函数把主机字节序转换为网络字节序。
  3. sin_addr:这是一个 struct in_addr 类型的结构体,用于存储 32 位的 IPv4 地址,且要以网络字节序存储。可以使用 inet_pton 函数把点分十进制的 IP 地址转换为网络字节序的二进制形式。
  4. sin_zero:这是一个长度为 8 的字符数组,其作用是填充结构体,使 struct sockaddr_in 和 struct sockaddr 长度相同。在使用时,通常将其初始化为 0

htons 

htons 函数的作用

htons 是 “Host to Network Short” 的缩写,其功能是把 16 位的无符号整数从主机字节序转换为网络字节序。由于不同计算机系统可能采用不同的字节序,为了保证数据在网络传输中的一致性,在发送数据前需要将相关的 16 位整数(如端口号)从主机字节序转换为网络字节序;在接收数据后,若需要处理这些 16 位整数,再将其从网络字节序转换为主机字节序

字节序概念

字节序是指多字节数据在内存中存储的顺序,主要分为大端字节序(Big-Endian)和小端字节序(Little-Endian):

  • 大端字节序:数据的高位字节存于内存的低地址处,低位字节存于内存的高地址处。这种字节序也被称作网络字节序,在网络传输中被广泛使用。
  • 小端字节序:数据的低位字节存于内存的低地址处,高位字节存于内存的高地址处。许多计算机系统(如 x86 架构)采用小端字节序。

使用场景

htons 函数通常在以下场景中使用:

  • 设置套接字的端口号:在进行网络编程时,当你要绑定一个套接字到特定的端口或者连接到远程服务器的特定端口时,需要使用 htons 函数将端口号从主机字节序转换为网络字节序。
  • 网络数据传输:在发送包含 16 位整数的数据时,需要使用 htons 函数确保数据以网络字节序发送。

inet_aton 

inet_aton 函数是 C 语言网络编程中用于处理 IPv4 地址转换的重要函数,它能将点分十进制表示的 IPv4 地址转换为网络字节序的二进制形式。

函数原型

inet_aton 函数在 <arpa/inet.h> 头文件中声明,其原型如下:

#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);

参数说明

  • cp:这是一个指向以空字符结尾的字符串的指针,该字符串表示点分十进制格式的 IPv4 地址,例如 "192.168.1.1"
  • inp:这是一个指向 struct in_addr 结构体的指针,用于存储转换后的二进制形式的 IPv4 地址。

返回值

  • 若转换成功,inet_aton 函数返回非零值。
  • 若转换失败(例如输入的字符串不是有效的点分十进制 IPv4 地址),则返回 0。

使用场景

inet_aton 函数主要用于以下场景:

  • 设置套接字地址:在网络编程中,当你需要将用户输入的点分十进制 IP 地址转换为二进制形式,以便在 struct sockaddr_in 结构体中使用时,可以使用该函数。
  • 网络配置:在进行网络配置时,可能需要将配置文件中存储的点分十进制 IP 地址转换为二进制形式,以便进行网络连接或路由设置。

connect

在网络编程里,connect 函数主要用于客户端程序,它的作用是尝试把客户端的套接字连接到指定的服务器地址和端口。

函数原型

connect 函数在 <sys/socket.h> 头文件中声明,其原型如下:

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明

  • sockfd:这是一个整型变量,代表客户端已经创建好的套接字描述符。该套接字描述符是通过之前调用 socket 函数创建得到的。
  • addr:这是一个指向 struct sockaddr 结构体的指针,其中存储着要连接的服务器的地址信息。不过在实际使用时,常常会使用 struct sockaddr_in(用于 IPv4)或 struct sockaddr_in6(用于 IPv6)结构体来存储地址信息,然后再将其强制转换为 struct sockaddr * 类型。
  • addrlen:这是一个 socklen_t 类型的变量,它表示 addr 所指向的结构体的长度。

返回值

  • 若连接成功,connect 函数返回 0。
  • 若连接失败,返回 -1,并且会设置 errno 变量来指示具体的错误原因。常见的错误原因包括 ECONNREFUSED(服务器拒绝连接)、ETIMEDOUT(连接超时)等。

使用场景

connect 函数主要用于客户端程序,当客户端想要与服务器建立连接时,就会调用该函数。在建立连接之后,客户端和服务器之间就可以进行数据的传输。

malloc

malloc 是 C 语言标准库中用于动态内存分配的函数,它允许程序在运行时根据需要分配内存,而不是在编译时就确定固定的内存大小。

函数原型

void *malloc(size_t size);

参数说明

  • size:需要分配的内存字节数,类型为 size_t,这是一个无符号整数类型,通常用于表示对象的大小。

返回值

  • 如果内存分配成功,malloc 会返回一个指向新分配内存块起始位置的指针,该指针类型为 void *,意味着可以将其转换为任何其他类型的指针。
  • 如果内存分配失败(例如系统没有足够的可用内存),malloc 会返回 NULL

popen

popen 函数是 C 标准库中的一个函数,它主要用于创建一个管道,然后执行一个 shell 命令,并返回一个文件指针,通过这个文件指针可以对命令的输入或输出进行操作。

函数原型

#include <stdio.h>

FILE *popen(const char *command, const char *type);

参数解释

  • command:这是一个字符串,它包含了要执行的 shell 命令。例如,"ls -l" 就是一个常见的命令,用于列出当前目录下的文件和文件夹的详细信息。
  • type:这也是一个字符串,它指定了管道的打开方式,有两种取值:
    • "r":表示以读模式打开管道。在这种模式下,popen 函数会执行 command 命令,并将该命令的标准输出连接到返回的文件指针上,这样就可以从这个文件指针中读取命令的输出结果。
    • "w":表示以写模式打开管道。在这种模式下,popen 函数会执行 command 命令,并将该命令的标准输入连接到返回的文件指针上,这样就可以向这个文件指针中写入数据,这些数据会作为命令的输入。

返回值

  • 如果 popen 函数调用成功,它会返回一个指向 FILE 对象的指针,这个指针可以用于后续的文件操作,如 freadfwrite 等。
  • 如果调用失败,它会返回 NULL,并且会设置 errno 变量来指明具体的错误类型。

使用场景

  • 获取命令输出:当你需要获取某个 shell 命令的输出结果并在程序中进行处理时,可以使用 popen 函数以读模式打开管道。例如,获取系统的磁盘使用情况、进程列表等信息。
  • 向命令输入数据:当你需要向某个命令提供输入数据时,可以使用 popen 函数以写模式打开管道。例如,向一个脚本程序传递参数。

fread

fread 函数是 C 标准库中用于从文件流中读取数据的函数,它在文件操作和数据处理方面有着广泛的应用。

函数原型

c

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

参数解释

  • ptr:这是一个指向用于存储读取数据的内存块的指针。也就是说,fread 函数会将从文件流中读取的数据存储到这个指针所指向的内存区域。
  • size:表示每个数据项的大小,以字节为单位。例如,如果你要读取整数,size 可以设置为 sizeof(int);如果要读取字符,size 可以设置为 sizeof(char)
  • nmemb:表示要读取的数据项的数量。fread 函数会尝试读取 nmemb 个大小为 size 字节的数据项。
  • stream:这是一个指向 FILE 对象的指针,表示要从哪个文件流中读取数据。FILE 对象通常是通过 fopen 函数打开文件后返回的。

返回值

fread 函数返回实际成功读取的数据项的数量。这个返回值可能小于 nmemb,有以下几种情况:

  • 到达文件末尾:当读取到文件末尾时,fread 函数会停止读取,此时返回值可能小于 nmemb
  • 发生错误:如果在读取过程中发生错误,fread 函数也会停止读取,返回值同样可能小于 nmemb。可以通过 ferror 函数检查是否发生了错误,通过 feof 函数检查是否到达了文件末尾。

使用场景

  • 读取二进制文件:当需要从二进制文件中读取数据时,fread 函数非常有用。例如,读取图像文件、音频文件等。
  • 批量读取数据:如果需要一次性读取多个相同类型的数据项,可以使用 fread 函数。例如,从文件中读取一组整数或结构体。

access

access 函数是 C 语言标准库中的一个系统调用函数,它主要用于检查调用进程是否对指定的文件或目录拥有某种权限,或者检查文件是否存在。下面将从不同方面为你详细介绍该函数。

头文件和函数原型

在使用 access 函数前,需要包含 <unistd.h> 头文件,其函数原型如下:

#include <unistd.h>
int access(const char *pathname, int mode);

参数说明

  • pathname:这是一个指向以空字符结尾的字符串的指针,该字符串表示要检查的文件或目录的路径。路径可以是绝对路径(如 /home/user/documents/file.txt),也可以是相对路径(如 ./file.txt)。
  • mode:这是一个整数,用于指定要检查的权限或状态。它可以是以下常量的一个或多个按位或组合:
    • R_OK:检查调用进程是否有读取该文件或目录的权限。
    • W_OK:检查调用进程是否有写入该文件或目录的权限。
    • X_OK:检查调用进程是否有执行该文件或目录的权限(对于目录来说,意味着是否可以进入该目录)。
    • F_OK:检查文件或目录是否存在。

返回值

  • 如果所有指定的权限检查都通过或者文件 / 目录存在(根据 mode 参数),access 函数返回 0
  • 如果有任何一个权限检查失败或者文件 / 目录不存在,函数返回 -1,并且会设置全局变量 errno 来指示具体的错误类型。常见的 errno 值如下:
    • EACCES:权限检查失败,调用进程没有所需的权限。
    • ENOENT:指定的文件或目录不存在。
    • ELOOP:在解析路径名时遇到了过多的符号链接。
    • ENAMETOOLONG:路径名太长。
    • ENOTDIR:路径中的某个部分不是一个目录。

chdir

chdir 是用于改变当前工作目录的函数,在不同环境下有一定差异,以下是详细介绍:

1. C 语言(POSIX 系统,如 Linux、macOS)

  • 头文件#include <unistd.h>
  • 函数原型int chdir(const char *path);
  • 功能:将当前进程的工作目录更改为 path 指向的目录,path 可以是绝对路径(如 /home/user/documents)或相对路径(如 ../target_dir)。
  • 返回值:成功返回 0,失败返回 -1,并设置 errno 错误码(如 ENOENT:路径不存在;EACCES:无访问权限)。

示例

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 尝试切换到 /tmp 目录
    if (chdir("/tmp") == 0) {
        printf("切换目录成功,当前目录:%s\n", getcwd(NULL, 0)); // 需包含 <unistd.h>
    } else {
        perror("切换目录失败");
        exit(1);
    }
    return 0;
}

 atoi

atoi 函数是 C 标准库中的一个字符串转换函数,用于将字符串转换为整数。以下是对该函数的详细介绍:

1. 函数原型

#include <stdlib.h>
int atoi(const char *nptr);

  • 参数 nptr:指向要转换的字符串的指针。

2. 功能描述

atoi 函数会从字符串 nptr 的起始位置开始,忽略前导的空白字符(如空格、制表符 \t 等),然后读取可选的正负号(+ 或 -),接着读取数字字符并将其转换为整数,直到遇到非数字字符(如字母、符号等)时停止转换。最终返回转换后的整数值

项目代码:

client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>

// 根据输入命令返回不同的标识值
int change(char cmd[128]) {
    if (strcmp("lls", cmd) == 0) {
        return 1;
    } else if (strcmp("ls", cmd) == 0) {
        return 2;
    } else if (strcmp("g", cmd) == 0) {
        return 3;
    } else if (strstr("cd", cmd) != NULL) {
        return 4;
    } else if (strstr(cmd, "lcd") != NULL) {
        return 5;
    } else if (strstr(cmd, "get") != NULL) {
        return 6;
    } else if (strstr(cmd, "put") != NULL) {
        return 7;
    }
}

// 从命令中获取第二个参数(假设命令以空格分隔)
char *getbind(char cmd[128]) {
    char *p;
    p = (char *)malloc(128);
    p = strtok(cmd, " ");
    p = strtok(NULL, " ");
    return p;
}

// 接收数据并保存到文件
void getmessage(char cmd[128], int c_fd) {
    char readbuf[8000];
    char *p = getbind(cmd);
    read(c_fd, readbuf, 8000);
    int fd = open(p, O_RDWR | O_CREAT, 0666);
    write(fd, readbuf, strlen(readbuf));
    printf("recrive successful!\n");
    close(fd);
    memset(p, 0, 8000);
}

// 发送文件数据
void putmessage(char cmd[128], int c_fd) {
    char *readbuf = (char *)malloc(128);
    int sfd;
    char *p = (char *)malloc(128);
    readbuf = getbind(cmd);
    if (access(readbuf, F_OK) == -1) {
        printf("no file\n");
    } else {
        sfd = open(readbuf, O_RDWR, 0666);
        read(sfd, p, 8000);
        write(c_fd, p, strlen(p));
        close(sfd);
        memset(p, 0, 8000);
    }
}

// 根据命令标识选择执行的操作
void choosecmd(char cmd[128], int c_fd) {
    int ret = change(cmd);
    char *p = (char *)malloc(8000);
    switch (ret) {
    case 2:
        read(c_fd, p, 1024);
        printf("%s\n", p);
        memset(p, 0, 1024);
        break;
    case 1:
        system("ls");
        break;
    case 3:
        printf("unconnecting\n");
        write(c_fd, "away host", 128);
        close(c_fd);
        exit(-1);
        break;
    case 4:
        printf("hello hjy\n");
        break;
    case 5:
        p = getbind(cmd);
        chdir(p);
        memset(p, 0, 8000);
        break;
    case 6:
        getmessage(cmd, c_fd);
        break;
    case 7:
        putmessage(cmd, c_fd);
        break;
    }
}

int main(int argc, char **argv) {
    char writebuf[128];
    char readbuf[1024];
    int c_fd;
    struct sockaddr_in c_addr;
    int client;

    if (argc != 3) {
        perror("argc");
        exit(1);
    }
    c_fd = socket(AF_INET, SOCK_STREAM, 0);
    printf("hellowdda!\n");
    if (c_fd == -1) {
        perror("socket");
        exit(1);
    }
    memset(&c_addr, 0, sizeof(struct sockaddr_in));
    c_addr.sin_family = AF_INET;
    c_addr.sin_port = htons(atoi(argv[2]));
    inet_aton(argv[1], &c_addr.sin_addr);

    client = sizeof(struct sockaddr_in);
    if (connect(c_fd, (struct sockaddr *)&c_addr, client) < 0) {
        perror("connect");
        exit(-1);
    }

    // 等待发送
    printf("connect....\n");
    while (1) {
        gets(writebuf);
        printf("cmd:%s\n", writebuf);
        write(c_fd, writebuf, strlen(writebuf));
        choosecmd(writebuf, c_fd);
        printf("-------------------cmd-------------------\n");
        memset(writebuf, 0, strlen(writebuf));
    }
    return 0;
}

server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>

// 根据输入命令返回不同的标识值,用于后续操作选择
int change(char cmd[128]) {
    if (strcmp("lls", cmd) == 0) {
        return 1;
    } else if (strcmp("ps", cmd) == 0) {
        return 2;
    } else if (strcmp("g", cmd) == 0) {
        return 3;
    } else if (strstr(cmd, "cd") != NULL) {
        return 4;
    } else if (strstr(cmd, "get") != NULL) {
        return 5;
    } else if (strstr(cmd, "put") != NULL) {
        return 6;
    }
}

// 从命令字符串中获取第二个参数(假设命令以空格分隔)
char *getbehind(char cmd[128]) {
    char *p;
    p = (char *)malloc(128);
    p = strtok(cmd, " ");
    p = strtok(NULL, " ");
    return p;
}

// 接收数据并保存到文件
void putmessage(char cmd[128], int c_fd) {
    char readbuf[8000];
    char *p = getbehind(cmd);
    read(c_fd, readbuf, 8000);
    int fd = open(p, O_RDWR | O_CREAT, 0666);
    write(fd, readbuf, strlen(readbuf));
    printf("receive successful!\n");
    close(fd);
    memset(readbuf, 0, 8000);
}

// 根据命令标识选择执行相应操作
void choosecmd(char cmd[128], int c_fd) {
    int sfd;
    FILE *fdb;
    char *readbuf = (char *)malloc(128);
    int ret;
    char freadbuf[128];
    char *p = (char *)malloc(800);
    ret = change(cmd);
    switch (ret) {
    case 1:
        fdb = popen("ls", "r");
        fread(freadbuf, sizeof(freadbuf), 1, fdb);
        write(c_fd, freadbuf, sizeof(freadbuf));
        memset(freadbuf, 0, sizeof(freadbuf));
        printf("ok\n");
        break;
    case 2:
        system("ps");
        break;
    case 3:
        read(c_fd, freadbuf, 128);
        printf("%s\n", freadbuf);
        exit(1);
        break;
    case 4:
        p = getbehind(cmd);
        chdir(p);
        memset(p, 0, sizeof(p));
        break;
    case 5:
        readbuf = getbehind(cmd);
        if (access(readbuf, F_OK) == -1) {
            write(c_fd, "NO file", sizeof("NO file"));
        } else {
            sfd = open(readbuf, O_RDWR, 0666);
            read(sfd, p, 8000);
            write(c_fd, p, strlen(p));
            close(sfd);
            memset(p, 0, 8000);
        }
        break;
    case 6:
        putmessage(cmd, c_fd);
        break;
    }
}

int main(int argc, char **argv) {
    int c_fd;
    int s_fd;
    int clen;
    int nread;
    char writebuf[128];
    char Readbuf[128];
    struct sockaddr_in s_addr;
    struct sockaddr_in c_addr;
    s_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (s_fd == -1) {
        perror("socket");
        exit(1);
    }
    if (argc != 3) {
        perror("argc");
        exit(1);
    }
    memset(&s_addr, 0, sizeof(struct sockaddr_in));
    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(atoi(argv[2]));
    inet_aton(argv[1], &s_addr.sin_addr);

    bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
    listen(s_fd, 10);
    // accept
    printf("wait connecting\n");
    clen = sizeof(struct sockaddr_in);
    while (1) {
        c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
        if (c_fd == -1) {
            perror("accept");
            exit(1);
        }
        printf("connet success, %s\n", inet_ntoa(c_addr.sin_addr));

        if (fork() == 0) {
            while (1) {
                nread = read(c_fd, Readbuf, 128);
                choosecmd(Readbuf, c_fd);
                memset(Readbuf, 0, sizeof(Readbuf));
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值