BitTorrent协议分析三

各个模块的设计和实现

 

4.1  种子解析模块的设计和实现

解析种子文件主要在parse_metafile.hparse_metafile.c中完成。parse_metafile.h文件的内容为:

parse_metafile.h

#ifndef PARSE_METAFILE

#define PARSE_METAFILE

 

// 保存从种子文件中获取的trackerURL

typedef struct _Announce_list {

     char    announce[128];

     struct  _Announce_list  *next;

} Announce_list;

 

// 保存各个待下载文件的路径和长度

typedef struct _Files {

     char    path[256];

     long    length;

     struct  _Files  *next;

} Files;

 

int read_metafile(char *metafile_name);            // 读取种子文件

int find_keyword(char *keyword,long *position);      // 在种子文件中查找某个关键词

int read_announce_list();                              // 获取各个tracker服务器的地址

int add_an_announce(char* url);                      // tracker列表添加一个URL

 

int get_piece_length();     // 获取每个piece的长度,一般为256KB

int get_pieces();               // 读取各个piece的哈希值

 

int is_multi_files();       // 判断下载的是单个文件还是多个文件

int get_file_name();            // 获取文件名,对于多文件,获取的是目录名

int get_file_length();        // 获取待下载文件的总长度

int get_files_length_path();    // 获取文件的路径和长度,对多文件种子有效

 

int get_info_hash();              // info关键词对应的值计算info_hash

int get_peer_id();            // 生成peer_id,每个peer都有一个20字节的peer_id

 

void release_memory_in_parse_metafile();// 释放parse_metafile.c中动态分配的内存

int  parse_metafile(char *metafile);       // 调用本文件中定义的函数,完成解析种子文件

 

#endif

以下是parse_metafile.c文件的头部,主要是包含了一些头文件和定义一些全局变量,各个函数的定义将在后面列出。

parse_metafile.c

#include <stdio.h>

#include <ctype.h>

#include <malloc.h>

#include <stdlib.h>

#include <string.h>

#include <time.h>

#include "parse_metafile.h"

#include "sha1.h"

 

char    *metafile_content = NULL;   // 保存种子文件的内容

long     filesize;                    // 种子文件的长度

 

int       piece_length   = 0;    // 每个piece的长度,通常为256KB262144字节

char     *pieces = NULL;     // 保存每个pieces的哈希值,每个哈希值为20字节

int       pieces_length = 0;     // 缓冲区pieces的长度

 

int      multi_file = 0;          // 指明是单文件还是多文件

char    *file_name = NULL;      // 对于单文件,存放文件名;对于多文件,存放目录名

long long file_length = 0;           // 存放待下载文件的总长度

Files    *files_head = NULL;     // 只对多文件种子有效,存放各个文件的路径和长度

 

unsigned    char info_hash[20];          // 保存info_hash的值,连接trackerpeer时使用

unsigned    char peer_id[20];            // 保存peer_id的值,连接peer时使用

 

Announce_list *announce_list_head = NULL;   // 用于保存所有tracker服务器的URL

 

下面对解析种子文件中用到函数功能解释如下。

ul  int read_metafile(char *metafile_name)

参数:metafile_name是种子文件名。

返回:处理成功返回0,否则返回−1

附注:将种子文件的内容读入到全局变量metafile_content所指向的缓冲区中以方便处理。该函数的实现代码为:

int read_metafile(char *metafile_name)

{

     long  i;

    

     // 以二进制、只读的方式打开文件

     FILE *fp = fopen(metafile_name,"rb");

     if(fp == NULL) {

          printf("%s:%d can not open file\n",__FILE__,__LINE__);

          return -1;

     }

    

     // 获取种子文件的长度,filesize为全局变量,在parse_metafile.c头部定义

     fseek(fp,0,SEEK_END);

     filesize = ftell(fp);

     if(filesize == -1) {

          printf("%s:%d fseek failed\n",__FILE__,__LINE__);

          return -1;

     }

     metafile_content = (char *)malloc(filesize+1);

     if(metafile_content == NULL) {

          printf("%s:%d malloc failed\n",__FILE__,__LINE__);

          return -1;

     }

    

     // 读取种子文件的内容到metafile_content缓冲区中

     fseek(fp,0,SEEK_SET);

     for(i = 0; i < filesize; i++)

          metafile_content[i] = fgetc(fp);

     metafile_content[i] = '\0';

    

     fclose(fp);

    

#ifdef DEBUG

     printf("metafile size is: %ld\n",filesize);

#endif

     return 0;

}

