4.2 位图管理模块的设计和实现
对位图的操作主要在bitfield.h和bitfield.c中,负责创建位图,设置和获取位图某一位的值,保存位图等。
bitfield.h
#ifndef BITFIELD_H
#define BITFIELD_H
typedef struct _Bitmap {
} Bitmap;
int
int
int
// 设置某一位的值
int
int
void release_memory_in_bitfield();
int
int
int
int
#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 <stdio.h>
#include <unistd.h>
#include <string.h>
#include <malloc.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "parse_metafile.h"
#include "bitfield.h"
extern int
extern char *file_name;
Bitmap
int
程序说明。
(1)语句“extern int pieces_length;”声明了一个变量,这个变量是在parse_metafile.c中定义的全局变量。如果要在其他源文件中使用某个源文件中定义的变量,需要在使用该变量的源文件的头部以extern关键字声明。注意声明和定义的区别,声明仅仅是告知编译器有某个变量,而对于定义,编译器要分配内存空间来存储该变量的值。
(2)全局变量bitmap指向自己的位图,可以从位图中获知下载的进度。peer的位图存放在Peer结构体中。
ul int create_bitfield()
该函数较为简单,不另加注释
int create_bitfield()
{
} else {
}
ul int get_bit_value(Bitmap *bitmap,int index)
int get_bit_value(Bitmap *bitmap,int index)
{
}
为了方便对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)
{
}
ul int all_zero(Bitmap *bitmap)
int all_zero(Bitmap *bitmap)
{
}
ul int all_set(Bitmap *bitmap)
int all_set(Bitmap *bitmap)
{
}
ul void release_memory_in_bitfield()
void release_memory_in_bitfield()
{
}
ul
int print_bitfield(Bitmap *bitmap)
{
}
ul int restore_bitmap()
int restore_bitmap()
{
}
ul int is_interested(Bitmap *dst,Bitmap *src)
int is_interested(Bitmap *dst,Bitmap *src)
{
}
以上函数的功能正确性测试代码如下:
// 测试时可以交换map1.bitfield和map2.bitfield的值或赋于其他值
Bitmap map1, map2;
unsigned char bf1[2] = { 0xa0, 0xa0 };
unsigned char bf2[2] = { 0xe0, 0xe0 };
map1.bitfield
map1.bitfield_length
map1.valid_length
map2.bitfield
map2.bitfield_length
map2.valid_length
int ret = is_interested(&map1,&map2);
printf("%d\n",ret);
在编写模块时,测试其中的每一个函数是很有必要的,否则无法知道模块中每一个函数是否达到预期的功能。限于篇幅,不能列出每个模块的测试代码。由于每个模块的相对独立性,读者不妨编写一些测试代码来测试某些模块的代码。
ul int get_download_piece_num()
功能:获取当前已下载到的总piece数。函数实现代码如下:
int get_download_piece_num()
{
}
4.3 出错处理模块的设计和实现
该模块由bterror.h和bterror.c文件构成,主要定义了一些错误类型,以及发生导致程序终止的致命性错误时程序的响应。
bterror.h
#ifndef BTERROR_H
#define BTERROR_H
#define FILE_FD_ERR
#define FILE_READ_ERR
#define FILE_WRITE_ERR
#define INVALID_METAFILE_ERR
#define INVALID_SOCKET_ERR
#define INVALID_TRACKER_URL_ERR
#define INVALID_TRACKER_REPLY_ERR
#define INVALID_HASH_ERR
#define INVALID_MESSAGE_ERR
#define INVALID_PARAMETER_ERR
#define FAILED_ALLOCATE_MEM_ERR
#define NO_BUFFER_ERR
#define READ_SOCKET_ERR
#define WRITE_SOCKET_ERR
#define RECEIVE_EXIT_SIGNAL_ERR
// 用于提示致命性的错误,程序将终止
void btexit(int errno,char *file,int line);
#endif
以下是bterror.c文件:
bterror.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "bterror.h"
void btexit(i nt errno,char *file,int line)
{
}
4.4 运行日志模块的设计和实现
本模块负责记录程序运行的日志,以备查询和分析程序行为,由log.h和log.c两个文件构成。
log.h
#ifndef
#define
#include <stdarg.h>
// 用于记录程序的行为
void logcmd(char *fmt,...);
// 打开日志文件
int init_logfile(char *filename);
// 将程序运行日志记录到文件
int logfile(char *file,int line,char *msg);
#endif
以下是log.c文件:
bterror.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include "log.h"
// 日志文件的描述符
int logfile_fd = -1;
// 在命令行上打印一条日志
void logcmd(char *fmt,...)
{
}
// 打开记录日志的文件
int init_logfile(char *filename)
{
}
// 将一条日志写入日志文件
int logfile(char *file,int line,char *msg)
{
}
程序说明。
函数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 <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#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
extern int
extern int
extern Peer *peer_head;
void do_clear_work()
{
}
void process_signal(int signo)
{
}
int set_signal_hander()
{
}
4.6 Peer管理模块的设计和实现
系统为每一个与之建立TCP连接的Peer构造一个Peer结构体。Peer管理模块负责管理由各个Peer结点构成的Peer链表,主要工作是创建结点,添加结点到Peer链表,从Peer链表中删除结点等。
peer.h
#ifndef PEER_H
#define PEER_H
#include <string.h>
#include <time.h>
#include "bitfield.h"
#define
#define
#define
#define
#define
#define
#define
// 发送和接收缓冲区的大小,16K可以存放一个slice,2K用来存放其他消息
#define
typedef struct _Request_piece {
} Request_piece;
typedef struct
} Peer;
int
Peer* add_peer_node();
int
void
int
int
void
void
#endif
程序说明。
图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
图13-5
“发送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字节的数据,不过若以这种方式发送会导致发送数据混乱,具体原因后面将会解释。其变量的含义请参考消息处理模块。