rsync源码解析 (7) 客户端/服务器通信协议

在上一章 文件属性与元数据处理 中,我们探索了 rsync 如何细致地处理文件的各种"标签",如权限、所有者和时间戳,确保同步的完美复刻。至此,我们已经了解了 rsync 如何处理单个文件系统上的任务。

但是,当 rsync 跨越网络,在两台相隔遥远的机器之间工作时,这一切是如何协调的呢?本章,我们将揭开 rsync 的另一大核心------它的通信协议。这套协议是 rsync 能够作为一个稳定、高效的网络工具的基石。

对讲机通话法则

想象一下,你和一位远方的朋友需要通过一对老式对讲机进行一次重要且复杂的对话。为了避免混乱,你们会事先约定一套规则:

  1. 对上频道:通话开始前,你们会先确认双方都在同一个频道上。如果你在频道 7,而他在频道 8,你们就无法沟通。
  2. 确认身份:"呼叫基地,我是游隼,听到请回答。"在交换敏感信息前,你们会用呼号来确认对方的身份。
  3. 统一格式:所有信息,无论是紧急求助、日常报告还是简单的"收到",你们都会用固定的格式进行。比如,每句话说完都加一个"完毕",这样对方就知道可以开始讲话了,避免了两人同时说话造成的干扰。

Rsync 的客户端和服务器进程之间的通信,就遵循着这样一套严谨的"对讲机法则"。在开始传输任何数据之前,它们会进行一次"握手",协商一个双方都支持的协议版本(对上频道)。如果需要,它们还会进行身份验证。之后,所有的数据,无论是文件内容、错误信息还是控制指令,都会被打包成特定格式的消息,通过一个单一的网络连接进行传输。这个过程,我们称之为多路复用

核心流程:一次远程同步的对话

让我们以一个典型的远程同步命令为例,看看这场"对话"是如何展开的:

bash 复制代码
rsync -av local_dir/ rsync://your-server.com/backup_module/

这行命令触发了一系列网络交互,主要可以分为三个阶段。

阶段一:握手与认证(建立连接)

这是 rsync 客户端和服务端(守护进程 rsyncd)初次接触的阶段。

sequenceDiagram participant C as 客户端 participant S as 服务器 (rsyncd) C->>S: 建立 TCP 连接 (默认端口 873) S->>C: 发送问候语: "@RSYNCD: 31.0" (我能说的最高版本是 31) C->>S: 回复问候语: "@RSYNCD: 32.0" (我能说的最高版本是 32) Note over C,S: 双方协商,决定使用较低的协议版本 31 C->>S: 请求访问模块: "backup_module" alt 模块需要认证 S->>C: 发送认证请求: "@RSYNCD: AUTHREQD challenge_string" C->>S: 发送用户名和加密后的密码 S->>S: 验证密码 alt 验证成功 S->>C: 确认: "@RSYNCD: OK" else 验证失败 S->>C: 错误并断开连接 end else 模块无需认证 S->>C: 确认: "@RSYNCD: OK" end

这个阶段的核心是协商出一个双方都能理解的"共同语言"------协议版本。Rsync 具有优秀的向后兼容性,正是得益于这个初始的握手环节。

深入代码:协议协商

这个握手过程主要在 clientserver.cexchange_protocols 函数中实现。

服务器端(作为接收方)首先表明自己的能力:

c 复制代码
// 文件: clientserver.c (服务器端简化逻辑)
void output_daemon_greeting(int f_out, int am_client)
{
	int our_sub = get_subprotocol_version(); // 获取自己的子版本号
	// ...
	// 发送自己的最高协议版本号
	io_printf(f_out, "@RSYNCD: %d.%d %s\n", protocol_version, our_sub, ...);
}

客户端 (作为发起方)收到后,会进行比较,并调整自己的 protocol_version 全局变量。

c 复制代码
// 文件: clientserver.c (客户端简化逻辑)
static int exchange_protocols(...)
{
    // ... 从服务器读取问候语,解析出 remote_protocol ...
    if (sscanf(buf, "@RSYNCD: %d.%d", &remote_protocol, &remote_sub) < 1) {
        // ... 错误处理 ...
        return -1;
    }

    // 如果我的版本比服务器高,就降级到服务器的版本
    if (protocol_version > remote_protocol) {
        protocol_version = remote_protocol;
    }
    // ... 其他更复杂的版本比较 ...

    return 0;
}

通过这个简单的交换,确保了即使是不同版本的 rsync 也能尽可能地协同工作。

阶段二:启动多路复用(切换到消息模式)

一旦握手成功,双方就像按下了对讲机上的通话按钮,切换到了"消息模式"。从此刻起,所有通信都将通过 io.c 中实现的多路复用机制进行。

这个机制的核心是 send_msgread_a_msg 函数。所有数据都被封装成带有 4 字节头部的消息。这个头部包含了:

  • 消息类型 (Tag) : 一个字节,表明这是什么类型的消息(如 MSG_DATAMSG_ERROR)。
  • 消息长度 (Length): 三个字节,表明后面跟着多少数据。