函数代码说明。

1)编译器预定义的宏__FILE____LINE__在程序中可以直接使用。__FILE__代表该宏所在的源文件的文件名,在源文件parse_metafile.c中该宏的值等于“parse_metafile.c”,宏__LINE__的值为__LINE__所在行的行号。

2)种子文件必须以二进制的方式打开,否则如果以字符方式打开可能无法读取整个文件的内容。无法读取的原因在于piecehash值中可能含有字符0x00,若文件以字符形式打开,遇到该字符,库函数就认为文件已经结束。

3)增加“#ifdef DEBUG  #endif ”,主要是为了方便调试。如果在parse_metafile.c文件的头部增加宏定义语句“#define DEBUG”,则程序运行时将执行“#ifdef DEBUG”和“#endif”之间的语句。在软件开发阶段,可以使用“#define DEBUG”来打印和查看某些关键的值,开发完毕,去掉该宏,则打印语句不会执行。

ul int find_keyword(char *keyword,long *position)

参数:keyword为要查找的关键字,position用于返回关键字第一个字符所在的下标。

返回:成功执行并找到关键字返回1,未找到返回0,执行失败返回−1。函数实现代码如下所示:

int find_keyword(char *keyword,long *position)

{

     long i;

    

     *position = -1;

     if(keyword == NULL)  return 0;

    

     for(i = 0; i < filesize-strlen(keyword); i++) {

          if( memcmp(&metafile_content[i], keyword, strlen(keyword)) == 0 ) {

               *position = i;

               return 1;

          }

     }

    

     return 0;

}

函数代码说明。

该函数在种子文件解析模块的源文件parse_metafile.c中被频繁使用,用于查找某些关键字。例如,关键字“8:announce”和“13:announce-list”之后都是Tracker服务器的地址,找到该关键字后,便可以获取Tracker的地址。

ul read_announce_list()

功能:获取Tracker地址,并将获取的地址保存到全局变量announce_list_head指向的链表中。该函数实现代码如下:

int read_announce_list()

{

     Announce_list  *node = NULL;

     Announce_list  *p    = NULL;

     int            len   = 0;

     long           i;

    

     if( find_keyword("13:announce-list",&i) == 0 ) {

          if( find_keyword("8:announce",&i) == 1 ) {

               i = i + strlen("8:announce");

               while( isdigit(metafile_content[i]) ) {

                    len = len * 10 + (metafile_content[i] - '0');

                    i++;

               }

               i++;  // 跳过 ':'

              

               node = (Announce_list *)malloc(sizeof(Announce_list));

               strncpy(node->announce,&metafile_content[i],len);

               node->announce[len] = '\0';

               node->next = NULL;

               announce_list_head = node;

          }

     }

     else {  // 如果有13:announce-list关键词就不用处理8:announce关键词

          i = i + strlen("13:announce-list");

          i++;         // 跳过 'l'

          while(metafile_content[i] != 'e') {

               i++;     // 跳过 'l'

               while( isdigit(metafile_content[i]) ) {

                    len = len * 10 + (metafile_content[i] - '0');

                    i++;

               }

               if( metafile_content[i] == ':' )  i++;

               else  return -1;

              

               // 只处理以http开头的tracker地址,不处理以udp开头的地址

               if( memcmp(&metafile_content[i],"http",4) == 0 ) {

                    node = (Announce_list *)malloc(sizeof(Announce_list));

                    strncpy(node->announce,&metafile_content[i],len);

                    node->announce[len] = '\0';

                    node->next = NULL;

                   

                    if(announce_list_head == NULL)

                         announce_list_head = node;

                    else {

                         p = announce_list_head;

                         while( p->next != NULL) p = p->next; // 使p指针指向最后个结点

                         p->next = node; // node成为tracker列表的最后一个结点

                    }

               }

              

               i = i + len;

               len = 0;

               i++;    // 跳过 'e'

               if(i >= filesize)  return -1;

          } // while 循环结束

     }

    

#ifdef DEBUG

     p = announce_list_head;

     while(p != NULL) {

          printf("%s\n",p->announce);

          p = p->next;

     }

#endif    

     return 0;

}

