深入分析tftp32源码:网络传输核心技术探究

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:tftp32是基于TCP/IP协议栈的轻量级文件传输协议软件,源码包珍贵且难以获得,是网络通信学习与研究的宝贵资源。文章将细致解读tftp32源码包中的关键知识点,深入其工作原理,为开发者提供开发参考。tftp32包括服务端和客户端功能,源码分析涵盖项目结构、服务端实现、客户端接口、数据包处理、文件操作等关键模块,同时涉及事件驱动编程、多线程技术、内存管理和错误处理等学习要点。实践与调试部分鼓励读者通过分析源码来深入理解TFTP协议,并提升网络编程技能。 tftp32 源码包

1. TFTP协议基础介绍

简述TFTP协议

TFTP(Trivial File Transfer Protocol)是一个简单的文件传输协议,用于在客户端和服务器之间进行文件传输。其设计目的是需要很小的代码实现,使得其能够容易地嵌入到操作系统或其它应用中。相较于FTP,TFTP不包含用户认证、目录浏览等功能,也没有庞大的状态机和复杂的命令交互。

TFTP的特点与优势

TFTP通常用于引导配置,如无盘启动环境中,因为它比FTP更轻量级,更适用于传输引导文件、系统文件等小文件。TFTP协议简单、小巧,且不需要建立复杂连接,能够快速启动文件传输过程。

TFTP的工作模式与传输过程

TFTP主要有两种工作模式:RRQ(读请求)和WRQ(写请求)。RRQ用于从服务器端读取文件,而WRQ用于向服务器端写入文件。在传输过程中,TFTP使用UDP协议,端口号为69。传输中,数据包被分为512字节的数据块进行发送,最后可能以一个小于512字节的数据块或一个ACK包结束,以实现对文件的完整传输。

2. tftp32项目结构分析

2.1 tftp32源码结构概览

2.1.1 主要源文件和目录布局

tftp32项目由多个源文件和目录组成,这些组件协同工作,共同实现TFTP协议的客户端和服务器功能。在项目根目录下,你可以找到以下几个关键的目录:

  • src/ :包含源代码文件,这个目录是理解tftp32工作原理的核心。
  • include/ :包含头文件,它们定义了项目中使用的数据结构和函数原型。
  • docs/ :存放项目文档,有助于开发者快速理解项目结构和功能。
  • bin/ :包含编译生成的可执行文件,用于实际的文件传输操作。

src/ 目录中,文件通常根据它们所承担的功能进行组织。例如, server.c client.c 分别实现了服务端和客户端的核心逻辑,而 utils.c 则包含了一些通用的辅助函数。

接下来,我们将深入探讨编译构建过程,理解如何从源代码构建出可执行程序。

2.1.2 编译构建过程解析

tftp32的编译构建过程涉及到几个关键的步骤,使用GNU工具链(gcc、make等)。以下是简化的构建流程:

  1. 环境准备 :确保编译环境安装了gcc和make。
  2. 配置 :使用 ./configure 脚本来检查系统环境并生成适合本系统的Makefile。
  3. 编译 :运行 make 命令来编译源代码,生成 tftp32 tftp32server 可执行文件。
  4. 安装 :使用 make install 来安装生成的可执行文件到系统目录。

在编译过程中,Makefile文件扮演着重要角色。它定义了各个源文件如何被编译和链接,以及最终生成的可执行文件名。下面是一个简化的Makefile示例:

CC=gcc
CFLAGS=-I./include

tftp32: src/tftp32.o utils.o
    $(CC) -o tftp32 src/tftp32.o utils.o $(CFLAGS)

tftp32server: src/server.o utils.o
    $(CC) -o tftp32server src/server.o utils.o $(CFLAGS)

.PHONY: clean
clean:
    rm -f *.o tftp32 tftp32server

该Makefile定义了如何构建 tftp32 tftp32server 目标,以及清理编译生成的中间文件。

2.2 tftp32的核心组件和功能

2.2.1 核心组件的功能划分

