对位图的操作主要在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;
}
该模块由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);
}
本模块负责记录程序运行的日志,以备查询和分析程序行为,由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__);”功能相同。
在运行过程中,程序可能会接收到一些信号,如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;
}
系统为每一个与之建立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字节的数据,不过若以这种方式发送会导致发送数据混乱,具体原因后面将会解释。其变量的含义请参考消息处理模块。