程序说明。

1)下面是某种子文件开头的一部分,请对照它来理解read_announce_list函数

d8:announce32:http://tk.greedland.net/announce13:announce-listll32:http://tk.greedland.net/announceel33:http://tk2.greedland.net/announceee...

第一个字符‘d’是B编码中字典的起始符,接着是关键字“8:announce”,该关键字是一个长度为8的字符串,其对应的值为长度为32的字符串“32:http://tk.greedland.net/announce”,它是一个Tracker服务器的URL,接着是关键字“13:announce-list”,该关键字对应的值是一个列表,因为关键字“13:announce-list”之后的第一个字符为列表的起始字符‘l’,该列表中含有两个元素,这两个元素的类型也都是列表。

如果有关键字“13:announce-list”就不用处理关键字“8:announce”的原因在于,前者对应的值中必定包含后者对应的值。

2)“#ifdef DEBUG”和“#endif”之间的语句用于打印各个TrackerURL

ul int add_an_announce(char *url)

功能:连接某些Tracker时会返回一个重定向的URL,需要连接该URL才能获取peer。函数实现代码如下:

int add_an_announce(char *url)

{

Announce_list *p = announce_list_head, *q;

    

     // 若参数指定的URLTracker列表中已存在,则无需添加

     while(p != NULL) {

          if(strcmp(p->announce,url) == 0)  break;

          p = p->next;

     }

     if(p != NULL)  return 0;

    

     q = (Announce_list *)malloc(sizeof(Announce_list));

     strcpy(q->announce,url);

     q->next = NULL;

    

     p = announce_list_head;

     if(p == NULL)  { announce_list_head = q; return 1; }

     while(p->next != NULL)  p = p->next;

     p->next = q;

     return 1;

}

ul  int is_multi_files()

功能:判断是下载多个文件还是单文件,若含有关键字“5:files”则说明下载的是多个文件。函数实现的代码如下:

int is_multi_files()

{

     long i;

    

     if( find_keyword("5:files",&i) == 1 ) {

          multi_file = 1;

          return 1;

     }

    

#ifdef DEBUG

     printf("is_multi_files:%d\n",multi_file);

#endif

    

     return 0;

}

ul int get_piece_length()

功能:获取piece的长度。函数实现的代码如下:

int get_piece_length()

{

     long i;

    

     if( find_keyword("12:piece length",&i) == 1 ) {

          i = i + strlen("12:piece length");  // 跳过 "12:piece length"

          i++;  // 跳过 'i'

          while(metafile_content[i] != 'e') {

               piece_length = piece_length * 10 + (metafile_content[i] - '0');

               i++;

          }

     } else {

          return -1;

     }

 

#ifdef DEBUG

     printf("piece length:%d\n",piece_length);

#endif

     return 0;

}

程序说明。

以下是某种子文件的一部分:12:piece lengthi262144e6:pieces16900:...

从中可以看到,关键字“12:piece length”后面跟一个B编码的整型数(以i作为起始字符,以e作为终结字符)。262144256K),说明每个piece的长度都是256KB(最后一个piece除外)。接着是关键字“6:pieces”,它对应的值是一个B编码的字符串,存放各个piecehash值,16900是字符串的长度,该字符串长度除以20即为piece数,因为每个piecehash值为固定的20字节。

ul get_pieces()