tftp32项目由多个核心组件构成,它们各自承担着不同的功能:

  • 客户端组件 :负责与TFTP服务器建立连接,并发送读写文件请求。
  • 服务端组件 :管理文件传输请求,并实现文件的存储和检索。
  • 数据包处理组件 :对TFTP协议数据包进行封装和解析,确保数据在客户端和服务器间正确传输。
  • 错误处理组件 :处理通信过程中可能出现的各类错误,提供重试和恢复机制。

每个组件都通过精心设计的接口与其他部分交互,保持了高内聚低耦合的设计原则。下面,我们将探讨核心组件中使用的数据结构。

2.2.2 源码中的关键数据结构

在tftp32的源码中,数据结构的设计直接关联到程序的性能和功能实现。下面列举一些关键的数据结构及其作用:

  • struct tftp_packet :表示一个TFTP数据包,包含操作码(如RRQ, WRQ, DATA等)和数据内容。
  • struct tftp_transfer :描述一个传输会话的状态,包括文件名、传输模式、文件句柄等。
  • struct tftp_session :表示客户端与服务端的会话信息,如客户端IP、端口号和当前传输状态。

这些数据结构的定义通常位于 include/ 目录下的头文件中,以便于在项目的所有源文件中引用。例如:

// include/tftp_types.h
typedef struct tftp_packet {
    uint16_t opcode;
    char data[512];
} tftp_packet;

在下一章节中,我们将进一步探讨服务端的核心实现细节,包括服务端启动流程和事件处理机制。

3. 服务端实现关键模块

3.1 服务端核心逻辑

3.1.1 服务端启动流程

服务端启动流程是 TFTP 服务器运作的基础,它包括初始化网络接口、设置监听端口以及处理客户端连接请求。在 tftp32 的实现中,这一流程尤为重要,因为它直接影响服务端的稳定性和性能。

// tftp32服务端启动示例代码
int main(int argc, char *argv[]) {
    // 初始化网络库
    network_init();

    // 设置监听端口,TFTP 默认端口是 69
    int port = 69;
    set_listening_port(port);

    // 开始监听端口,等待客户端连接
    while (1) {
        // 该函数会阻塞当前线程,直到有新的客户端连接
        TFTPConnection* connection = accept_new_connection();
        if (connection) {
            // 对于每个客户端连接,启动一个新的线程进行处理
            handle_client_connection(connection);
        }
    }

    return 0;
}

3.1.2 服务端事件处理机制

服务端事件处理机制决定了 TFTP 服务端如何响应客户端的请求。在 tftp32 中,事件驱动编程模型被用来处理多种事件,例如接收数据包、定时超时、文件操作完成等。

graph TD;
    A[开始监听] --> B{是否收到请求};
    B -->|是| C[处理请求];
    B -->|否| D[等待超时];
    C --> E{处理结果};
    E -->|成功| F[返回响应];
    E -->|失败| G[返回错误];
    F --> H[继续监听];
    G --> H;
    D --> H;

3.2 服务端功能模块详解

3.2.1 文件传输处理

文件传输处理模块负责管理文件的读取和发送操作。当接收到读取请求(RRQ)或写入请求(WRQ)时,该模块将启动相应的处理流程。

// 读取请求处理函数
void handle_read_request(TFTPConnection* conn, const char* filename, const char* mode) {
    FILE *file = fopen(filename, "rb");
    if (file == NULL) {
        // 文件不存在,发送错误响应
        send_error(conn, FILE_NOT_FOUND);
        return;
    }

    // 读取文件内容并发送
    char buffer[516];
    int file_size = 0;
    while ((file_size = fread(buffer, 1, 512, file)) > 0) {
        send_data_packet(conn, buffer, file_size);
    }

    // 发送文件结束响应
    send_ack_packet(conn);

    fclose(file);
}

// 写入请求处理函数
void handle_write_request(TFTPConnection* conn, const char* filename, const char* mode) {
    FILE *file = fopen(filename, "wb");
    if (file == NULL) {
        // 文件创建失败,发送错误响应
        send_error(conn, ACCESS_VIOLATION);
        return;
    }

    char buffer[516];
    int received_size;
    while ((received_size = receive_data_packet(conn, buffer, sizeof(buffer))) > 0) {
        fwrite(buffer, 1, received_size, file);
    }

    fclose(file);
    // 发送写入成功响应
    send_ack_packet(conn);
}

