通过Rsync实现文件远程备份

通过Rsync实现文件远程备份

一、概述

Linux实现文件远程同步备份,可以考虑的方案是scp和rsync。
比较常用的方案包括:
1、nfs实现web数据共享
2、rsync+inotify实现数据同步
3、rsync+sersync实现数据同步
4、unison+inotify实现数据双向同步
5、lsyncd实现数据同步

rsync官网 https://rsync.samba.org/


rsync使用角色包括客户端(SRC)和服务端(DEST),客户端和服务端都需要安装rsync,服务端需要启动rsync服务。

二、使用说明

1.安装和常用命令说明

yum方式安装
yum -y install rsync

查看配置文件路径
rpm -qc rsync

查看版本
rsync --version

守护进程启动rsync
rsync --daemon


源码编译安装
wget https://download.samba.org/pub/rsync/src/rsync-3.2.3.tar.gz
tar zxf rsync-3.2.3.tar.gz
cd rsync-3.2.3
./configure
make
make install

2.快速使用示例

从本地上传到服务器
rsync -avz --delete --progress --password-file=/etc/rsync_client.passwd /data/backup/tmp/ rsync_backup@192.168.1.100::ossbackup

从服务器端下载到本地
rsync -avz --delete --progress --password-file=/etc/rsync_client.passwd rsync_backup@192.168.1.100::ossbackup /data/backup/tmp/

可以发现,上传和下载仅是src和dest参数位置对调


对应rsync服务端配置示例/etc/rsyncd.conf:
uid = root
gid = root
use chroot = no
max connections = 10 
timeout = 900
ignore nonreadable = yes
log file=/var/log/rsyncd.log
pid file=/var/run/rsyncd.pid
lock file=/var/run/rsyncd.lock
dont compress=*.gz *.tgz *.zip *.z *.Z *.rpm *.deb *.bz2
[mysqlbackup]
comment=MySQL remote backup
path=/data/mysqldb_backup
ignore errors=yes
hosts allow=192.168.1.101
auth users=rsync_backup
secrets file=/etc/rsyncd.passwd
list=false
read only=no
[ossbackup]
comment=oss remote backup
path=/data/oss_files
ignore errors=yes
hosts allow=192.168.1.102
auth users=rsync_backup
secrets file=/etc/rsyncd.passwd
list=false
read only=no

3.参数说明

daemon(服务端)模式

用途: rsync --daemon [选项]...
 
选项
 --address=ADDRESS 绑定到指定的地址
 --bwlimit=RATE 限制套接字I/O带宽
 --config=FILE 不使用默认位置的rsyncd.conf文件,额外指定
 -M, --dparam=OVERRIDE 覆盖全局守护进程配置参数
 --no-detach 不要进行fork并后台运行
 --port=PORT 监听指定的端口
 --log-file=FILE 输出日志到指定文件
 --log-file-format=FMT 用指定格式更新日志
 --sockopts=OPTIONS 指定自定义的TCP选项
 -v, --verbose 详细模式输出
 -4, --ipv4 偏向于使用IPv4
 -6, --ipv6 偏向于使用IPv6
 --help 显示帮助信息

客户端参数

用途: rsync [OPTION]... SRC [SRC]... DEST
通过远程shell访问方式:
rsync [选项]... [用户名@]HOST:SRC [DEST]
rsync [选项]... SRC [SRC]... [用户名@]HOST:DEST
通过rsync daemon访问方式:
rsync [选项]... [用户名@]HOST::SRC [DEST]
rsync [选项]... SRC [SRC]... [用户名@]HOST::DEST
rsync [选项]... SRC [SRC]... rsync://[用户名@]HOST[:PORT]/DEST
rsync [选项]... rsync://[USER@]HOST[:PORT]/SRC [DEST]
 
请注意,':'会使用ssh来远程连接,而'::'以及'rsync://'则用于以tcp方式连接一个rsync daemon服务器,这个需要SRC(源),以及包含模块名字的DEST(目的)
 