功能:获取每个piecehash值,并保存到pieces所指向的缓冲区中。函数实现的代码如下:

int get_pieces()

{

     long i;

    

     if( find_keyword("6:pieces", &i) == 1 ) {

          i = i + 8;              // 跳过 "6:pieces"

          while(metafile_content[i] != ':') {

               pieces_length = pieces_length * 10 + (metafile_content[i] - '0');

               i++;

          }

          i++;                    // 跳过 ':'

          pieces = (char *)malloc(pieces_length+1);

          memcpy(pieces,&metafile_content[i],pieces_length);

          pieces[pieces_length] = '\0';

     } else {

          return -1;

     }

#ifdef DEBUG

     printf("get_pieces ok\n");

#endif

     return 0;

}

ul  get_file_name()

功能:获取待下载的文件的文件名,如果下载的是多个文件,则获取的是目录名。函数实现的代码如下:

int get_file_name()

{

     long  i;

     int   count = 0;

    

     if( find_keyword("4:name", &i) == 1 ) {

          i = i + 6;  // 跳过 "4:name"

          while(metafile_content[i] != ':') {

               count = count * 10 + (metafile_content[i] - '0');

               i++;

          }

          i++;        // 跳过 ':'

          file_name = (char *)malloc(count+1);

          memcpy(file_name,&metafile_content[i],count);

          file_name[count] = '\0';

     } else {

          return -1;

     }

    

#ifdef DEBUG

     printf("file_name:%s\n",file_name);

#endif

     return 0;

}

程序说明。

以下是一个完整的较为简单的种子文件:

d8:announce32:http://tk.greedland.net/announce13:announce-listll32:http://tk.greedland.net/annou

nceel33:http://tk2.greedland.net/announceee13:creation datei1187968874e4:infod6:lengthi119861

306e4:name31:[ymer][naruto][246][jp_cn].rmvb10:name.utf-831:[ymer][naruto][246][jp_cn].rmv

b12:piece lengthi262144e6:pieces9160:...ee

关键字“13:creation date”之前的部分已经在介绍read_announce_list函数时分析过了,此处不再赘述。关键字“13:creation date”及其对应的值“i1187968874e”,它指明了创建种子文件的时间。我们注意到时间是一个整数,它是自197011到种子文件创建时所经过的秒数,Linux中有专门的库函数处理这种表示类型的时间。

关键字“4:info”对应的值是一个字典,因为该关键字之后的第一个字符是B编码中字典的起始符‘d’,与该起始符对应的终止符是文件末尾的倒数第二个‘e’。计算info_hash时,就是以关键字“4:info”对应的值作为输入,计算其hash值,将得到的值作为info_hash。文件末尾最后一个字符‘e’与文件开头的’d’对应,因此整个种子文件就是一个B编码的字典。

关键字“6:length”对应的值是待下载文件的长度,以字节为单位,可以大致地确定待下载文件的长度为119MB

关键字“4:name”对应的值为待下载的文件的文件名,在这个种子文件中没有关键字“5:files”说明待下载的是单文件。

关键字“10:name.utf-8对应的值也是待下载文件的文件名,只不过以UTF-8的形式表示,UTF-8的形式可以表示宽字符,即中文、日文、朝鲜文等字符。

ul int get_file_length()

功能:获取待下载文件的长度。函数实现的代码如下:

int get_file_length()

{

     long i;

    

     if(is_multi_files() == 1)  {

          if(files_head == NULL)  get_files_length_path();

          Files *p = files_head;

          while(p != NULL) { file_length += p->length; p = p->next; }

     } else {

          if( find_keyword("6:length",&i) == 1 ) {

               i = i + 8;  // 跳过 "6:length"

               i++;        // 跳过 'i'

               while(metafile_content[i] != 'e') {

                    file_length = file_length * 10 + (metafile_content[i] - '0');

                    i++;

               }    

          }

     }

    

#ifdef DEBUG

     printf("file_length:%lld\n",file_length);

#endif

     return 0;

}

ul get_files_length_path()