3.2.2 客户端连接管理

客户端连接管理模块负责监控和服务端所有的客户端连接。它跟踪每个连接的状态,并在必要时进行管理,如超时、断开或其他异常处理。

// 连接管理函数示例
void manage_client_connections() {
    // 获取当前所有客户端连接列表
    TFTPConnectionList* connections = get_connection_list();

    // 遍历所有连接
    for (int i = 0; i < connections->length; ++i) {
        TFTPConnection* conn = connections->connections[i];

        // 检查连接是否超时
        if (is_timeout(conn)) {
            // 发送超时错误响应
            send_error(conn, TIMED_OUT);
            // 关闭连接
            close_connection(conn);
            // 从列表中移除该连接
            connections->connections[i] = NULL;
            --i; // 重新检查刚刚移动到当前位置的连接
        }

        // 其他连接管理操作...
    }
}

在本节中,我们详细探讨了服务端核心逻辑和服务端功能模块的关键组成部分。了解了服务端如何响应启动流程和事件处理机制,以及如何管理文件传输和客户端连接。这些关键模块是 tftp32 服务端能够稳定运行的基础,而掌握这些知识对于进一步优化和维护 TFTP 服务端至关重要。

4. 客户端接口实现

4.1 客户端主要功能

4.1.1 文件读写操作接口

客户端接口实现的核心是处理用户发起的文件读写操作请求。文件读写操作接口通常需要提供简单的API供用户调用,以便用户可以通过这些接口来请求服务器上的文件,以及将文件上传到服务器。下面是一个简单的文件读取操作接口的实现示例:

int read_file(const char *filename) {
    FILE *fp = fopen(filename, "rb"); // 打开文件用于读取
    if (!fp) {
        perror("Error opening file for reading");
        return -1;
    }

    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), fp)) {
        // 这里可以通过TFTP协议向服务器发送请求获取文件内容
        // 然后逐行打印到标准输出
        printf("%s", buffer);
    }

    fclose(fp);
    return 0;
}

参数说明: - const char *filename :需要读取的文件名。 - 返回值:0表示成功,-1表示失败。

逻辑分析: 此函数首先尝试打开指定的文件进行读取。如果文件成功打开,它会进入一个循环,通过标准的文件操作函数 fgets 逐行读取文件内容。在实际的TFTP客户端实现中,这里会涉及到通过TFTP协议向服务端发送读取请求,并接收服务端返回的数据包,然后再将数据输出。如果文件打开失败,函数会输出错误信息并返回错误代码。

4.1.2 连接和断开的管理

连接的建立和断开是客户端与服务端交互的基础。在TFTP协议中,连接的建立通常在客户端发起读写请求时自动完成,而断开则是在文件传输完成后由客户端显式发起。以下展示了如何在代码中管理TFTP客户端的连接和断开操作:

void establish_connection(const char *server_ip) {
    // 建立与服务端的连接,参数为服务端的IP地址
    // 在TFTP中,这通常涉及到设置网络套接字等操作
}

void disconnect_connection() {
    // 断开与服务端的连接
    // 在TFTP中,可能涉及关闭网络套接字等操作
}

这里只是提供了概念性的函数接口,实际的网络编程实现细节会涉及到对套接字(sockets)的操作,包括创建套接字、绑定IP地址、监听端口、接受连接、发送和接收数据包等等。

4.2 客户端与服务端交互流程

4.2.1 请求发送和接收机制

在TFTP客户端与服务端的交互过程中,客户端需要发送请求并接收来自服务端的响应。下面是一个简化的请求发送和接收的流程描述:

enum tftp_opcodes {
    RRQ = 1,  // 读请求
    WRQ,      // 写请求
    DATA,     // 数据包
    ACK,      // 确认包
    ERROR     // 错误消息
};

typedef struct {
    uint16_t opcode;  // 操作码
    char *filename;   // 文件名
    char *mode;       // 传输模式
} tftp_packet;

void send_request(tftp_packet *packet) {
    // 发送请求到服务端,根据packet中的opcode确定请求类型
    // 实际发送时需要将请求封装成TFTP协议格式的数据包,并通过网络发送
}

