BitTorrent协议分析三

论坛 期权论坛 脚本     
匿名网站用户   2020-12-21 08:15   35   0

各个模块的设计和实现

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 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:1136255
帖子:227251
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP