BitTorrent协议分析四

4.2  位图管理模块的设计和实现

对位图的操作主要在bitfield.h和bitfield.c中,负责创建位图,设置和获取位图某一位的值,保存位图等。

bitfield.h

#ifndef BITFIELD_H

#define BITFIELD_H

 

typedef struct _Bitmap {

     unsigned char  *bitfield;          // 保存位图

     int           bitfield_length; // 位图所占的总字节数

     int           valid_length;      // 位图有效的总位数,每一位代表一个piece

} Bitmap;

 

int  create_bitfield();                            // 创建位图,分配内存并进行初始化

int  get_bit_value(Bitmap *bitmap,int index);    // 获取某一位的值

int  set_bit_value(Bitmap *bitmap,int index, unsigned char value);

// 设置某一位的值

int  all_zero(Bitmap *bitmap);                   // 全部清零

int  all_set(Bitmap *bitmap);                        // 全部设置为1

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

int  print_bitfield(Bitmap *bitmap);               // 打印位图值,用于调试

 

int  restore_bitmap();   // 将位图存储到文件中

                              // 在下次下载时,先读取该文件获取已经下载的进度

int  is_interested(Bitmap *dst,Bitmap *src);     // 拥有位图src的peer是否对拥有

                                                   // dst位图的peer感兴趣

int  get_download_piece_num();                    // 获取当前已下载到的总piece数

 

#endif

 

程序说明。

(1)结构体Bitmap中,bitfield_length为指针bitfield所指向的内存的长度(以字节为单位),而valid_length为位图的有效位数。例如,某位图占100字节,而有效位数位795,则位图最后一个字节的最后5位(100 ´ 8−795)是无效的。

(2)函数is_interested用于判断两个peer是否感兴趣,如果peer1拥有某个piece,而peer2没有,则peer2对peer1感兴趣,希望从peer1处下载它没有的piece。

(3)函数get_download_piece_num用于获得已下载的piece数,其方法是统计结构体Bitmap的bitfield成员所指向的内存中值为1的位数。

文件bitfield.c的头部包含的文件如下:

bitfield.c文件头部包括的内容

#include

#include

#include

#include

#include

#include

#include

#include "parse_metafile.h"

#include "bitfield.h"

 

extern int  pieces_length;

extern char *file_name;

 

Bitmap       *bitmap = NULL;           // 指向位图

int            download_piece_num = 0; // 当前已下载的piece数

 

程序说明。

(1)语句“extern int pieces_length;”声明了一个变量,这个变量是在parse_metafile.c中定义的全局变量。如果要在其他源文件中使用某个源文件中定义的变量,需要在使用该变量的源文件的头部以extern关键字声明。注意声明和定义的区别,声明仅仅是告知编译器有某个变量,而对于定义,编译器要分配内存空间来存储该变量的值。

(2)全局变量bitmap指向自己的位图,可以从位图中获知下载的进度。peer的位图存放在Peer结构体中。

ul int create_bitfield()

该函数较为简单,不另加注释

int create_bitfield()

{

     bitmap = (Bitmap *)malloc(sizeof(Bitmap));

     if(bitmap == NULL) {

          printf("allocate memory for bitmap fiailed\n");

          return -1;

     }

    

     // pieces_length除以20即为总的piece数

     bitmap->valid_length = pieces_length / 20;

     bitmap->bitfield_length = pieces_length / 20 / 8;

     if( (pieces_length/20) % 8 != 0 )  bitmap->bitfield_length++;

    

     bitmap->bitfield = (unsigned char *)malloc(bitmap->bitfield_length);

     if(bitmap->bitfield == NULL)  {

          printf("allocate memory for bitmap->bitfield fiailed\n");

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

          return -1;

     }

    

     char bitmapfile[64];

     sprintf(bitmapfile,"�itmap",pieces_length);

     int  i;

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

     if(fp == NULL) {  // 若打开文件失败,说明开始的是一个全新的下载

          memset(bitmap->bitfield, 0, bitmap->bitfield_length);

} else {

          fseek(fp,0,SEEK_SET);

          for(i = 0; i < bitmap->bitfield_length; i++)

               (bitmap->bitfield)[i] = fgetc(fp);

          fclose(fp);

          // 给download_piece_num赋新的初值

          download_piece_num = get_download_piece_num();

     }

    

     return 0;

}

ul int get_bit_value(Bitmap *bitmap,int index)

 

int get_bit_value(Bitmap *bitmap,int index) 

{

     int                ret;

     int                byte_index;

     unsigned char      byte_value;

     unsigned char      inner_byte_index;

    

     if(bitmap==NULL || index >= bitmap->valid_length)  return -1;

     byte_index = index / 8;

     byte_value = bitmap->bitfield[byte_index];

     inner_byte_index = index % 8;

    

     byte_value = byte_value >> (7 - inner_byte_index);

     if(byte_value % 2 == 0) ret = 0;

     else ret = 1;

    

     return ret;

}

为了方便对get-bit-value函数的理解,可以假设某位图为2字节(其值为10110011 01010100),有效位数为14位,也就是待下载文件共有14个piece。位图第一个字节指明index为0~7的piece是否已下载,第二个字节指明index为8~13的piece是否已下载。现在要判断index为8的piece是否已经下载,也就是要获取位图第二个字节最高位的值。

ul int set_bit_value(Bitmap *bitmap,int index,unsigned char value)

 

int set_bit_value(Bitmap *bitmap,int index,unsigned char v)

{

     int     byte_index;

     unsigned charinner_byte_index;

    

     if(bitmapv==NULL || index >= bitmap->valid_length)  return -1;

     if((v != 0) && (v != 1))   return -1;

     byte_index = index / 8;

     inner_byte_index = index % 8;

     v = v << (7 - inner_byte_index);

     bitmap->bitfield[byte_index] = bitmap->bitfield[byte_index] | v;

    

     return 0;

}

ul int all_zero(Bitmap *bitmap)

 

int all_zero(Bitmap *bitmap)

{

     if(bitmap->bitfield == NULL)  return -1;

     memset(bitmap->bitfield,0,bitmap->bitfield_length);

     return 0;

}

ul int all_set(Bitmap *bitmap)

 

int all_set(Bitmap *bitmap)

{

     if(bitmap->bitfield == NULL)  return -1;

     memset(bitmap->bitfield,0xff,bitmap->bitfield_length);

     return 0;  

}

ul void release_memory_in_bitfield()

 

void release_memory_in_bitfield()

{

     if(bitmap->bitfield != NULL) free(bitmap->bitfield);

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

}

ul  int print_bitfield(Bitmap *bitmap)

 

int print_bitfield(Bitmap *bitmap)

{

     int i;

    

     for(i = 0; i < bitmap->bitfield_length; i++) {

          printf("%.2X ",bitmap->bitfield[i]); // 以16进制的方式打印每个位图中的字节

          if( (i+1) % 16 == 0)  printf("\n");  // 每行打印16个字节

     }

     printf("\n");

    

     return 0;

}

ul int restore_bitmap()

 

int restore_bitmap()

{

     int  fd;

     char bitmapfile[64];

    

     if( (bitmap == NULL) || (file_name == NULL) )  return -1;

     sprintf(bitmapfile,"�itmap",pieces_length);

     fd = open(bitmapfile,O_RDWR|O_CREAT|O_TRUNC,0666);

     if(fd < 0)  return -1;

     write(fd,bitmap->bitfield,bitmap->bitfield_length);

     close(fd);

    

     return 0;

}

ul int is_interested(Bitmap *dst,Bitmap *src)

 

int is_interested(Bitmap *dst,Bitmap *src)

{

     unsigned char const_char[8] = { 0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01};

     unsigned char c1, c2;

     int              i, j;

    

     if( dst==NULL || src==NULL )  return -1;

     if( dst->bitfield==NULL || src->bitfield==NULL )  return -1;

     if( dst->bitfield_length!=src->bitfield_length || dst->valid_length!=src-> valid_length )

          return -1;

     // 如果dst中某位为1而src对应为0,则说明src对dst感兴趣

     for(i = 0; i < dst->bitfield_length-1; i++) {

          for(j = 0; j < 8; j++) {  // 比较某个字节的所有位

               c1 = (dst->bitfield)[i] & const_char[j];  // 获取每一位的值

               c2 = (src->bitfield)[i] & const_char[j];

               if(c1>0 && c2==0)  return 1;

          }

     }

    

     j = dst->valid_length % 8;

     c1 = dst->bitfield[dst->bitfield_length-1];

     c2 = src->bitfield[src->bitfield_length-1];

     for(i = 0; i < j; i++) {  // 比较位图的最后一个字节

          if( (c1&const_char[i])>0 && (c2&const_char[i])==0 )

               return 1;

     }

     return 0;

}

以上函数的功能正确性测试代码如下:

// 测试时可以交换map1.bitfield和map2.bitfield的值或赋于其他值

Bitmap map1, map2;

unsigned char bf1[2] = { 0xa0, 0xa0 };  // 位图每一位的值为10100000 10100000

unsigned char bf2[2] = { 0xe0, 0xe0 };  // 位图每一位的值为11100000 11100000

 

map1.bitfield        = bf1;

map1.bitfield_length  = 2;

map1.valid_length    = 11;

map2.bitfield        = bf2;

map2.bitfield_length  = 2;

map2.valid_length    = 11;

 

int ret = is_interested(&map1,&map2);  

printf("%d\n",ret);

在编写模块时,测试其中的每一个函数是很有必要的,否则无法知道模块中每一个函数是否达到预期的功能。限于篇幅,不能列出每个模块的测试代码。由于每个模块的相对独立性,读者不妨编写一些测试代码来测试某些模块的代码。

ul int get_download_piece_num()

功能:获取当前已下载到的总piece数。函数实现代码如下:

int get_download_piece_num()

{

     unsigned char const_char[8] = { 0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01};

     int           i, j;

    

     if(bitmap==NULL || bitmap->bitfield==NULL)  return 0;

     download_piece_num = 0;

    

     for(i = 0; i < bitmap->bitfield_length-1; i++) {

          for(j = 0; j < 8; j++) {

               if( ((bitmap->bitfield)[i] & const_char[j]) != 0)

                    download_piece_num++;

          }

     }

    

     unsigned char c = (bitmap->bitfield)[i];  // c存放位图最后一个字节的值

     j = bitmap->valid_length % 8;         // j是位图最后一个字节的有效位数

     for(i = 0; i < j; i++) {

          if( (c & const_char[i]) !=0 ) download_piece_num++;

     }

     return download_piece_num;

}

4.3  出错处理模块的设计和实现

该模块由bterror.h和bterror.c文件构成,主要定义了一些错误类型,以及发生导致程序终止的致命性错误时程序的响应。

bterror.h

#ifndef BTERROR_H

#define BTERROR_H

 

#define FILE_FD_ERR                     -1  // 无效的文件描述符

#define FILE_READ_ERR                   -2  // 读文件失败

#define FILE_WRITE_ERR                  -3  // 写文件失败

#define INVALID_METAFILE_ERR            -4  // 无效的种子文件

#define INVALID_SOCKET_ERR          -5  // 无效的套接字

#define INVALID_TRACKER_URL_ERR     -6  // 无效的Tracker URL

#define INVALID_TRACKER_REPLY_ERR   -7  // 无效的Tracker回应

#define INVALID_HASH_ERR                -8  // 无效的hash值

#define INVALID_MESSAGE_ERR         -9  // 无效的消息

#define INVALID_PARAMETER_ERR       -10 // 无效的函数参数

#define FAILED_ALLOCATE_MEM_ERR     -11 // 申请动态内存失败

#define NO_BUFFER_ERR                   -12 // 没有足够的缓冲区

#define READ_SOCKET_ERR             -13 // 读套接字失败

#define WRITE_SOCKET_ERR                -14 // 写套接字失败

#define RECEIVE_EXIT_SIGNAL_ERR     -15 // 接收到退出程序的信号

 

// 用于提示致命性的错误,程序将终止

void btexit(int errno,char *file,int line);

 

#endif

以下是bterror.c文件:

bterror.c

#include

#include

#include

#include "bterror.h"

 

void btexit(i nt errno,char *file,int line)

{

     printf("exit at %s : %d with error number : %d\n",file, line, errno);

     exit(errno);

}

4.4  运行日志模块的设计和实现

本模块负责记录程序运行的日志,以备查询和分析程序行为,由log.h和log.c两个文件构成。

log.h

#ifndef  LOG_H

#define  LOG_H

#include

 

// 用于记录程序的行为

void logcmd(char *fmt,...);

 

// 打开日志文件

int init_logfile(char *filename);

 

// 将程序运行日志记录到文件

int logfile(char *file,int line,char *msg);

 

#endif

以下是log.c文件:

bterror.c

#include

#include

#include

#include

#include

#include

#include

#include "log.h"

// 日志文件的描述符

int logfile_fd = -1;

// 在命令行上打印一条日志

void logcmd(char *fmt,...)

{

     va_list ap;

    

     va_start(ap,fmt);

     vprintf(fmt,ap);

     va_end(ap);

}

// 打开记录日志的文件

int init_logfile(char *filename)

{

     logfile_fd = open(filename,O_RDWR|O_CREAT|O_APPEND,0666);

     if(logfile_fd < 0) {

          printf("open logfile failed\n");

          return -1;

     }

     return 0;

}

// 将一条日志写入日志文件

int logfile(char *file,int line,char *msg)

{

     char buff[256];

    

     if(logfile_fd < 0)  return -1;

     snprintf(buff,256,"%s:%d %s\n",file,line,msg);

     write(logfile_fd,buff,strlen(buff));

     return 0;

}

程序说明。

函数logcmd是一个变长参数的函数,也就是函数的参数个数是可变的,类似于printf函数。语句“logcmd(“%s:%d error\n”,__FILE__, __LINE__);”的功能与“printf(“%s:%d error\n”,__FILE__, __LINE__);”功能相同。

4.5  信号处理模块的设计和实现

在运行过程中,程序可能会接收到一些信号,如SIGINT、SIGTERM,这些信号的默认动作是立即终止程序。在信号处理模块,定义处理这些信号的函数。当信号产生时,系统自动调用相应的信号处理函数以便执行一些善后操作,如释放动态申请的内存、关闭文件描述符、关闭套接字。本模块由signal_hander.h和signal_hander.c两个文件构成。

signal_hander.h

#ifndef SIGNAL_HANDER_H

#define SIGNAL_HANDER_H

 

// 做一些清理工作,如释放动态分配的内存

void do_clear_work();

// 处理一些信号

void process_signal(int signo);

// 设置信号处理函数

int set_signal_hander();

 

#endif

以下是signal_hander.c文件:

signal_hander.c

#include

#include

#include

#include

#include "parse_metafile.h"

#include "bitfield.h"

#include "peer.h"

#include "data.h"

#include "tracker.h"

#include "torrent.h"

#include "signal_hander.h"

 

extern int  download_piece_num;

extern int  *fds;

extern int  fds_len;

extern Peer *peer_head;

 

 

void do_clear_work()

{

     // 关闭所有peer的socket

     Peer *p = peer_head;

     while(p != NULL) {

          if(p->state != CLOSING)  close(p->socket);

          p = p->next;

     }

     // 保存位图

     if(download_piece_num > 0) {

          restore_bitmap();

     }

     // 关闭文件描述符

     int i;

     for(i = 0; i < fds_len; i++) {

          close(fds[i]);

     }

     // 释放动态分配的内存

     release_memory_in_parse_metafile();

     release_memory_in_bitfield();

     release_memory_in_btcache();

     release_memory_in_peer();

     release_memory_in_torrent();

    

     exit(0);

}

 

void process_signal(int signo)

{

     printf("Please wait for clear operations\n");

     do_clear_work();

}

 

 

int set_signal_hander()

{

     if(signal(SIGPIPE,SIG_IGN) == SIG_ERR) {

          perror("can not catch signal:sigpipe\n");

          return -1;

     }

    

     if(signal(SIGINT,process_signal) == SIG_ERR) {

          perror("can not catch signal:sigint\n");

          return -1;

     }

    

     if(signal(SIGTERM,process_signal) == SIG_ERR) {

          perror("can not catch signal:sigterm\n");

          return -1;

     }

    

     return 0;

}

4.6  Peer管理模块的设计和实现

系统为每一个与之建立TCP连接的Peer构造一个Peer结构体。Peer管理模块负责管理由各个Peer结点构成的Peer链表,主要工作是创建结点,添加结点到Peer链表,从Peer链表中删除结点等。

peer.h

#ifndef PEER_H

#define PEER_H

#include

#include

#include "bitfield.h"

 

#define  INITIAL           -1  // 表明处于初始化状态

#define  HALFSHAKED       0    // 表明处于半握手状态

#define  HANDSHAKED       1    // 表明处于全握手状态

#define  SENDBITFIELD         2    // 表明处于已发送位图状态

#define  RECVBITFIELD    3    // 表明处于已接收位图状态

#define  DATA                4    // 表明处于与peer交换数据的状态

#define  CLOSING          5   // 表明处于即将与peer断开的状态

 

// 发送和接收缓冲区的大小,16K可以存放一个slice,2K用来存放其他消息

#define  MSG_SIZE  (2*1024+16*1024)

 

typedef struct _Request_piece {

     int     index;               // 请求的piece的索引

     int     begin;               // 请求的piece的偏移

     int     length;              // 请求的长度,一般为16KB

     struct _Request_piece *next;

} Request_piece;  // 定义数据请求队列的结点

 