tftp_packet *receive_response() {
    // 接收来自服务端的响应数据包,并进行解析
    // 在这里可能需要循环等待接收,直到收到服务端的确认包或错误响应
    // 返回值为指向解析后响应数据包的指针,调用者负责释放内存
}

参数说明和逻辑分析: - tftp_packet 结构体用于封装TFTP请求或响应数据包,包含操作码、文件名和传输模式等字段。 - send_request 函数用于发送请求到服务端。根据传入的 tftp_packet 的操作码,选择发送读请求(RRQ)或写请求(WRQ)。 - receive_response 函数用于接收服务端的响应。这里涉及到网络I/O操作,可能需要使用select/poll等方法非阻塞地等待数据到达。函数返回一个指向解析后的响应数据包的指针。

4.2.2 错误处理和重试逻辑

错误处理和重试逻辑是客户端与服务端交互中的重要部分,保证了客户端在遇到错误时可以做出恰当的响应,并尝试重新建立连接或进行数据重传。

int handle_error(tftp_packet *packet) {
    // 处理错误消息,可能需要根据错误代码采取不同的操作
    // 例如,如果是找不到文件(Error Code 1),则可能需要提示用户检查文件名
    // 如果是访问权限问题(Error Code 2),则可能需要检查用户权限等
    // 返回值通常为错误代码,0表示没有错误
}

void retry_request(tftp_packet *packet, int retries) {
    // 发送请求并处理响应,如果遇到错误则重试
    // 重试次数由retries参数指定
    // 重试逻辑通常需要限制重试次数,避免无限循环
}

参数说明: - tftp_packet *packet :指向当前请求的数据包的指针。 - int retries :允许的重试次数。

逻辑分析: handle_error 函数根据返回的错误包中的错误码来决定接下来的操作。客户端在收到错误响应后,通常会显示错误信息给用户并提供可能的解决方案。例如,如果错误码是1(文件不存在),则提示用户检查文件名是否正确;如果错误码是2(访问被拒绝),则可能需要检查用户的权限设置。

retry_request 函数则封装了发送请求、接收响应和错误处理的逻辑,如果请求失败(即收到错误响应),并且重试次数尚未用完,它会重新发送请求。它也会在循环中逐步减少重试次数,直到重试次数耗尽为止。这样可以避免在网络不可靠时客户端陷入无限重试的境地。

此部分展示的是错误处理和重试机制的核心逻辑,实际的实现可能需要更多的细节处理,例如超时处理、日志记录和用户反馈等。

5. 数据包处理与文件操作

5.1 数据包封装与解析

5.1.1 数据包的结构分析

数据包是TFTP协议进行数据传输的基本单位。TFTP数据包由报头和数据两部分组成。报头固定为4字节,由操作码(2字节)、块号(2字节)组成。操作码用于标识数据包的类型,例如RRQ(读请求)、WRQ(写请求)、DATA(数据块)、ACK(确认)、ERROR(错误)。块号是递增的,用于标识当前传输的数据块编号。

数据部分的大小是变化的,依据MTU(最大传输单元)和TFTP协议规范限制,每个数据包最多可以携带512字节的数据(不包括4字节的报头)。对于文件的最后一个数据块,其大小可以小于512字节,这种情况下标志着传输的结束。

5.1.2 封装和解析算法实现

在tftp32项目中,数据包的封装和解析是通过一系列函数实现的。封装函数将数据和操作码结合,形成符合协议的数据包,并发送到网络。解析函数则对收到的数据包进行分解,提取出操作码、块号和数据部分。

// 封装数据包函数示例
void pack_tftp_packet(uint16_t opcode, uint16_t block_num, char* data, int data_len, char* packet) {
    // 将操作码和块号放入报头
    memcpy(packet, &opcode, 2);
    memcpy(packet + 2, &block_num, 2);

    // 将数据部分拷贝到数据包中
    if (data && data_len > 0) {
        memcpy(packet + 4, data, data_len);
    }
    // 数据包总长度不能超过516字节(包括报头)
    int packet_size = data_len + 4 <= 516 ? data_len + 4 : 516;
    // 计算并设置数据包的长度
    set_packet_length(packet_size);
}