功能:对于多文件,获取各个文件的路径以及长度。函数实现的代码如下:

int get_files_length_path()

{

     long   i;

     int    length;

     int    count;

     Files  *node  = NULL;

     Files  *p     = NULL;

    

     if(is_multi_files() != 1) {

          return 0;

     }

    

     for(i = 0; i < filesize-8; i++) {

          if( memcmp(&metafile_content[i],"6:length",8) == 0 )

          {

               i = i + 8;  // 跳过 "6:length"

               i++;        // 跳过 'i'

               length = 0;

               while(metafile_content[i] != 'e') {

                    length = length * 10 + (metafile_content[i] - '0');

                    i++;

               }

               node = (Files *)malloc(sizeof(Files));

               node->length = length;

               node->next = NULL;

               if(files_head == NULL)

                    files_head = node;

               else {

                    p = files_head;

                    while(p->next != NULL) p = p->next;

                    p->next = node;

               }

          }

          if( memcmp(&metafile_content[i],"4:path",6) == 0 )

          {

               i = i + 6;  // 跳过 "4:path"

               i++;        // 跳过 'l'

               count = 0;

               while(metafile_content[i] != ':') {

                    count = count * 10 + (metafile_content[i] - '0');

                    i++;

               }

               i++;        // 跳过 ':'

               p = files_head;

               while(p->next != NULL) p = p->next;

               memcpy(p->path,&metafile_content[i],count);

               *(p->path + count) = '\0';

          }

     }

    

     return 0;

}

程序说明。

13-2是一个多文件种子的一部分,可以参照图13-2理解get_files_length_path函数。

多文件种子的关键字“5:files”对应的值是比较复杂的。关键字“5:files”说明这是一个多文件种子,它对应的值是一个列表,列表的每个元素是字典,每个字典代表一个待下载文件。

13-2  多文件种子示例

5:filesl”及字符‘d’之后,有一个关键字“6:length”及其值“i127025815e”,然后是关键字“4:path”,其值为一个列表“l34:[BBsee出品][军情观察室08.22].rmvbe”,“rmvbee”中最后一个‘e’与字符‘d’对应。

然后“d6:lengthi76e4:pathl42:综艺 美剧 篮球 足球 尽在迅视XunTv.Net.urlee”又是一个字典。“urleee”中最后一个‘e’与“5:files”后的‘l’构成一个列表。“4:name”所跟的是目录名,然后是“12:piece length”关键字,“6:pieces”关键字。

从中可以总结出:有一个目录名“[BBsee出品][军情观察室08.22]”,其中存放了两个文件“[BBsee出品][军情观察室08.22].rmvb”和“综艺 美剧 篮球 足球 尽在迅视 XunTv. Net.url”,长度分别为127025815字节和76字节。

ul int get_info_hash ()

功能:计算info_hash的值。函数实现代码如下:

int get_info_hash()

{

     int   push_pop = 0;

     long  i, begin, end;

    

     if(metafile_content == NULL)  return -1;

     // begin的值表示的是关键字"4:info"对应值的起始下标

     if( find_keyword("4:info",&i) == 1 )  begin = i+6;

else  return -1;

    

     i = i + 6;        // 跳过 "4:info"

     for(; i < filesize; )

          if(metafile_content[i] == 'd') {

               push_pop++;

               i++;

          } else if(metafile_content[i] == 'l') {

               push_pop++;

               i++;

          } else if(metafile_content[i] == 'i') {

               i++;      // 跳过 i

               if(i == filesize)  return -1;

               while(metafile_content[i] != 'e') {

                    if((i+1) == filesize)  return -1;

                    else i++;

               }

               i++;      // 跳过 e

} else if((metafile_content[i] >= '0') && (metafile_content[i] <= '9')) {

               int number = 0;

               while((metafile_content[i] >= '0') && (metafile_content[i] <= '9')) {

                    number = number * 10 + metafile_content[i] - '0';

                    i++;

               }

               i++;      // 跳过‘!’:

               i = i + number;

} else if(metafile_content[i] == 'e') {

               push_pop--;

               if(push_pop == 0) { end = i; break; }

               else  i++;

          } else {

               return -1;

          }

     if(i == filesize)  return -1;

    

     SHA1_CTX context;

     SHA1Init(&context);

     SHA1Update(&context, &metafile_content[begin], end-begin+1);

     SHA1Final(info_hash, &context);

    

#ifdef DEBUG

     printf("info_hash:");

     for(i = 0; i < 20; i++) 

          printf("%.2x ",info_hash[i]);

     printf("\n");

#endif

     return 0;

}