c 复制代码
// 文件: io.c (发送消息的简化逻辑)
int send_msg(enum msgcode code, const char *buf, size_t len, int convert)
{
    char *hdr;
    // ... 在发送缓冲区 (iobuf.msg) 中找到空间 ...

    // 写入 4 字节的消息头
    SIVAL(hdr, 0, ((MPLEX_BASE + (int)code) << 24) + len);

    // ... 将 buf 的内容写入发送缓冲区 ...
    return 1;
}

这样,一个单一的 TCP 流就可以同时承载多种不同类型的数据,而不会混淆。

阶段三:执行同步任务(交换消息)

现在,所有在前面章节学到的 rsync 核心逻辑------文件列表传输、增量传输算法等------都开始运行。但它们的所有输入和输出,都变成了对 read_...write_... 函数的调用,这些函数最终会将数据打包成消息进行收发。

对话流程大致如下:

  1. 发送者 :通过一系列 MSG_DATA 消息,将文件列表发送给接收者。
  2. 接收者 :接收并解析这些 MSG_DATA 消息,在内存中重建文件列表。
  3. 接收者 (生成器进程):对于需要更新的文件,计算校验和,并通过 MSG_DATA 消息发回给发送者。
  4. 发送者 :接收校验和,计算差异,然后将差异数据(新数据块和匹配指令)通过 MSG_DATA 消息发送给接收者。
  5. 接收者(接收者进程):根据收到的差异数据重建文件。
  6. 期间 :如果任何一方发生错误,比如"权限不足",它会发送一个 MSG_ERROR 消息。如果连接空闲时间过长,一方会发送一个空内容的 MSG_DATA 消息作为"心跳包"(Keep-alive),防止连接被防火墙断开。

整个过程由 io.c 中的 perform_io 函数驱动,它在一个循环中处理输入和输出,确保数据流畅通无阻。

深入代码:消息的读取与分发

当数据到达时,read_a_msg 函数负责解析。

c 复制代码
// 文件: io.c (读取消息的简化逻辑)
static void read_a_msg(void)
{
    // 读取 4 字节的消息头
    int tag = raw_read_int();

    // 解析出消息类型 (code) 和长度 (msg_bytes)
    size_t msg_bytes = tag & 0xFFFFFF;
    tag = (tag >> 24) - MPLEX_BASE;

    // 根据消息类型进行不同的处理
    switch (tag) {
    case MSG_DATA:
        // 标记接下来 msg_bytes 的数据是原始文件数据
        iobuf.raw_input_ends_before = iobuf.in.pos + msg_bytes;
        break;
    case MSG_ERROR:
    case MSG_INFO:
        // 读取错误或信息字符串,并打印到日志
        raw_read_buf(data, msg_bytes);
        rwrite((enum logcode)tag, data, msg_bytes, ...);
        break;
    case MSG_SUCCESS:
        // 表示一个文件成功传输
        // ...
        break;
    // ... 其他消息类型的处理 ...
    }
}

这个 switch 语句就像一个总调度台,根据收到的消息类型,将数据分发给正确的处理程序。

总结

在本章中,我们揭开了 rsync 作为网络工具的通信秘密。我们了解到,看似简单的远程同步命令背后,是一套复杂而稳健的通信协议。

  • 握手先行:通信始于一次"握手",客户端和服务器通过协商确定一个双方都支持的协议版本,为后续所有通信奠定了基础。
  • 身份验证:对于需要权限控制的模块,rsync 会通过"挑战-应答"机制进行安全的身份验证。
  • 一切皆消息:握手之后,所有数据,无论是文件内容、元数据、错误信息还是控制信号,都被封装成带有类型和长度的消息。
  • 单连接多路复用 :所有这些不同类型的消息都在一个单一的 TCP 连接上进行传输,由 io.c 中的多路复用层进行管理,确保数据有序、不混淆。

至此,我们已经完成了 rsync 源码学习之旅的全部核心章节。从最开始的选项与配置解析,到其赖以成名的增量传输算法,再到独特的三进程角色模型,以及文件列表、过滤规则和元数据处理等关键机制,最后到本章的网络通信协议,我们已经全面探索了 rsync 这款强大工具的内部构造。

希望这一系列章节能够帮助你理解 rsync 设计的精妙之处,并为你深入探索其他优秀开源项目提供灵感和信心。恭喜你完成了这段旅程!

相关推荐
重启的码农17 小时前
rsync源码解析 (3) 进程角色 (Sender/Receiver/Generator)
源码
重启的码农17 小时前
rsync源码解析 (6) 文件属性与元数据处理
源码
重启的码农17 小时前
rsync源码解析 (5) 文件过滤规则系统
源码
重启的码农17 小时前
rsync源码解析 (4) 文件列表 (File List)
源码
前端双越老师18 小时前
为何前端圈现在不关注源码了?
面试·前端框架·源码
RPA+AI十二工作室1 天前
影刀RPA_抖音评价获取_源码解读
运维·机器人·自动化·源码·rpa·影刀
RPA+AI十二工作室1 天前
影刀RPA_Temu关键词取数_源码解读
大数据·自动化·源码·rpa·影刀
重启的码农2 天前
rsync源码解析 (2) 增量传输算法
源码
重启的码农2 天前
rsync源码解析 (1) 选项与配置解析
源码