typedef struct  _Peer {

     int            socket;                  // 通过该socket与peer进行通信

     char           ip[16];                  // peer的ip地址

     unsigned short   port;                    // peer的端口号

     char           id[21];                  // peer的id

    

     int            state;                    // 当前所处的状态

     int            am_choking;             // 是否将peer阻塞

     int            am_interested;         // 是否对peer感兴趣

     int            peer_choking;          // 是否被peer阻塞

     int            peer_interested;       // 是否被peer感兴趣

    

     Bitmap         bitmap;                 // 存放peer的位图

    

     char          *in_buff;                // 存放从peer处获取的消息

     int            buff_len;                // 缓存区in_buff的长度

     char          *out_msg;                // 存放将发送给peer的消息

     int            msg_len;                 // 缓冲区out_msg的长度

     char          *out_msg_copy;         // out_msg的副本,发送时使用该缓冲区

     int            msg_copy_len;         // 缓冲区out_msg_copy的长度

     int            msg_copy_index;        // 下一次要发送的数据的偏移量

    

     Request_piece  *Request_piece_head;       // 向peer请求数据的队列

     Request_piece  *Requested_piece_head;    // 被peer请求数据的队列

    

     unsigned int    down_total;              // 从该peer下载的总字节数

     unsigned int    up_total;                 // 向该peer上传的总字节数

    

     time_t         start_timestamp;          // 最近一次接收到peer消息的时间

     time_t         recet_timestamp;              // 最近一次发送消息给peer的时间

     time_t         last_down_timestamp;     // 最近下载数据的开始时间

     time_t         last_up_timestamp;       // 最近上传数据的开始时间

     long long      down_count;          // 本计时周期从peer下载的数据的字节数

     long long      up_count;           // 本计时周期向peer上传的数据的字节数

     float          down_rate;             // 本计时周期从peer处下载数据的速度

     float          up_rate;               // 本计时周期向peer处上传数据的速度

    

     struct _Peer   *next;

} Peer;

 

int   initialize_peer(Peer *peer);         // 对peer各个成员进行初始化

Peer* add_peer_node();                       // 添加一个peer结点

int   del_peer_node(Peer *peer);               // 删除一个peer结点

void  free_peer_node(Peer *node);              // 释放一个peer结点的内存

int   cancel_request_list(Peer *node);        // 撤消当前请求队列

int   cancel_requested_list(Peer *node);     // 撤消当前被请求队列

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

void  print_peers_data();                    // 打印peer链表中某些成员的值,用于调试

 

#endif

程序说明。

 

 

图13-3  状态转换图

 

(1)Peer结构体是整个程序最重要的数据结构,也是最复杂的数据结构。peer.h中首先定义了7种状态。各个状态的转换图如图13-3所示。

Halfshaked(半握手状态)是指已发送握手消息但未收到对方的握手消息,或者已经接收到对方的握手消息,但己方未发送握手消息。处于Data状态时双方可以交换数据,此时Peer结构体中的am_choking、am_interested、peer_choking、peer_interested4个成员变量有效。

如图13-4所示,当am_interested = 1,peer_choking = 0时,也就是客户端对peer感兴趣,而且peer没有将客户端阻塞,此时可以发送数据请求,即发送request消息请求peer发送数据,peer接收到请求后发送piece消息,数据就被封装在piece消息中。

如图13-5所示,当peer_interested = 1,am_choking = 0时也就是peer对客户端感兴趣,而且我们没有将该peer阻塞,此时如果peer发送request消息请求数据,则应该构造并发送piece消息,其中数据被封装在piece消息中。

图13-5中“发送unchoke消息”的时机是执行选择非阻塞peer算法时,选中该peer作为非阻塞peer或者选中该peer作为优化非阻塞peer。“发送choke消息”的时机类似。

 

图13-4  从下载数据的角度看到的处于Data状态时的内部状态

 

图13-5  从上传数据的角度看到的处于Data状态时的内部状态

“发送have消息,拥有了peer没有的piece”的含义是己方刚刚下载到一个piece,此时通过发送have消息告知所有peer客户端已拥有了某个piece,如果peer没有这个piece且原先peer对本客户端不感兴趣,则发送have消息后,该peer就对该客户端感兴趣了。

(2)Peer结构体中定义两个发送缓冲区out_msg和out_msg_copy,将刚刚生成的消息暂存在out_msg中,调用套接字函数send向peer发送消息时使用out_msg_copy缓冲区。out_msg_copy缓冲区的大小是18KB,而send函数一次最多发送1500字节,因此要使用msg_copy_index记录下次应该发送的数据的起始下标。事实上,send函数一次也可以发送超过1500字节的数据,不过若以这种方式发送会导致发送数据混乱,具体原因后面将会解释。其变量的含义请参考消息处理模块。

你可能感兴趣的:(TCP/IP)