// 解析数据包函数示例
void unpack_tftp_packet(char* packet, uint16_t* opcode, uint16_t* block_num, char* data, int* data_len) {
    // 从报头提取操作码和块号
    memcpy(opcode, packet, 2);
    memcpy(block_num, packet + 2, 2);
    // 提取数据部分
    if (data && data_len) {
        *data_len = get_packet_data_length(packet);
        memcpy(data, packet + 4, *data_len);
    }
}

在上述代码中, pack_tftp_packet 函数用于创建TFTP数据包,而 unpack_tftp_packet 用于解析接收到的数据包。 set_packet_length get_packet_data_length 是假设存在的辅助函数,分别用于设置和获取数据包的长度。解析函数通过指针参数返回操作码、块号和数据长度,以供进一步处理。

5.2 文件传输的实现细节

5.2.1 文件读取和写入流程

文件读取和写入是文件传输过程中的两个核心步骤。在tftp32项目中,当接收到RRQ或WRQ请求时,服务端会打开指定的文件,并根据请求类型进行读取或写入操作。

对于读操作(RRQ),服务端会在接收到客户端的请求后,打开指定的文件,然后读取文件内容并封装成多个数据包发送给客户端,直到文件结束。

对于写操作(WRQ),服务端会等待客户端发送数据包,收到数据包后写入文件,直到客户端发送结束信号。

// 读操作(RRQ)文件处理伪代码
for (;;) {
    data = read_file_part(file, 512); // 读取文件的一部分
    if (data == NULL || data_length <= 0) {
        break; // 文件结束或读取错误
    }
    pack_tftp_packet(DATA, block_num++, data, data_length, packet); // 封装数据包
    send_packet_to_client(packet); // 发送数据包给客户端
}

// 写操作(WRQ)文件处理伪代码
for (;;) {
    if (!receive_packet_from_client(packet)) {
        break; // 接收错误或文件结束信号
    }
    unpack_tftp_packet(packet, &opcode, &block_num, data, &data_length);
    if (opcode == DATA) {
        write_file_part(file, data, data_length); // 写入文件部分
    } else if (opcode == ACK) {
        // 确认块号,处理重发逻辑
    }
}

在读操作中,服务端不断读取文件内容并发送给客户端,直到文件读取完毕。写操作中,服务端则等待客户端发送数据,收到后写入文件。

5.2.2 传输过程中的同步和确认机制

为了确保文件传输的可靠性,TFTP协议使用了简单的停止-等待ARQ(自动重传请求)机制。客户端发送数据包后,必须等待服务端发送一个ACK包,确认接收到了特定的块号。只有在收到ACK后,客户端才能发送下一个数据块。

这种机制保证了数据的完整性,但是效率并不高,因为它要求发送方在发送下一个数据包之前必须等待接收方的确认。为了优化性能,tftp32项目可能引入了一些改进策略,比如批处理ACK,允许在一定条件下连续发送多个数据包而不等待每个ACK。

为了实现同步和确认机制,需要在客户端和服务端之间维护一个窗口大小,表示未被确认的数据包数量。如果窗口内数据包在预定时间内没有得到确认,就会触发重传机制。

// 简化的同步和确认机制伪代码
int expected_block_num = 0;
while (!file_transfer_completed) {
    if (is_sending_file) {
        if (expected_block_num == received_ack_block_num) {
            send_next_data_block(); // 发送下一个数据块
            expected_block_num++;
        }
    } else {
        if (received_data_block_num == expected_block_num + 1) {
            send_ack(expected_block_num); // 发送ACK确认
        }
    }
}

在此伪代码中, is_sending_file 表示当前是发送方还是接收方。 expected_block_num 是预期收到的下一个数据块的块号, received_ack_block_num 是实际收到的ACK的块号。根据状态和块号,决定发送数据块或者ACK。

请注意,以上代码片段是为了展示概念而设计的,并不是实际的tftp32代码,实际的实现会更复杂且考虑到各种异常情况和边界条件。在tftp32项目中,应该可以找到对应上述功能实现的函数或方法。

6. 源码学习要点概述

6.1 事件驱动编程在tftp32中的应用

