解析种子文件主要在parse_metafile.h和parse_metafile.c中完成。parse_metafile.h文件的代码内容为:
parse_metafile.h
#ifndef PARSE_METAFILE
#define PARSE_METAFILE
// 保存从种子文件中获取的tracker的URL
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
#include
#include
#include
#include
#include
#include "parse_metafile.h"
#include "sha1.h"
char *metafile_content = NULL; // 保存种子文件的内容
long filesize; // 种子文件的长度
int piece_length = 0; // 每个piece的长度,通常为256KB即262144字节
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的值,连接tracker和peer时使用
unsigned char peer_id[20]; // 保存peer_id的值,连接peer时使用
Announce_list *announce_list_head = NULL; // 用于保存所有tracker服务器的URL
下面对解析种子文件中用到函数功能解释如下。
l 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)种子文件必须以二进制的方式打开,否则如果以字符方式打开可能无法读取整个文件的内容。无法读取的原因在于piece的hash值中可能含有字符0x00,若文件以字符形式打开,遇到该字符,库函数就认为文件已经结束。
(3)增加“#ifdef DEBUG #endif ”,主要是为了方便调试。如果在parse_metafile.c文件的头部增加宏定义语句“#define DEBUG”,则程序运行时将执行“#ifdef DEBUG”和“#endif”之间的语句。在软件开发阶段,可以使用“#define DEBUG”来打印和查看某些关键的值,开发完毕,去掉该宏,则打印语句不会执行。
l 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的地址。
l 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:...
第一个字符‘d’是B编码中字典的起始符,接着是关键字“8:announce”,该关键字是一个长度为8的字符串,其对应的值为长度为32的字符串“32:”,它是一个Tracker服务器的URL,接着是关键字“13:announce-list”,该关键字对应的值是一个列表,因为关键字“13:announce-list”之后的第一个字符为列表的起始字符‘l’,该列表中含有两个元素,这两个元素的类型也都是列表。
如果有关键字“13:announce-list”就不用处理关键字“8:announce”的原因在于,前者对应的值中必定包含后者对应的值。
(2)“#ifdef DEBUG”和“#endif”之间的语句用于打印各个Tracker的URL。
l int add_an_announce(char *url)
功能:连接某些Tracker时会返回一个重定向的URL,需要连接该URL才能获取peer。函数实现代码如下:
int add_an_announce(char *url)
{
Announce_list *p = announce_list_head, *q;
// 若参数指定的URL在Tracker列表中已存在,则无需添加
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;
}
l 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;
}
l 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作为终结字符)。262144(256K),说明每个piece的长度都是256KB(最后一个piece除外)。接着是关键字“6:pieces”,它对应的值是一个B编码的字符串,存放各个piece的hash值,16900是字符串的长度,该字符串长度除以20即为piece数,因为每个piece的hash值为固定的20字节。
l get_pieces()
功能:获取每个piece的hash值,并保存到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;
}
l 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:13:announce-listll32:http://tk.greedland.net/annou
nceel33: 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”,它指明了创建种子文件的时间。我们注意到时间是一个整数,它是自1970年1月1日到种子文件创建时所经过的秒数,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的形式可以表示宽字符,即中文、日文、朝鲜文等字符。
l 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;
}
l 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字节。
l 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’:遇到一个0~9的数字说明接下来是一个字符串,跳过该字符串继续扫描;遇到‘e’则将push_pop值减1,如果减1后,push_pop值为0,说明已经找到了与‘d’匹配的‘e’。其思路类似于使用数据结构中的“栈”进行括号匹配操作。
(2)以“SHA1”开头的变量和函数用于计算一段文本的hash值,这些变量和函数的定义在sha1.h和sha1.c文件中,hash值的计算原理不必深究。计算hash值的这段代码的功能是以metafile_content[begin]~metafile_content[end]这end-begin+1个字符作为输入,计算其hash值,并把结果保存到info_hash所指向的数组中。
l int get_peer_id()
功能:生成一个惟一的peer id。函数实现代码如下:
int get_peer_id()
{
// 设置产生随机数的种子
srand(time(NULL));
// 使用rand函数生成一个随机数,并使用该随机数来构造peer_id
// peer_id前8位固定为-TT1000-
sprintf(peer_id,"-TT1000-%12d",rand());
#ifdef DEBUG
printf("peer_id:%s\n",peer_id);
#endif
return 0;
}
l 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);
}
}
l 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;
}