程序说明。

1)在种子文件解析模块,由种子文件的内容计算info_hash的值是比较复杂的。前面已经提到,由关键字“4:info”对应的值来计算info_hash,该关键字对应的值是一个B编码的字典,问题的关键在于找到与“4:info”之后的‘d’对应的‘e’。

get_info_hash函数中找到所需要的‘e’的思路是:在“4:info”之后,每当遇到字典的起始符‘d’,则将push_pop的值加1(push_pop初始值为0),遇到列表的起始符‘l’也作相同处理;遇到整数的起始符‘i’则一直扫描直到找到与之对应的终结符‘e’:遇到一个09的数字说明接下来是一个字符串,跳过该字符串继续扫描;遇到‘e’则将push_pop值减1,如果减1后,push_pop值为0,说明已经找到了与‘d’匹配的‘e’。其思路类似于使用数据结构中的“栈”进行括号匹配操作。

2)以“SHA1”开头的变量和函数用于计算一段文本的hash值,这些变量和函数的定义在sha1.hsha1.c文件中,hash值的计算原理不必深究。计算hash值的这段代码的功能是以metafile_content[begin]metafile_content[end]end-begin+1个字符作为输入,计算其hash值,并把结果保存到info_hash所指向的数组中。

ul int get_peer_id()

功能:生成一个惟一的peer id。函数实现代码如下:

int get_peer_id()

{

     // 设置产生随机数的种子

     srand(time(NULL));

     // 使用rand函数生成一个随机数,并使用该随机数来构造peer_id

// peer_id8位固定为-TT1000-

     sprintf(peer_id,"-TT1000-d",rand());

    

#ifdef DEBUG

     printf("peer_id:%s\n",peer_id);

#endif

     return 0;

}

ul void release_memory_in_parse_metafile()

功能:释放动态申请的内存。函数实现代码如下:

void release_memory_in_parse_metafile()

{

     Announce_list *p;

     Files         *q;

    

     if(metafile_content != NULL)     free(metafile_content);

     if(file_name != NULL)          free(file_name);

     if(pieces != NULL)              free(pieces);

    

     while(announce_list_head != NULL) {

          p = announce_list_head;

          announce_list_head = announce_list_head->next;

          free(p);

     }

    

     while(files_head != NULL) {

          q = files_head;

          files_head = files_head->next;

          free(q);

     }

}

ul int parse_metafile(char *metafile)

功能:调用parse_metafile.c中定义的函数,完成解析种子文件。该函数由main.c调用。

返回:解析成功返回0,否则返回-1。函数实现代码如下:

int parse_metafile(char *metafile)

{

     int ret;

    

     // 读取种子文件

     ret = read_metafile(metafile);

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 从种子文件中获取tracker服务器的地址

     ret = read_announce_list();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 判断是否为多文件

     ret = is_multi_files();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 获取每个piece的长度,一般为256KB

     ret = get_piece_length();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 读取各个piece的哈希值

     ret = get_pieces();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 获取要下载的文件名,对于多文件的种子,获取的是目录名

     ret = get_file_name();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 对于多文件的种子,获取各个待下载的文件路径和文件长度

     ret = get_files_length_path();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 获取待下载的文件的总长度

     ret = get_file_length();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     // 获得info_hash,生成peer_id

     ret = get_info_hash();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

     ret = get_peer_id();

     if(ret < 0) { printf("%s:%d wrong",__FILE__,__LINE__); return -1; }

    

     return 0;

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值