选项
 -v, --verbose 详细模式输出
 --info=FLAGS 输出INFO级别
 --debug=FLAGS 输出DEBUG级别
 --msgs2stderr 用于调试的特殊输出处理
 -q, --quiet 忽略非error的输出
 --no-motd 忽略Daemon模式的MOTD
 -c, --checksum 让自动跳过基于校验和而非默认的修改时间以及文件大小
 -a, --archive 归档(压缩)模式,表示以递归方式传输文件,并保持所有文件属性等同于-rlptgoD(无 -H,-A,-X)
 --no-OPTION 关闭隐含的选项(例如 --no-D)
 -r, --recursive 对子目录以递归模式处理
 -R, --relative 使用相对路径信息
 --no-implied-dirs 不使用--relative发送隐含的目录
 -b, --backup 创建备份,也就是对于目的已经存在有同样的文件名时,将老的文件重新命名为~filename.可以使用--suffix选项来指定不同的备份文件前缀
 --backup-dir=DIR 将备份文件(如~filename)存放在指定目录下
 --suffix=SUFFIX 定义备份文件前缀,默认是~
 -u, --update 仅仅进行更新,也就是跳过所有已经存在于DST,并且文件时间晚于要备份的文件(不覆盖更新的文件)
 --inplace update destination files in-place (SEE MAN PAGE)
 --append 将数据附加到较短的文件
 --append-verify 类似--append,但是对旧数据会计算校验和
 -d, --dirs 不使用递归传输目录
 -l, --links 不处理符号链接(保留符号链接)
 -L, --copy-links 将符号链接处理为具体的文件或者文件夹
 --copy-unsafe-links 只处理不安全的符号链接
 --safe-links 忽略不在SRC源目录的符号链接
 --munge-links munge符号链接使它们更安全(但会无法使用)
 -k, --copy-dirlinks 把指向文件夹的符号链接转换为文件夹
 -K, --keep-dirlinks 把接收端的指向文件夹的符号链接当做文件夹
 -H, --hard-links 保留硬链接
 -p, --perms 保留权限
 -E, --executability 保留文件的可执行属性
 --chmod=CHMOD 影响文件或文件夹的属性
 -A, --acls 保留ACLs (代表--perms)
 -X, --xattrs 保留扩展属性
 -o, --owner 保留所有者(仅限superuser)
 -g, --group 保留组
 --devices 保留设备文件(仅限superuser)
 --copy-devices 把设备文件内容当做文件一样进行复制处理
 --specials 保留特殊文件
 -D 和--devices --specials一样
 -t, --times 保留修改时间
 -O, --omit-dir-times 忽略文件夹的修改时间
 -J, --omit-link-times 忽略符号链接的修改时间
 --super 接收端尝试使用superuser进行操作
 --fake-super 使用xattrs来存储和恢复权限属性
 -S, --sparse 对稀疏文件进行特殊处理以节省空间
 --preallocate 在写入前预分配DST文件
 -n, --dry-run 执行一个没有实际更改的试运行,只会显示文件会被如何操作
 -W, --whole-file 拷贝文件,不进行增量检测
 -x, --one-file-system 不要跨越文件系统边界
 -B, --block-size=SIZE 检验算法使用的块尺寸,默认是700字节
 -e, --rsh=COMMAND 指定使用rsh,ssh方式进行数据同步
 --rsync-path=PROGRAM 指定远程服务器上的rsync命令所在路径
 --existing 仅仅更新那些已经存在于DST的文件,而不备份那些新创建的文件 
 --ignore-existing 跳过更新已存在于DST的文件
 --remove-source-files 发送方删除非文件夹的源文件
 --del --delete-during的一个alias
 --delete 删除那些DST中SRC没有的文件
 --delete-before 传输前删除,而非传输过程中
 --delete-during 在传输过程中删除
 --delete-delay 在传输过程中确定要删除的,在传输结束后进行删除
 --delete-after 在传输结束后删除,而非传输过程中
 --delete-excluded 同样删除接收端那些被该选项指定排除的文件
 --ignore-missing-args 忽略丢失的源参数不输出错误
 --delete-missing-args 从DEST删除丢失的源参数
 --ignore-errors 即使出现I/O错误也进行删除
 --force 即使文件夹非空也强制删除
 --max-delete=NUM 不删除超过指定数量的文件
 --max-size=SIZE 不传输超过指定大小的文件
 --min-size=SIZE 不传输小于指定大小的文件
 --partial 保留那些因故没有完全传输的文件,以是加快随后的再次传输(即断点续传)
 --partial-dir=DIR 将因故没有完全传输的文件放到指定文件夹
 --delay-updates 在传输末尾把所有更新的文件放到位
 -m, --prune-empty-dirs 从文件列表中删除空目录链
 --numeric-ids 不要把uid/gid值映射为用户/组名
 --usermap=STRING 自定义用户名映射
 --groupmap=STRING 自定义组名映射
 --chown=USER:GROUP 简单的用户/组名映射
 --timeout=SECONDS 设置I/O超时,单位为秒
 --contimeout=SECONDS 设置Daemon连接超时,单位为秒
 -I, --ignore-times 不跳过那些有同样的时间和大小的文件
 -M, --remote-option=OPTION 只把指定选项发送到远端
 --size-only 只跳过大小相同的文件
 --modify-window=NUM 决定文件是否时间相同时使用的时间戳窗口,默认为0
 -T, --temp-dir=DIR 在指定文件夹中创建临时文件
 -y, --fuzzy 如果DEST没有任何文件,查找类似的文件
 --compare-dest=DIR 同样比较DIR中的文件来决定是否需要备份
 --copy-dest=DIR 和上面的类似,但是还会复制指定文件夹中的没有改变的文件
 --link-dest=DIR 和上面类似,只是没有改变的文件会被硬链接到DST
 -z, --compress 在传输过程中进行压缩
 --compress-level=NUM 指定压缩级别0-9,默认为6
 --skip-compress=LIST 跳过压缩文件后缀在指定列表中的文件
 -C, --cvs-exclude 自动跳过CVS的生成文件
 -f, --filter=RULE 添加一个文件过滤规则
 -F 等于--filter='dir-merge /.rsync-filter'
    重复的: --filter='- .rsync-filter'
 --exclude=PATTERN 排除符合匹配规则的文件
 --exclude-from=FILE 从指定文件中读取需要排除的文件
 --include=PATTERN 包含(不排除)符合匹配规则的文件
 --include-from=FILE 从指定文件中读取需要包含(不排除)的文件
 --files-from=FILE 从指定文件中读取SRC源文件列表
 -0, --from0 从文件中读取的文件名以''终止
 -s, --protect-args 没有空格分隔;只有通配符的特殊字符
 --address=ADDRESS 绑定到指定的地址
 --port=PORT 指定其他的rsync服务端口
 --sockopts=OPTIONS 指定自定义的TCP选项
 --blocking-io 对远程shell使用阻塞IO
 --stats 提供某些文件的传输状态
 -8, --8-bit-output 在输出中留下高比特的字符
 -h, --human-readable 用人类可读的格式输出数字
 --progress 在传输过程中显示进度
 -P 等同于--partial --progress
 -i, --itemize-changes 输出对所有更新的变更摘要
 --out-format=FORMAT 用指定格式输出更新
 --log-file=FILE 将日志保存到指定文件
 --log-file-format=FMT 用指定格式更新日志
 --password-file=FILE 从文件读取Daemon服务器密码
 --list-only 不复制而是只列出
 --bwlimit=RATE 限制套接字I/O带宽
 --outbuf=N|L|B 设置输出缓冲,为None,Line或者Block
 --write-batch=FILE 写入批量更新到指定文件
 --only-write-batch=FILE 和上面类似,但是对DST进行只写的更新
 --read-batch=FILE 从指定文件读取一个批量更新
 --protocol=NUM 强制使用指定的老版本协议
 --iconv=CONVERT_SPEC 对文件名进行字符编码转换
 --checksum-seed=NUM 设置块/文件的校验和种子
 -4, --ipv4 偏向于使用IPv4
 -6, --ipv6 偏向于使用IPv6
 --version 打印版本号
