一、文件同步
不同主机之间的文件同步是服务器开发过程中一个重要的基础操作,它是cp及scp的一个扩充版本,能够实现不同主机之间的文件复制,能够提供增量复制,能够提供安全性验证。rsync的代码实现数量并不多,核心的文件更少,和很多理论作者的操作一样,该文件使用C语言编写,代码的编写、变量的命令、文档的编写都不是很全面。rsync的原理我在之前的一篇文章中大致有描述,也就是说原理并没有复杂到大家需要花费很多时间来完成的程度。但是软件的实现就是这样(包括很多事情),你明白了并不代表牛就行了,在明白、熟练使用和实现之间是三种不同的等级,当我们在工程中遇到一个问题,希望知道一个确定答案的时候,大部分人只是通过一个简单的实验来验证这个功能是可用的。验证只是一个单例的验证过程,它不能对于可种可能的情况给出一个确定的答案。这篇博客基于问题是:当我们同步两个文件夹的时候,源和目的两个文件夹有若干个文件,在目的文件夹中,有若干个文件对于rsync提供的用户来说是不能修改的,而大量的其它文件是可以的修改同步的,此时rsycn是如何执行的?如果在同步的过程中发现权限问题,此时之后的文件是否会继续拷贝?父进程如何知道这种情况是否发生?因为这种情况对于一些关键的操作是致命的。这种交互的机制在代码中是如何实现的?客户端和服务器之间是如何制定协议的。
二、rsync的大致流程
1、同步源根据用户的输入,确定自己此次需要同步的文件列表,这些文件如果是递归的,会进一步展开各个文件夹以及各个实体文件、常规文件,客户端把这个文件列表发送给服务器,此时客户端会对各个需要同步的文件进行编号,编号从0开始,每个需要同步的文件和文件夹逐个一次编号、之后客户端和服务器之间通过这个编号来确定正在同步的文件。
2、对服务器来说,用户端创建的各种选项(例如update只更新服务器比客户端时间要老的文件,通过exit表示只更新服务器端已经存在的文件、或者本地文件)以及服务器端本地文件存在情况回应服务器各种文件的状态。这个状态下,有些文件可能根本不需要客户端真正的发送,这些文件其实在服务器和客户端之间根本不需要交互。此时同样还可能有其它问题,例如说指定的目的文件夹对指定的用户来说连打开的权限都没有,所以你甚至并不知道这个文件到底是否存在,更不用说更新了(这个甚至可以认为是一种错误情况)。 排除之前的情况,服务器将指定的文件的校验和、各个文件内不同文件块的签名逐个分块发送给客户端。
3、同步源的到这些信息之后,逐个对照本地文件的各个块,使用rsync算法中描述的算法对本地文件进行逐个比对,确定哪些块是源和目的是相同的,哪些是不同从而客户端需要逐个字节传送给同步目的的。
4、同步目的读取这些内容,根据本地之前存在的文件内容加上第3步发送的差异信息(相当于patch文件)生成一个tmp文件,这个文件就是同步源最终应该保存的文件。当这个tmp文件被写入完整之后,删除原始文件,通过rename将临时文件扶正,同步完成。在这一步同样可能出现问题,例如你根本没有权限在那个文件夹下创建tmp文件,此时之后的操作是否还会继续?
三、协议号的确定
协议号的确定在客户端和服务器最开始建立链接的时候就可以确定,它们执行的流程是相同的,就是在连接之后各自首先向交互端中写入自己的版本号(编译时确定),接着各自从交互接口中读出对方发送的版本号。之后的内容就有所不同,服务端写入一个随机的种子数值,客户端则纯粹读入。
四、发送文件列表
这个文件列表由rsync客户端启动的时候选项确定,例如通过-r的话各个子文件夹都需要包含,或者命令行中传入了各个文件夹等内容。客户端将这些信息收集之后,把这个列表文件名、修改时间、长度、文件权限等信息按照一定的格式发送给服务器。
客户端的发送流程为
client_run--->>>send_file_list--->>send_file_name--->>>send_file_entry
/* We must make sure we don't send a zero flag byte or the
* other end will terminate the flist transfer. Note that
* the use of XMIT_TOP_DIR on a non-dir has no meaning, so
* it's harmless way to add a bit to the first flag byte. */
if (protocol_version >= 28) {
if (!flags && !S_ISDIR(mode))
flags |= XMIT_TOP_DIR;
if ((flags & 0xFF00) || !flags) {
flags |= XMIT_EXTENDED_FLAGS;
write_byte(f, flags);
write_byte(f, flags >> 8);
} else
write_byte(f, flags);
} else {
if (!(flags & 0xFF) && !S_ISDIR(mode))
flags |= XMIT_TOP_DIR;
if (!(flags & 0xFF))
flags |= XMIT_LONG_NAME;
write_byte(f, flags);
}
这里首先发送一个标志位结构,根据这个结构标志位的不同,之后可以增加一个或者多个额外的实体内容,例如,如果传递XMIT_SAME_TIME标志位,那么就不会再传递文件修改时间字段。这个列表结构大家有兴趣的同学可以看看。
五、接收文件列表、生成文件签名及文件块签名
do_server_recv--->>>>do_recv
在该函数中,服务器端进行了一次fork,但是没有exec,相当于创建了一个子进程,执行了一个特殊的函数。这两个任务的分工也比较明确。此时一个用来将客户端发送的文件列表逐个遍历本地的情况,生成这些文件的签名,这个过程叫做generator(相对于sender、receiver,这三个在实现中都有各自对应的源文件)。主进程执行generate_files--->>>recv_generator
if (fd == -1) {
rsyserr(FERROR, errno, "failed to open %s, continuing",
full_fname(fnamecmp));
pretend_missing:
/* pretend the file didn't exist */
if (preserve_hard_links && hard_link_check(file, HL_SKIP))
return;
write_int(f_out,i);
write_sum_head(f_out, NULL);
return;
}
……
write_int(f_out,i);写回文件标号。
generate_and_send_sums(fd, st.st_size, f_out, f_copy);
其中generate_and_send_sums函数就会计算这个文件sum结构,这个sum包含了文件的长度、分块的大小(这个大小根据每个文件大小的不同而不同),
这个过程可以认为已经结束,现在看之前提出的问题,当文件状态不能打开的时候,此时服务器如何表现?如果对应的本地文件不存在或者不需要更新,此时服务器如何处理?
文件不存在时:
if (statret == -1) {
if (preserve_hard_links && hard_link_check(file, HL_SKIP))
return;
if (stat_errno == ENOENT) {
write_int(f_out,i);
if (!dry_run && !read_batch)
write_sum_head(f_out, NULL);可以看到,写入的sum结构是一个空指针,但是文件标号i是有写回的。
} else if (verbose > 1) {其它情况,例如权限问题,此时也没有写回文件编号,但是执行了rsyserr操作,大家不要忽略这个操作,它客户端返回值的正确性有决定性意义。
rsyserr(FERROR, stat_errno,
"recv_generator: failed to stat %s",
full_fname(fname));
}
return;
}
当忽略文件时
if (opt_ignore_existing && fnamecmp == fname) {
if (verbose > 1)
rprintf(FINFO, "%s exists
", safe_fname(fname));
return;
}
可以看到,没有向客户端写回文件编号及内容,同时打印FINFO信息给客户端,也就是在客户端执行的时候通过-v可以看到的一个输出。
六、客户端根据文件发送文件块and/or文本内容
客户端通过send_files函数完成文件内容的发送
while (1) {
unsigned int offset;
i = read_int(f_in);//只要对方发送文件只不是-1,就认为是一个合法的文件编号。这里也可以看到,对端没必要把所有的文件都返回,可以选择性要求发送哪些文件,对于stat错误的文件,在这里不会有发送。在之前的对端发送中,可以看到无论什么情况,对方是不会发送-1这个值过来的,当-1发送过来的时候,表示这个交互就已经结束。
if (i == -1) {
if (phase == 0) {
phase++;
csum_length = SUM_LENGTH;
write_int(f_out, -1);
if (verbose > 2)
rprintf(FINFO, "send_files phase=%d
", phase);
/* For inplace: redo phase turns off the backup
* flag so that we do a regular inplace send. */
make_backups = 0;
continue;
}
break;
}
if (i < 0 || i >= flist->count) {
rprintf(FERROR, "Invalid file index %d (count=%d)
",
i, flist->count);
exit_cleanup(RERR_PROTOCOL);
}
七、服务器生成文件
和之前说的一样,此时我们可以思考或者寻找一下如果此时对于文件夹没有写入权限,此时的mktimp文件就会失败,此时服务器如何处理这种情况。
recv_files
while (1) {
cleanup_disable();
i = read_int(f_in); 这里和客户端是对应的,同样是以文件编号开始。
if (i == -1) {
if (read_batch) {
if (next_gen_i != flist->count)
while (read_int(batch_gen_fd) != -1) {}
next_gen_i = -1;
}
if (phase)
break;
phase = 1;
csum_length = SUM_LENGTH;
if (verbose > 2)
rprintf(FINFO, "recv_files phase=%d
", phase);
send_msg(MSG_DONE, "", 0);
if (keep_partial)
make_backups = 0; /* prevents double backup */
continue;
}
当文件mktmp失败时
if (fd2 == -1) {
rsyserr(FERROR, errno, "mkstemp %s failed",
full_fname(fnametmp));同样是打印错误。
discard_receive_data(f_in, file->length);这一点也非常重要,因为这里并没有告诉对端这里出现了错误,所以客户端会始终把自己的文件diff内容发送过来,所以此时接收端要通过这个函数忽略到发送内容。
if (fd1 != -1)
close(fd1);
continue;
}
八、客户端如何知道服务器出现错误。
rsyserr--->>>rwrite这个内容是会发送个客户端的,而客户端在读取内容的时候是会感受到这个时间。内容为
read_int--->>>readfd--->>>readfd--->>>readfd_unbuffered
read_loop(fd, line, 4);
tag = IVAL(line, 0);
remaining = tag & 0xFFFFFF;
tag = (tag >> 24) - MPLEX_BASE;
case MSG_INFO:
case MSG_ERROR:
if (remaining >= sizeof line) {
rprintf(FERROR, "multiplexing overflow %d:%ld
",
tag, (long)remaining);
exit_cleanup(RERR_STREAMIO);
}
read_loop(fd, line, remaining);
rwrite((enum logcode)tag, line, remaining);
remaining = 0;
break;
在上面的rwrite
if (code == FERROR) {
log_got_error = 1;
f = stderr;
}
在进程退出的时候会判断这个返回值
client_run---->>>exit_cleanup(_exit_cleanup)--->>>
if (code == 0) {
if ((io_error & ~IOERR_VANISHED) || log_got_error)
code = RERR_PARTIAL;
else if (io_error)
code = RERR_VANISHED;
}
九、如何在字节流中区分提示信息和文件内容
这个可以这么想:文件块的长度一般都比较小,不会大于1<<24个字节,而不同块的编号是负数,这种FERR格式的组合是不再这个区间之内的,所以可以判断出哪些是新新协议中对方发送的一个提示信息FINFO,哪些是文件操作信息FIOERR,并且将之后的内容提取之后打印在客户端的标准输出中,这也就是我们看到的内容。