事件驱动编程是一种程序设计范式,以事件作为程序控制权转移的基础。在tftp32项目中,事件驱动模型的原理和优势体现在其非阻塞I/O操作和高效率的并发处理。

6.1.1 事件驱动模型的原理和优势

事件驱动模型的核心在于事件循环,即程序在等待和响应事件发生之间交替运行,而不是连续执行。

graph LR
    A[启动事件循环] -->|事件发生| B[事件处理]
    B --> C[等待下一个事件]
    C -->|事件发生| B

这种方式的优势包括: - 响应性 :快速响应外部事件,提升用户体验。 - 效率 :利用系统资源进行多任务处理,避免CPU空闲。

6.1.2 tftp32中的事件驱动实践

在tftp32项目中,事件驱动编程的具体实践包括使用select/poll/epoll等机制监控文件描述符状态,及时响应网络事件。

struct pollfd {
    int fd;         // 文件描述符
    short events;   // 请求关注的事件
    short revents;  // 实际发生的事件
};

// 示例代码:使用poll进行事件监听
int timeout = 100; // 100毫秒超时
struct pollfd pfds[] = {
    {socket_fd, POLLIN, 0},
};

int n = poll(pfds, 1, timeout);
if (n > 0 && (pfds[0].revents & POLLIN)) {
    // 处理读取事件
}

6.2 tftp32的多线程技术

多线程技术允许tftp32在处理多个客户端请求时,实现并行操作,提高系统的并发能力和效率。

6.2.1 线程模型的选择和实现

tftp32项目采用的线程模型通常基于POSIX线程库,即pthreads。这种模型下,主线程负责监听和接受客户端连接,子线程负责具体的文件传输任务。

pthread_t worker_threads[MAX_CLIENTS];
// 创建子线程处理客户端请求
for (int i = 0; i < MAX_CLIENTS; i++) {
    pthread_create(&worker_threads[i], NULL, client_thread_func, (void *)&client[i]);
}

6.2.2 线程同步和资源竞争解决方案

多线程环境下,线程同步是保证数据一致性的关键。tftp32通过互斥锁(mutexes)和条件变量(condition variables)等机制解决线程同步问题。

pthread_mutex_t mutex;
pthread_cond_t cond;

pthread_mutex_lock(&mutex);
// 临界区代码
pthread_mutex_unlock(&mutex);

pthread_cond_wait(&cond, &mutex);
// 条件满足后的操作

6.3 内存管理和错误处理机制

6.3.1 动态内存分配和释放策略

tftp32项目中,内存管理需要考虑内存泄漏和碎片化问题。项目通常会采用智能指针和内存池等技术来优化内存的使用。

// 示例代码:使用智能指针自动管理内存
#include <memory>

std::unique_ptr<char[]> buffer(new char[1024]);

6.3.2 错误检测、报告和恢复流程

错误处理在tftp32中是不可或缺的,涉及到错误的检测、报告和恢复等环节。通常,项目会定义错误码和异常处理机制来处理和记录错误信息。

#define ERR_BAD_USAGE 1
#define ERR_NOT_FOUND 2
// ...

switch (error_code) {
    case ERR_BAD_USAGE:
        // 报告错误用法
        break;
    case ERR_NOT_FOUND:
        // 报告资源未找到
        break;
    // ...
    default:
        // 默认错误处理
        break;
}

通过本章的学习,我们可以看到tftp32项目在事件驱动编程、多线程和内存管理等关键领域的应用和实践。这些要点是深入理解项目源码的基础,对于有志于深入研究和优化tftp32项目的开发者而言,是必备的知识储备。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:tftp32是基于TCP/IP协议栈的轻量级文件传输协议软件,源码包珍贵且难以获得,是网络通信学习与研究的宝贵资源。文章将细致解读tftp32源码包中的关键知识点,深入其工作原理,为开发者提供开发参考。tftp32包括服务端和客户端功能,源码分析涵盖项目结构、服务端实现、客户端接口、数据包处理、文件操作等关键模块,同时涉及事件驱动编程、多线程技术、内存管理和错误处理等学习要点。实践与调试部分鼓励读者通过分析源码来深入理解TFTP协议,并提升网络编程技能。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值