(-h) --help 显示帮助信息

服务端全局参数(rsyncd.conf)

在文件中[modlue模块名]之前的所有参数都是全局参数,当然也可以在全局参数部分定义模块参数,这时候该参数的值就是所有模块的默认值。

全局参数 描述
motd file motd file
log file 指定rsync的日志文件,而不将日志发送给syslog。
pid file 指定rsync的pid文件。
syslog facility 指定rsync发送日志消息给syslog时的消息级别,常见的消息级别是:uth, authpriv, cron, daemon, ftp, kern, lpr, mail, news, security, sys-log, user, uucp, local0, local1, local2, local3,local4, local5, local6和local7。默认值是daemon。

服务端模块参数(rsyncd.conf)

在全局参数之后就需要定义一个或多个模块了,模块中可以定义以下参数:模块以[模块名]开始,直到另一个模块的开始结束

模块参数 描述
comment 给模块指定一个描述,该描述连同模块名在客户连接得到模块列表时显示给客户。默认没有描述定义。
path 指定该模块的供备份的目录树路径,该参数是必须指定的
use chroot 如果"use chroot"指定为true,那么rsync在传输文件以前首先chroot到path参数所指定的目录下。这样做的原因是实现额外的安全防护,但是缺点是需要以roots权限,并且不能备份指向外部的符号连接所指向的目录文件。默认情况下chroot值为true。推荐:?
max connections 指定该模块的最大并发连接数量以保护服务器,超过限制的连接请求将被告知随后再试。默认值是0,也就是没有限制。
lock file 指定支持max connections参数的锁文件,默认值是/var/run/rsyncd.lock
read only 该选项设定是否允许客户上载文件。如果为true那么任何上载请求都会失败,如果为false并且服务器目录读写权限允许那么上载是允许的。默认值为true。
list 该选项设定当客户请求可以使用的模块列表时,该模块是否应该被列出。如果设置该选项为false,可以创建隐藏的模块。默认值是true。
uid 该选项指定当该模块传输文件时守护进程应该具有的uid,配合gid选项使用可以确定哪些可以访问怎么样的文件权限,默认值是"nobody"。
gid 该选项指定当该模块传输文件时守护进程应该具有的gid。默认值为"nobody"。就是说当同步到这个模块的时候,rsync守护进行要使用什么样的UID和GID权限来读取本地文件。(疑问:不懂是否是这样理解)
exlude 用来指定多个由空格隔开的多个模式列表,并将其添加到exclude列表中。这等同于在客户端命令中使用--exclude来指定模式,不过配置文件中指定的exlude模式不会传递给客户端,而仅仅应用于服务器。一个模块只能指定一个exlude选项,但是可以在模式前面使用"-"和"+"来指定是 exclude还是include。但是需要注意的一点是该选项有一定的安全性问题,客户很有可能绕过exlude列表,如果希望确保特定的文件不能被访问,那就最好结合uid/gid选项一起使用。
include 用来指定多个由空格隔开的多个rsync并应该exlude的模式列表。这等同于在客户端命令中使用--include来指定模式,结合 include和 exlude可以定义复杂的exlude/include规则。一个模块只能指定一个include选项,但是可以在模式前面使用"-"和"+"来指定是 exclude还是include。
auth users 该选项指定由空格或逗号分隔的用户名列表,只有这些用户才允许连接该模块。这里的用户和系统用户没有任何关系。如果"auth users"被设置,那么客户端发出对该模块的连接请求以后会被rsync请求challenged进行验证身份这里使用的 challenge/response认证协议。用户的名和密码以明文方式存放在"secrets file"选项指定的文件中。默认情况下无需密码就可以连接模块(也就是匿名方式)。
secrets file 该选项指定一个包含定义用户名:密码对的文件。只有在"auth users"被定义时,该文件才有作用。文件每行包含一个username:passwd对。一般来说密码最好不要超过8个字符。没有默认的 secures file名,需要限式指定一个。(例如:/etc/rsyncd.secrets)
hosts allow 该选项指定哪些IP的客户允许连接该模块。客户模式定义可以是以下式: 192.168.1.1 指定只有某个IP地址 192.168.1.0/24 指定某个网络的客户端Backup.linux.com 指定某个主机名才能访问默认是允许所有主机连接。
hosts deny 指定不允许连接rsync服务器的机器,可以使用hosts allow的定义方式来进行定义。默认是没有hosts deny定义
ignore errors 指定rsyncd在判断是否运行传输时的删除操作时忽略server上的IP错误,一般来说rsync在出现IO错误时将将跳过--delete操作,以防止因为暂时的资源不足或其它IO错误导致的严重问题。
ignore nonreadable 指定rysnc服务器完全忽略那些用户没有访问权限的文件。这对于在需要备份的目录中有些文件是不应该被备份者得到的情况是有意义的。
transfer logging 使rsync服务器使用ftp格式的文件来记录下载和上载操作在自己单独的日志中。
log format 通过该选项用户在使用transfer logging可以自己定制日志文件的字段。其格式是一个包含格式定义符的字符串,可以使用的格式定义符如下所示:默认log格式为:"%o %h [%a] %m (%u) %f %l",一般来说,在每行的头上会添加"%t [%p] "。在源代码中同时发布有一个叫rsyncstats的perl脚本程序来统计这种格式的日志文件。%h 远程主机名%a 远程IP地址%m 模块名%t 当前时间%f 文件名%l 文件长度字符数
timeout 通过该选项可以覆盖客户指定的IP超时时间。通过该选项可以确保rsync服务器不会永远等待一个崩溃的客户。超时单位为秒钟,0表示没有超时定义,这也是默认值。对于匿名rsync服务器来说,一个理想的数字是600。
dont compress 用来指定那些不进行压缩处理再传输的文件,默认值是 *.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz 因为这些文件已经经过压缩咯,默认不用修改。

