功能:对比不同 Linux 主机下两个目录的文件,找出其中存在不同的文件,用于文件同步。
开发过程中遇到的问题:
设计思路:遍历目录下的所有文件,获取文件信息,将文件信息保存到文件中,并通过 Socket 传输该文件对信息进行对比。
保存文件信息的数据结构选择自平衡二叉查找树。按照文件名的顺序,将文件信息保存在平衡二叉搜索树中。
选择二叉树的原因是当文件数量比较多时,进行插入、删除等操作比较方便。其他也可以使用排序数组或链表实现。
整个代码的头文件代码如下:
#ifndef __FILESYNC_H
#define __FILESYNC_H
#include
#include
#include
#define SYNC_SRC 1 //源地址
#define SYNC_DST 0 //目的地址
/**文件信息结构体*/
struct fileinfo_st {
time_t time; //文件最后修改时间
char *pwd; //文件路径
};
/**保存进文件的文件信息结构体*/
struct saveinfo_st {
time_t time; //文件最后修改时间
char pwd[260]; //文件路径
};
/**平衡二叉查找树节点*/
struct tree_node {
struct fileinfo_st data;
struct tree_node *left;
struct tree_node *right;
int height;
};
/**文件操作*/
#define TYPE_DELETE 0
#define TYPE_CHANGE 1
#define TYPE_CREATE 2
typedef struct tree_node * SEARCH_TREE;
/**avltree.c*/
void insert_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo);
void delete_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo);
/**filelist.c*/
int readfilelist(char *basepath, SEARCH_TREE *tree);
/**treecompare.c*/
void free_tree(SEARCH_TREE *tree);
void save_tree_to_file(int fd, SEARCH_TREE tree);
void compare_info(int outside, int inside, PTASK_INFO tran_task, int pos);
#endif
其中 SYNC_SRC、SYNC_DST 分别表示源地址和目的地址。由于进行目录对比的最终目的是实现文件同步,所以需要区分源地址和目的地址。
fileinfo_st 结构体是二叉树节点中的文件信息,其中包含 pwd、time 两个成员。其中 pwd 是带绝对路径的文件名,time 是文件的最后修改时间,通过这两个成员简单判断文件的不同。也可以添加其他属性进行判断。
saveinfo_st 结构体是要保存到文件中的信息,成员含义和 fileinfo_st 结构体相同。由于对比的目录位于不同的主机上,需要将二叉树上的文件信息保存到文件中,然后通过 Socket 传输出去进行对比。
tree_node 结构体是二叉树的节点,成员包含文件信息 data、左孩子 left、右孩子 right 和 节点深度 height。
功能接口的实现包含以下文件:
avltree.c:二叉树的操作;
filelist.c:获取目录下的文件信息;
treecompare.c:对文件信息进行对比。
二叉树的主要操作为向二叉树中插入或删除节点。
相关代码如下:
#include
#include
#include
#include "filesync.h"
/**
* 获取节点高度
* @param node 二叉树节点
* @return 返回节点的深度值,只有一个根节点的高度为0,空树的高度-1
*/
int node_height(struct tree_node *node)
{
return node==NULL ? -1 : node->height;
}
/**
* 比较两个数的值,获取最大值
* @param a 数值a
* @param b 数值b
* @return 返回最大值
*/
int max(int a,int b)
{
return a/**
* LL型平衡旋转。当tree的左子树的左子树上的节点使得tree的平衡度为2时,以tree为中心进行右旋。
* @param tree 进行右旋的根节点
*/
void LLrotate(SEARCH_TREE *tree)
{
struct tree_node *tmp;
tmp = (*tree)->left; //保存节点的左子树
(*tree)->left = tmp->right; //tmp节点的右子树作为tree的左子树
tmp->right = (*tree); //把tree作为tmp的右子树
//到此树旋转完成,更新树的深度,以tree,tmp为节点的树的深度发生了变化;
(*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right)) + 1;
tmp->height = max(node_height(tmp->left), node_height(tmp->right)) + 1;
//根节点发生了变化,由k1变为了k
(*tree) = tmp;
}
/**
* RR型平衡旋转。当tree的右子树的右子树上的节点使得tree的平衡度为-2时,以tree为中心进行左旋。
* @param tree 进行左旋的根节点
*/
void RRrotate(SEARCH_TREE *tree)
{
struct tree_node *tmp;
tmp = (*tree)->right; //保存节点的右子树
(*tree)->right = tmp->left;
tmp->left = (*tree);
//到此树旋转完成,更新树的深度,以tmp,tree为节点的树的深度发生了变化;
(*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right)) + 1;
tmp->height = max(node_height(tmp->left), node_height(tmp->right)) + 1;
//根节点发生了变化,由tree变为了tmp
(*tree) = tmp;
}
/**
* LR型平衡旋转。当tree的左子树的右子树上的节点使得T的平衡度为2时,进行二叉树的平衡
* @param tree 进行平衡的根节点
*/
void LRrotate(SEARCH_TREE *tree)
{
//先以tree的左子树为中心进行左旋
RRrotate(&((*tree)->left));
//再以tree为中心进行右旋
LLrotate(tree);
}
/**
* RL型平衡旋转。当tree的右子树的左子树上的节点使得T的平衡度为-2时,进行二叉树的平衡
* @param tree 进行平衡的根节点
*/
int RLrotate(SEARCH_TREE *tree)
{
//先以tree的右子树为中心进行右旋
LLrotate(&((*tree)->right));
//再以tree为中心进行左旋
RRrotate(tree);
}
/**
* 左平衡处理
* @param tree 进行平衡的根节点
*/
void left_balance(SEARCH_TREE *tree)
{
struct tree_node *tmp = (*tree)->left;
if(node_height(tmp->left) - node_height(tmp->right) == -1)
{
//右子树高于左子树,在右子树插入的
LRrotate(tree); //LR型平衡旋转
}
else
{
LLrotate(tree); //LL型平衡旋转
}
}
/**
* 右平衡处理
* @param tree 进行平衡的根节点
*/
void right_balance(SEARCH_TREE *tree)
{
struct tree_node *tmp = (*tree)->right;
if(node_height(tmp->right) - node_height(tmp->left) == -1)
{
//左子树比右子树高,说明在左子树插入的
RLrotate(tree); //RL型平衡旋转
}
else
{
RRrotate(tree); //RR型平衡旋转
}
}
/**
* 平衡二叉搜索树的插入操作
* @param tree 平衡二叉搜索树的根节点
* @param fileinfo 插入节点的文件信息
*/
void insert_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo)
{
//如果当前查找的根结点为空树,表明查无此结点,故插入结点。
if(NULL == *tree)
{
(*tree) = (struct tree_node *)malloc(sizeof(struct tree_node));
(*tree)->data = fileinfo;
(*tree)->height = 0;
(*tree)->left = NULL;
(*tree)->right = NULL;
}
else if(strcmp((*tree)->data.pwd, fileinfo.pwd) > 0) //在左子树插入
{
insert_node(&((*tree)->left), fileinfo);
//判断是否破坏AVL树的平衡性
if (node_height((*tree)->left) - node_height((*tree)->right) == 2)
left_balance(tree); //左平衡处理
}
else if(strcmp((*tree)->data.pwd, fileinfo.pwd) < 0) //在右子树插入
{
insert_node(&((*tree)->right), fileinfo);
//判断是否破坏AVL树的平衡性
if (node_height((*tree)->right) - node_height((*tree)->left) == 2)
right_balance(tree); //右平衡处理
}
else { //已有此结点,更新节点信息
free((*tree)->data.pwd);
(*tree)->data = fileinfo;
}
//更新树的高度
(*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right)) + 1;
}
/**
* 平衡二叉搜索树的删除操作
* @param tree 平衡二叉搜索树的根节点
* @param fileinfo 删除节点的文件信息
*/
void delete_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo)
{
if(NULL == (*tree)) //空树直接返回
return;
if(strcmp((*tree)->data.pwd, fileinfo.pwd) > 0) //在左子树中查找
{
delete_node(&((*tree)->left), fileinfo);
if(node_height((*tree)->right) - node_height((*tree)->left) == 2)
right_balance(tree); //树右平衡处理
}
else if(strcmp((*tree)->data.pwd, fileinfo.pwd) < 0) //在右子树中查找
{
delete_node(&((*tree)->right), fileinfo);
if(node_height((*tree)->left) - node_height((*tree)->right) == 2)
left_balance(tree); //树左平衡处理
}
else //找到要删除的元素节点
{
if((*tree)->left == NULL) //左子树为空
{
struct tree_node *tmp = *tree;
*tree = (*tree)->right; //用右孩子代替此节点
free(tmp->data.pwd);
free(tmp); //释放内存
}
else if((*tree)->right == NULL) //右子树为空
{
struct tree_node *tmp = *tree;
*tree = (*tree)->left; //用左孩子代替此节点
free(tmp->data.pwd);
free(tmp);
}
else //左右子树都不为空
{
//一般的删除策略是左子树的最小数据 或 右子树的最小数据 代替该节点
struct tree_node *tmp = (*tree)->left; //从左子树中查找
while(tmp->right != NULL)
tmp = tmp->right;
//此时的tmp指向左子树中的最大元素
(*tree)->data = tmp->data;
delete_node(&((*tree)->left), tmp->data); //递归的删除该节点
}
}
//更新节点的高度
if(*tree)
(*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right));
}
insert_node() 函数向二叉树中插入文件信息节点,参数为指向二叉树根节点地址的指针 tree,要插入的文件信息 fileinfo。
对节点信息进行修改,如修改文件的最后修改时间,同样调用该函数。
delete_node() 函数从二叉树中删除文件信息节点,参数与 insert_node() 函数项目。
其他几个函数为对二叉树进行平衡和计算节点深度的功能。
遍历要监视的目录,获取所有文件(包含目录)的信息,保存到二叉树的数据结构中。
#include
#include
#include
#include
#include
#include
#include
#include
#include "filesync.h"
/**
* 获取文件完整路径
* @param basepath 文件所在目录
* @param name 文件名
* @return 返回文件的完整路径
*/
char *get_pwd(const char *basepath, const char *name)
{
char *path;
path = (char *)malloc(strlen(basepath) + strlen(name) + 2);
sprintf(path, "%s/%s", basepath, name);
return path;
}
/**
* 获取文件信息
* @param pwd 文件路径
* @return 文件信息struct fileinfo_st结构体
*/
struct fileinfo_st get_fileinfo(char *pwd)
{
struct stat filestat;
struct fileinfo_st fileinfo;
int ret;
int fd;
ret = stat(pwd, &filestat);
if (ret != 0) {
perror("Get file infomation failed");
}
//文件最后修改时间
fileinfo.time = filestat.st_mtime;
//文件路径
fileinfo.pwd = pwd;
return fileinfo;
}
/**
* 获取监视目录下文件信息
* @param basepath 监视目录
* @param tree 保存文件信息的二叉树
* @return 成功返回0,失败返回-1
*/
int readfilelist(char *basepath, SEARCH_TREE *tree)
{
DIR *dir;
struct dirent *ptr;
char base[1000];
while ((dir=opendir(basepath)) == NULL)
{
perror("Open dir error...");
sleep(1);
}
while ((ptr=readdir(dir)) != NULL)
{
if(strcmp(ptr->d_name,".") == 0 || strcmp(ptr->d_name,"..") == 0) //忽略. 和 .. 文件
continue;
else if(ptr->d_type == 4) { //目录文件
struct fileinfo_st fileinfo = get_fileinfo(get_pwd(basepath, ptr->d_name));
insert_node(tree, fileinfo);
memset(base, '\0', sizeof(base));
strcpy(base, basepath);
strcat(base, "/");
strcat(base, ptr->d_name);
readfilelist(base, tree);
}
else {
struct fileinfo_st fileinfo = get_fileinfo(get_pwd(basepath, ptr->d_name));
insert_node(tree, fileinfo);
}
}
closedir(dir);
return 0;
}
readfilelist() 函数实现获取文件信息的功能,参数包含监视目录 basepath、指向二叉树根节点地址的指针 tree。
实现方式为打开监视目录后,通过 readdir() 函数获取目录下的所有文件,读取文件中所需要的信息。
其他函数,get_pwd() 函数功能为在文件名前加上绝对路径;fileinfo_st() 函数获取需要的文件信息,例如文件最后修改时间。
将二叉树中的节点信息保存到文件中,之后需要通过 Socket 发送该文件,例如从目的地址发送到源地址,源地址将收到的文件信息与自己的文件信息做对比,找出不同。
#include
#include
#include
#include "filesync.h"
/**
* 删除整个二叉树
* @param tree 保存文件信息的二叉树
*/
void free_tree(SEARCH_TREE *tree)
{
if ((*tree) != NULL) {
free_tree(&((*tree)->left));
free_tree(&((*tree)->right));
free((*tree)->data.pwd);
free((*tree));
}
}
/**
* 遍历二叉树,按照文件名顺序保存文件信息
* @param fd 保存文件信息的文件描述符
* @param tree 保存文件信息的二叉树
*/
void save_tree_to_file(int fd, SEARCH_TREE tree)
{
if (tree != NULL) {
save_tree_to_file(fd, tree->left);
struct saveinfo_st fileinfo;
fileinfo.time = tree->data.time;
memcpy(fileinfo.pwd, tree->data.pwd, (strlen(tree->data.pwd) + 1));
write(fd, &fileinfo, sizeof(struct saveinfo_st));
save_tree_to_file(fd, tree->right);
}
}
/**
* 通过 Socket 将存在不同的文件从源地址发送到目的地址
* @param pwd 发生修改的文件名
* @param type 对文件进行的操作。取值为以下参数之一:
* TYPE_DELETE:将文件从内网删除;
* TYPE_CHANGE:文件发生变化,将该文件从外网复制到内网
* TYPE_CREATE:文件被创建,将该文件从外网复制到内网
*/
void send_to_transfer(char *pwd, int type)
{
/**该部分代码实现不做展示*/
}
/**
* 对比外网和内网文件信息,将不同点进行文件传输
* @param outside 保存外网文件信息的文件描述符
* @param inside 保存内网文件信息的文件描述符
*/
void compare_info(int outside, int inside)
{
struct saveinfo_st outside_info;
struct saveinfo_st inside_info;
int outside_ret, inside_ret;
while (1) {
outside_ret = read(outside, &outside_info, sizeof(struct saveinfo_st));
inside_ret = read(inside, &inside_info, sizeof(struct saveinfo_st));
//外网信息文件和内网信息文件都读到了结尾
if (inside_ret == 0 && outside_ret == 0)
break;
//内网信息文件读到了结尾,外网信息文件没有读到结尾
else if (inside_ret == 0 && outside_ret != 0) {
do {
//发消息给文件传输模块
send_to_transfer(outside_info.pwd, TYPE_CREATE);
} while (read(outside, &outside_info, sizeof(struct saveinfo_st)) != 0);
break;
}
//外网信息文件读到了结尾,内网信息文件没有读到结尾
else if (outside_ret == 0 && inside_ret != 0) {
do {
//发消息给文件传输模块
send_to_transfer(inside_info.pwd, TYPE_DELETE);
} while (read(inside, &inside_info, sizeof(struct saveinfo_st)) != 0);
break;
}
//外网信息文件和内网信息文件都没有读到结尾
else {
//存在相同名字的文件
if (strcmp(outside_info.pwd, inside_info.pwd) == 0) {
//对比文件最后修改时间是否相等
if (outside_info.time == inside_info.time)
continue;
else
send_to_transfer(outside_info.pwd, TYPE_CHANGE);
continue;
}
//外网中有多的文件
if (strcmp(outside_info.pwd, inside_info.pwd) < 0) {
send_to_transfer(outside_info.pwd, TYPE_CREATE);
lseek(inside, 0 - sizeof(struct saveinfo_st), SEEK_CUR);
continue;
}
//内网中有多的文件
if (strcmp(outside_info.pwd, inside_info.pwd) > 0) {
send_to_transfer(inside_info.pwd, TYPE_DELETE);
lseek(outside, 0 - sizeof(struct saveinfo_st), SEEK_CUR);
continue;
}
}
}
}
save_tree_to_file() 函数将二叉树中的信息保存到文件中。
compare_info() 函数对比源地址和目的地址的信息文件。实际使用时区分为外网和内网,分别对应源地址和目的地址。参数为信息文件的文件描述符。
send_to_transfer() 函数的实现不做具体说明。基本实现方法为在源地址和目的地址之间建立 TCP 连接,先将文件名、文件大小等信息发送出去,然后发送文件内容,文件发送结束时再发送个结束的信息。
以上接口的调用流程如下:
两边的目录对比完后,只需要在源地址每隔一段时间获取下文件信息,和上一次的文件信息对比即可。
main() 函数的实现代码就不贴出来了,调用以上接口即可。
由于实际监视目录是挂载的 Windows 的共享目录,一些 Linux 下的文件同步工具自己测试时不支持,所以自己写了目录对比的功能。可能有其他文件同步工具自己没有找到,或者自己的目录对比上还存在问题,希望有大神指教。