rsyncd.conf方式参数说明

#指定传输文件时守护进程具有的用户ID,这里表示默认为nobady
uid=nobady
#指定传输文件时守护进程具有的用户组ID,这里表示默认为nobady
gid=nobody
#禁止切换目录
use chroot=no
#客户端的最大连接数
max connection=10
#检查口令文件的权限,口令文件的权限用户属组必须是root,权限必须是600
strict modes=yes
#pid文件的位置
pid file=/var/run/rsyncd.pid
#lock文件的位置
lock file=/var/run/rsyncd.lock
#日志文件的位置
log file=/var/log/rsyncd.log
 
#定义模块名,客户端命令跟在IP后面的,作为DST的一部分
[mysqlbackup]
#指定这个模块需要同步的路径
path=/data/mysqldb_backup
#这个是注释 可以自己定义
comment=MySQL remote backup
#忽略一些无关的IO错误
ignore errors
#no代表客户端可以上传文件,yes表示只读取
read only=no
#no表示客户端可以下载文件,yes表示不能下载
write only=no
#表示允许连接的主机地址
hosts allow=192.168.1.101
#表示不允许连接的主机地址
hosts deny=*
#不允许该模块被客户端列出
list=false
#指定传输文件时守护进程具有的用户ID,
uid=root
#指定传输文件时守护进程具有的用户组ID,
gid=root
#用来指定连接该模块的用户名,用户名可以自定义,这个是客户端命令跟在IP前面那个
auth users=rsync_backup
#指定密码文件,文件里面记录的是用户名:密码
secrets file=/etc/rsyncd.passwd
EOF
 
echo "用户名:密码" >/etc/rsyncd.passwd
#权限必须600
chmod 600 /etc/rsyncd.passwd
#开机启动
echo "rsync --daemon --config=/etc/rsyncd.conf" >>/etc/rc.local
#立即运行服务端,其实这儿的配置文件位置就是默认的,可以不加--config
rsync --daemon --config=/etc/rsyncd.conf

三、场景使用:MySQL备份数据文件同步至远程备份服务器

场景:
CentOS7
MySQL服务器(192.168.1.100)每天凌晨通过mysqldump全量备份到本机(*.zip格式),保留最近30天的备份文件。每个zip格式的备份文件大小超过1G。
需要在本地备份结束后,同步备份文件至远程另一台备份服务器((192.168.1.101)。

服务端安装(备份服务器)

1.安装rsync

yum -y install rsync

2.修改配置文件/etc/rsyncd.conf

uid = root
gid = root
use chroot = no
max connections = 10 
timeout = 900
ignore nonreadable = yes
log file=/var/log/rsyncd.log
pid file=/var/run/rsyncd.pid
lock file=/var/run/rsyncd.lock
dont compress=*.gz *.tgz *.zip *.z *.Z *.rpm *.deb *.bz2
[mysqlbackup]
comment=MySQL remote backup
path=/data/mysqldb_backup
ignore errors=yes
hosts allow=192.168.1.101
auth users=rsync_backup
secrets file=/etc/rsyncd.passwd
list=false
read only=no

3.创建密码文件

#创建密码文件,rsync认证的用户和密码, 不需要使用useradd真实创建用户:

echo "rsync_backup:123456" >/etc/rsyncd.passwd
chmod 600 /etc/rsyncd.passwd

4.通过systemctl启动rsyncd服务

systemctl start rsyncd #启动rsync
systemctl status rsyncd #查看状态
systemctl enable rsyncd #设置为开机启动
systemctl restart rsyncd #重启

查看rsync的log
cat /var/log/rsyncd.log

5.防火墙配置

#rsync默认端口是873

firewall配置方式:
firewall-cmd --permanent --add-port=873/tcp    
firewall-cmd --reload

iptables配置方式:
iptables -I INPUT -p tcp --dport 873 -j ACCEPT
service iptables save
systemctl restart iptables.service

rsync同步文件时可能会报错:
rsync: failed to connect to xxx: Permission denied (13)
出现这个错误是因为selinux, linux安全增强的补丁导致的。
解决方法是禁用selinux的安全增强, 可以使用命令 setenforce 0来临时禁用,或者通过修改配置文件/etc/sysconfig/selinux 添加 SELINUX=disabled来永久禁用

客户端安装(MySQL服务器)

1.安装rsync

yum -y install rsync

2.配置密码文件

#这里需要把服务端配置的rsync_backup密码写到客户端的密码文件中(仅密码)

echo "123456" >/etc/rsync_client.passwd
chmod 600 /etc/rsync_client.passwd

3.客户端执行同步

/data/backup/mysqlbackup_to_remote.sh

nohup rsync -a --delete --include="*.zip" --exclude=* --password-file=/etc/rsync_client.passwd /data/backup/mysql/ rsync_backup@192.168.1.100::mysqlbackup >/dev/null 2>&1 &

备注:

这里仅用-a参数而未增加压缩参数-z的原因是,是因为这里仅同步指定的mysql备份文件(*.zip),本身就是压缩文件格式,且单个文件比较大,考虑到性能,故无需再压缩传输。

这里使用--delete参数是因为源数据mysql备份文件是仅保留最近30天数据(即源数据会自动删除30天之前的备份文件)。故同步时,考虑到存储压力,让服务器端保持同步,也同步删除30天之前的数据文件。

4.配置crontab定时执行

crontab -e

#mysql local backup
0 0 * * * root sh /data/backup/mysql/mysql_backup.sh

#mysql remote backup
0 4 * * * root sh /data/backup/mysqlbackup_to_remote.sh

附: mysql本地备份脚本(mysql_backup.sh)

#!bin/bash
cd /data/backup/mysql

#所有需要备份的库
zipName="mysqldb_"

#遍历备份所有的库
mysqldump -h 127.0.0.1 -uroot -p123456 --all-databases > "./"mysqldb"_"$(date +%y%m%d%H)".sql"

#对备份好的sql打成zip包
zip $zipName$(date +%y%m%d%H)".zip"  ./*.sql

#删除所有sql临时文件
rm -rf ./*.sql

#只保留近30天的备份数据
oldDate=`date --date='30 day ago' +%y%m%d`
rm -rf ./$zipName${oldDate}*

四、rsync+inotify实时数据同步

1.inotify-tools安装

yum -y install inotify-tools

如果没有epol源就安装,然后再执行
wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo

2.notify脚本

/data/backup/inotify_rsync.sh

#!/bin/bash
srcdir=/data/backup/mysql/
inotifywait -mrq --timefmt "%d%m%y%H:%M" --format "%T%w%f%e" -e modify,delete,create,attrib ${srcdir} 
| while read file
do
  #echo "${file} is notified!"
  nohup rsync -a --delete --include="*.zip" --exclude=* --password-file=/etc/rsync_client.passwd /data/backup/mysql/ rsync_backup@192.168.1.100::mysqlbackup >/dev/null 2>&1 &
done

-m 持续监听
-r 使用递归形式监视目录
-q 减少冗余信息,只打印出需要的信息
-e 指定要监视的事件,多个时间使用逗号隔开
–timefmt 时间格式
–format 监听到的文件变化的信息
ymd分别表示年月日,H表示小时,M表示分钟
–format 说明:
%w 表示发生事件的目录
%f 表示发生事件的文件
%e 表示发生的事件
%Xe 事件以“X”分隔
%T 使用由–timefmt定义的时间格式

五、相关参考资料

原文地址:https://www.cnblogs.com/huligong1234/p/13513395.html