环境:VMware-Workstation-12-Pro,Windows-10,CentOS-7.5,Xshell5
[TOC]
NFS介绍
什么是NFS(Network File System)
简单来说NFS就是实现文件共享功能的,与windows文件共享功能类似,但本篇博客只讨论多台Linux
服务器之间通过NFS共享文件,不涉及windows。NFS一般的用法是,选一台Linux服务器作为NFS服务
端,把该机器上某个目录共享出去,一般是共享到局域网内,然后局域网内的其它机器都连接上这台
服务器,操作NFS服务端共享出来的指定目录。
NFS也是一套标准的文件系统协议,是1984年Sun Microsystems开发的,现在是RFC维护,最新
版本是NFSv4.2。
我们知道把数据共享给其它机器,那是需要联网传输数据的,而NFS本身不具备通过网络传输数据的
功能,而是依赖RPC服务(Remote Procedure Call)实现的,RPC最早也是Sun Microsystems
开发的,现在也是RFC标准,RPC传输数据是基于TCP/UDP协议的,使用的是固定端口111。
搭建NFS服务需要的软件包
在CentOS7里,如果我们使用yum安装nfs-utils,不要着急确认,可以看到提示的依赖信息如下
图所示:
看到了吧,其中我们需要关心的的依赖软件包就是rpcbind。也就是说搭建NFS服务需要安装两个软件
包nfs-utils和rpcbind,分别对应了NFS母体和其所需要的数据传输服务RPC,不过使用yum安装系统
会自动处理依赖,即只需安装nfs-utils包即可。
如果我们使用yum卸载nfs-utils包,其安装时所对应的依赖,都是不会被卸载的。
安装完毕之后我们可以使用rpm -ql nfs-utils
,看一下该包里都有哪些命令和文件:
[root@nfs-server ~]# rpm -ql nfs-utils
/usr/lib/systemd/system/nfs-server.service
/usr/sbin/exportfs
/usr/sbin/showmount
/var/lib/nfs/etab
上面的输出省略了一些内容。
极简步骤搭建NFS服务
俗话说的好,光说不练假把式,本节我们就用最少的步骤,完全不考虑原理,专注实现功能,让读者
快速看到效果。
准备两台机器
首先启动两台Linux虚拟机,关闭防火墙和SELinux,使用Xshell连接好。最关键的,IP地址和
主机名按照下面的表格配置:
机器
主机名
公网IP
内网IP
1号虚拟机
nfs-server
10.0.0.31
10.0.0.7
2号虚拟机
web-client
172.16.1.31
172.16.1.7
这里我简单解释下,1号虚拟机是NFS服务器,就是共享目录给别人用的机器,2号虚拟机是客户端,
就是使用1号机器共享出来的目录的机器。这里我给每个机器都配置了两块网卡,这不是必须的,
事实上一块网卡也能实现效果。另外具有一定计算机网络背景知识的读者可以看到,我写的虽然是
公网IP,其实也是用于局域网的IP来模拟的,如果在生产环境中,会根据实际需要,替换成真正的
公网IP地址。
在下面的叙述中,有时我会显示说明实在哪台机器上进行操作,有时读者则需要通过主机名来判断,
当前需要操作哪台机器。
推荐使用克隆虚拟机来搭建测试环境,关于虚拟机克隆以及克隆后添加网卡和修改网络参数,可以
参考这里:
配置服务端(nfs-server)
1 安装nfs-utils
[root@nfs-server ~]# yum install nfs-utils -y
2 重启rpcbind服务
[root@nfs-server ~]# systemctl restart rpcbind
3 配置/etc/exports
[root@nfs-server ~]# vim /etc/exports
[root@nfs-server ~]# cat /etc/exports
/data *(rw)
4 创建要共享的目录并更改所有者为nfsnobody
[root@nfs-server ~]# mkdir /data
[root@nfs-server ~]# chown -R nfsnobody:nfsnobody /data
5 重启nfs-server服务
[root@nfs-server ~]# systemctl restart nfs-server
配置客户端(web-client)
1 安装nfs-utils
[root@web-client ~]# yum install nfs-utils -y
2 重启rpcbind服务
[root@web-client ~]# systemctl restart rpcbind
3 挂载远端(172.16.1.31)上的目录
[root@web-client ~]# mkdir /data
[root@web-client ~]# mount -t nfs 172.16.1.31:/data /data
4 测试
[root@nfs-server ~]# ls /data
f1.txt f2.txt f3.txt
[root@web-client ~]# ls /data
f1.txt f2.txt f3.txt
[root@web-client ~]# echo "123456" > /data/f4.txt
[root@web-client ~]# ls /data
f1.txt f2.txt f3.txt f4.txt
[root@web-client ~]# cat f4.txt
123
[root@nfs-server ~]# ls /data
f1.txt f2.txt f3.txt f4.txt
[root@nfs-server ~]# cat /data/f4.txt
123456
从上述测试效果中可以看出,客户挂载完毕远端共享出来的目录/data后,在客户端操作/data目录
几乎和操作本地目录一致,也就是对客户端来说远端nfs服务器是透明的,无需关心,正常操作/data
目录即可。无论是在客户端还是在本地端操作/data目录,操作的都是同一块磁盘,读写的都是相同
的物理空间。
NFS服务简单执行流程
比如现在NFS服务器共享了一个/data目录给客户端,客户端要往/data目录里写入数据,大致会经过
如下几个步骤:
- 客户端把请求传递给自己的rpcbind服务
- rpcbind通过TCP/UDP协议把请求传递给服务端的rpcbind服务
- 服务端的rpcbind请求传递给服务端的nfs服务
- 服务端的nfs服务,操作本地件,写入数据
可以看到rpcbind服务在NFS共享服务之间起到了中介的作用,事实上nfs服务具有非常多的端口,
而且是随机的,直接通过这些端口与客户端操作非常不便,而rpcbind使用的是固定端口111,
这样客户端与服务端只需要监听111端口,即可完成通信。正是rpcbind在其中承担了客户端和
服务端之间端口映射与转换的工作。
基本了解nfs共享服务的操作流程之后,下面我们就开始详细说说上面每一步的操作步骤都是何含义,
并且对关键部分进行展开细说,下面很多内容都会提到上面的极简案例。首先一个比较关键的,就是
要弄明白,共享文件的访问权限。
NFS文件访问权限
要共享一个目录给别人用,我们直觉能想到的就是,需要共享什么目录,共享给谁,这两个基本问题。
另外还有一个非常重要的部分:共享文件的权限。我们上面的极简示例中,服务端共享/data目录,
客户端挂载/data目录,之后就可正常读写,且慢,大家有没有注意到我在客户端是使用root用户
读写/data目录里的内容的,假如在客户端换成普通用户呢?这里我明确的告诉大家,对于上述极简
示例中,客户端使用普通用户,只能读,不能写,示例如下:
[oldboy@web-client ~]$ cat /data/f4.txt
123456
[root@web-client ~]# su - oldboy
[oldboy@web-client ~]$ echo "hello" >> /data/f4.txt
-bash: /data/f4.txt: Permission denied
到这里,相信读者已经开始思考了,我们先抛开NFS,就我们已有的Linux文件系统权限的知识先分析
一下,在上面的极简案例中我们设置了这样一条:
[root@nfs-server ~]# chown -R nfsnobody:nfsnobody /data
这是什么鬼呢,首先chown是更改目录所有者和所属组,-R参数是递归修改子目录和子文件,这是我们
已有的知识,那nfsnobody是什么鬼,让我告诉你,这个用户是我们安装nfs-utils自动为我们创建
的,这个软件包还为我们创建了一些其它用户,大家可以检查/etc/passwd看到,不过我们只关
心nfsnobody这个用户,可以id命令先看下这个用户的信息:
[root@nfs-server ~]# id nfsnobody
uid=65534(nfsnobody) gid=65534(nfsnobody) groups=65534(nfsnobody)
这里的名字和ID都是固定的,只要安装了nfs-utils包,该用户就会自动被创建。现在我们在服务端
检查一下/data目录的属性:
[root@nfs-server ~]# ls -ld /data
drwxr-xr-x 2 nfsnobody nfsnobody 62 Sep 8 21:12 /data
可以看到,在服务端本地,/data目录的所有者和所述组都是nfsnobody,那我们在服务端建立一个
文件,观察下,这个文件的权限又是什么:
[root@nfs-server ~]# touch /data/f5.txt
[root@nfs-server ~]# ls -l /data/f5.txt
-rw-r--r-- 1 root root 0 Sep 8 21:50 /data/f5.txt
可以看到符合我们的预期f5.txt的所有者和所属组都是root用户。那么我们换到客户端,看一下在
服务端/data目录是什么权限:
[root@web-client ~]# umount /data
[root@web-client ~]# ls -ld /data
drwxr-xr-x 2 root root 6 Sep 6 12:24 /data
[root@web-client ~]# mount -t nfs 172.16.1.31:/data /data
[root@web-client ~]# ls -ld /data
drwxr-xr-x 2 nfsnobody nfsnobody 90 Sep 8 21:50 /data
可以看到,未挂载远端共享的/data目录,查看本地的/data目录其所有者和所属组都是root,而
挂载到远端/data目录后,所有者和所属组和远端/data目录保持一致,是nfsnobody,符合预期。
现在我们在客户端/data目录里创建一个文件,看一下其权限:
[root@web-client ~]# touch /data/f6.txt
[root@web-client ~]# ls -l /data/f6.txt
-rw-r--r-- 1 nfsnobody nfsnobody 0 Sep 8 21:50 /data/f6.txt
问题出现了,我们明明用的是root用户,但是创建出来的文件其所有者和所述组确是nfsnobody,这是
怎么一回事,不用说这肯定是服务端nfs的配置文件在起作用。
稳住,我知道大家看到这里,仍是云里雾里,似乎不知所云,相信我,本节看完,一切豁然开朗。
我们再看一下在极简案例中,我们服务端的配置文件/etc/exports
[root@nfs-server ~]# cat /etc/exports
/data *(rw)
这里的/data就是服务器要共享的目录名,*(星号)表示共享给所有机器使用,rw表示客户端对该目录
具有读写权限。实际上小括号里可以配置多个参数,我们这里虽然只写了一个参数,但是nfs服务默认
就有一些预设参数,即默认值,这些默认参数到底有哪些呢,可以在/var/lib/nfs/etab中看到:
上面的内容都是在一行的,黄色部分是我们直接配的,小括号里其它参数则都是nfs的默认值,我们
重点关注的是里面的红色默认参数:
anonuid=65534,anongid=65534
正好是我们的nfsnobody用户的uid和gid
root_squash
直接翻译过来是压缩root用户,这是针对客户端来说的,含义是当客户端使用root用户
操作/data目录里的文件时,将其"压缩"(转换成)anonuid和anongid对应的用户,在
极简案例中就是nfsnobody用户
no_all_squash
也是针对客户端来说的,含义就是,客户端不管使用什么用户身份操作,都保持其原始身
份不变,即原始是什么用户身份映射到服务端还是什么身份。在我们的极简案例中,root
用户除外,其映射到服务端会被转换成nfsnobody身份,英文我们默认参数单独定义了
root_squash来规定root的映射规则。
该参数不常用,正如我们上文举例那样,客户端假如使用的不是root用户,比如客户端
使用的是oldboy普通用户,/data目录的所有者是nfsnobody,那么oldboy对于/data
目录来说,就是其它人,即只具有读的权限,可见这种默认的配置,服务端已经配置了
/data目录开放读写权限给客户端,但是客户端必须使用root用户操作/data,这样才
能映射为服务端的nfsnobody身份,也就是对/data目录具有了读写权限。
读者可能会想,我在客户端就是使用nfsnobody用户登录,来操作/data目录,这样不就也
可以执行写操作了吗,不行的。nfsnobody是虚拟用户,不允许登录。
那么问题来了,如果我们把/etc/exports配置成/data *(rw)
,其含义就是其它客户端机器必须
使用root用户操作/data,才能正常读写远程/data目录。我们的需求是,服务端不管使用什么用户
操作远程/data目录,我们都给映射到服务器这边的nfsnobody用户,这样/data目录里清一色,
所有者和所属组都是nfsnobody,看起来清爽的同时也便于我们维护,那么如何修改/etc/exports
才能实现我们的需求呢,请看下一节。
NFS服务端配置
NFS服务器端的配置参数在/etc/exports
。该配置文件说明了这台NFS服务器有哪些目录共享
给客户端使用,客户端对这些目录具有什么操作权限等。可以使用man exports
查看参数配置
帮助。
简单示例如下:
[root@nfs-server ~]# cat /etc/exports
/data 172.16.1.7(rw)
上面的配置参数表示,服务器把/data目录共享给172.16.1.7这台机器使用,客户端具有的权限是
读和写(rw)
通过示例可以看到,NFS服务器端/etc/exports配置参数,基本格式如下:
NFS共享目录 客户端地址1(参数1,参数2, ...) 客户端地址2(参数1,参数2, ...)
客户端地址的表示方法
星号(*),表示所有服务器。
172.16.1.7,表示这一台机器。 172.16.1.0/24,该配置最常用,表示同一个局域网内的所有机器(★★★) web-client,这是直接使用主机名标识客户端地址,需要做DNS解析,原理还是找到主机名对应的
IP地址。
/etc/exports参数
NFS共享参数
作用
rw
读写权限
ro
只读权限(read-only)
root_squash
默认值,客户端root用户映射为服务端指定用户
no_root_squash
客户端root用户映射为服务端root用户
all_squash
客户端所有用户映射为服务端指定用户
no_all_squash
默认值,客户端什么用户映射到服务端还是什么用户
sync
默认值,写入数据时,硬盘与内存保持同步
async
写入数据时,先写入内存,再写入硬盘
anonuid
指定映射用户的uid,默认值65534(nfsnobody)
anongid
指定映射用户的gid,默认值65534(nfsnobody)
指定用户即anonuid和anongid所指定的用户。 sync和async一般认为异步性能高,同步数据安全,然实际情况区别不大,使用默认值即可。
no_root_squash,该参数对于服务端比较危险,随便在局域网内找一台机器使用root登录,
操作远程/data目录,映射到NFS服务端这边也是root用户,权限过大。 上面表格列出了10个参数,其中前4对,都是2选一,最后2个参数一般同时指定。
现在我们解决,在NFS文件访问权限那一节里留下的问题,我们可以使用下述配置:
[root@nfs-server ~]# cat /etc/exports
/data 172.16.1.0/24(rw,sync,all_squash)
上面这个配置是比较常用的配置,同一局域网内的所有客户端,操作远程/data目录时,都会被映射
为服务端上的nfsnobody用户。
/etc/exports里面的配置是可以用#,注释掉的
/etc/exports生效
修改完毕/etc/exports之后,有两种方式可以使其生效:
1 重启nfs-server服务
[root@nfs-server nfs]# systemctl restart nfs-server
2 重载/etc/exports配置文件
[root@nfs-server nfs]# exportfs -arv
exporting 172.16.1.0/24:/data
a(all 所有),r(reexport 重新),v(verbose 详细信息),这三个参数一般连用表示从新加载
/etc/exports文件里所有的配置,应用新的配置。
还可以使用exportfs -auv
,u(unexport 取消),表示取消该台NFS服务器上共享的所有目录。
更多exportfs用法,请参考man exportfs
重启服务比较强硬,而重载服务则比较温柔,平滑一些。什么,重启服务器是配置参数生效?别闹。
3 服务端查看配置参数是否修改成功(/var/lib/nfs/etab)
比如我们把服务端的/etc/exports配置成如下内容:
[root@nfs-server ~]# cat /etc/exports
/data 172.16.1.0/24(rw,sync,all_squash)
[root@nfs-server nfs]# systemctl restart nfs-server
此时可以检查下/var/lib/nfs/etab,该文件里的内容,是否已经成功变更为我们所修改的内容,
当然此文件里还包含默认共享参数:
可以看到我们的修改已经生效。
4 客户端的操作(非必须)
如果客户端已经事先知道服务端修改了共享参数,可以重新挂载远程目录:
[root@web-client ~]# umount /data
[root@web-client ~]# mount -t nfs 172.16.1.31:/data /data
上述操作不是必须的,服务端只要修改了共享参数,则客户端立即生效,比如刚开始共享为读写权限,
随后修改为只读权限,那么客户端即刻就无法在向共享目录里写入数据,这很好理解客户端的操作最终
都是通过rpcbind远程映射到服务端,然后服务端再进一步通过NFS相关服务向磁盘中写入数据的。
只不过在有些情况下,服务端修改了配置参数而客户端没有重新挂载,这时第1次操作远端目录时,会
等待一会。
创建共享目录
服务端除了启动rpcbind和nfs-server服务,配置/etc/exports之外,另外一个重要的步骤就是
要创建共享目录了,这是必须的,要共享这个目录而这个目录没有,那肯定要报错的。在我们的极
简单案例中,我们执行了如下两条命令:
[root@nfs-server ~]# mkdir /data
[root@nfs-server ~]# chown -R nfsnobody:nfsnobody /data
这个时候我们就知道,为什么需要把/data目录的所有者和所属组改成nfsnobody,该用户是安装
nfs-utils包自动为我们创建的,它的uid和gid都是65534,而/etc/exports里anonuid和
anongid默认的值正是65534,所对应的就是nfsnobody用户,说白了就是NFS为了我们方便,当我
们并不关心/data的所有者是谁时,给它指定成nfsnobody即可。
现在情况有变,我们不想让/data目录的所有者和所属组都是nfsnobody,想指定一个id和名字都
固定的其它用户,比如uid和gid都是666的www用户,这时我们对客户端和服务端都需要进行操作:
1 服务端操作
[root@nfs-server ~]# groupadd -g 666 www
[root@nfs-server ~]# useradd -u 666 -g 666 -s /sbin/nologin -M www
[root@nfs-server ~]# id www
uid=666(www) gid=666(www) groups=666(www)
[root@nfs-server ~]# vim /etc/exports
[root@nfs-server ~]# cat /etc/exports
/data 172.16.1.0/24(rw,sync,all_squash,anonuid=666,anongid=666)
[root@nfs-server ~]# exportfs -arv
exporting 172.16.1.0/24:/data
[root@nfs-server ~]# chown -R www:www /data
[root@nfs-server ~]# ls -ld /data
drwxr-xr-x 2 www www 90 Sep 8 23:11 /data
把www用户建立成虚拟用户不是必须的,只不过为了安全考虑,默认的nfsnobody就是虚拟用户,其
无法登录系统。uid和gid都设置成固定值是为了便于管理。
2 客户端操作
[root@web-client ~]# groupadd -g 666 www
[root@web-client ~]# useradd -u 666 -g 666 -s /sbin/nologin -M www
[root@nfs-web-client ~]# id www
uid=666(www) gid=666(www) groups=666(www)
客户端建立同ID,同名称的www用户不是必须的,但是如果我们不建立,那么在客户端查看/data目录
下的属性时,会像下面这样:
[root@web-client ~]# ls -l /data
total 12
-rw-r--r-- 1 666 666 14 Sep 8 12:27 f1.txt
-rw-r--r-- 1 666 666 7 Sep 8 09:27 f2.txt
-rw-r--r-- 1 666 666 0 Sep 7 19:28 f3.txt
那么多666?一点都不顺,而是老别扭了,因此我们需要在客户端这边也建立同ID,同名称的用户,
需要注意,Linux系统是根据ID识别用户的,客户端这边最好不要有其它用户id是666,那就太乱
了。创建好同样的ID后,我再看一下属性:
[root@web-client ~]# ls -l /data
total 12
-rw-r--r-- 1 www www 14 Sep 8 12:27 f1.txt
-rw-r--r-- 1 www www 7 Sep 8 09:27 f2.txt
-rw-r--r-- 1 www www 0 Sep 7 19:28 f3.txt
这样才符合我们的认知直觉。
NFS服务端配置小结
现在,我们再看一下极简案例中配置NFS服务端配置的步骤(稍作修改):
[root@nfs-server ~]# yum install nfs-utils -y
[root@nfs-server ~]# systemctl restart rpcbind
[root@nfs-server ~]# cat /etc/exports
/data 172.16.1.0/24(rw,sync,all_squash)
[root@nfs-server ~]# mkdir /data
[root@nfs-server ~]# chown -R nfsnobody:nfsnobody /data
[root@nfs-server ~]# systemctl restart nfs-server
如果对上面的每一行都清楚其作用的话,那么恭喜你也感谢你,说明NFS服务端配置套路已基本掌握,
也说明我上面的内容写的不错哈。不过现在记得,未必以后也记得,我在这里也总结下:
- rpcbind服务必须先启动,然后再启动nfs-server服务,否则NFS服务搭建失败。
- 修改服务端配置文件后,需要重启服务或重载配置文件使其生效。
- 要共享的目录其所有者和所属组必须是anonuid和anongid指定的用户。
NFS客户端配置
服务端已经配置完毕,现在就等着客户端使用了。NFS服务是rpcbind服务进行远程通信的,显然
客户端这边同样需要rpcbind服务,不过为了方便我们在客户端也直接装上nfs-utils包,不过只
需启动rpcbind服务即可。
[root@web-client ~]# yum install nfs-utils -y
[root@nfs-server ~]# systemctl restart rpcbind
另外如果服务端共享目录的所有者和所属组不是nfsnobody,在客户端最好也创建一个同ID,用名称
的用户,创建方法见上文。
查看远端共享目录
rpcbind服务已经启动完毕,现在可以挂载远端目录了,但是且慢,如果我们不知道远端共享的是什么
目录呢,这时我们需要看一下远端共享了哪些目录,此时我们可以使用下面的命令:
[root@web-client ~]# showmount -e 172.16.1.31
Export list for 172.16.1.31:
/data 172.16.1.0/24
完整语法如下:
showmount [ --exports ] [ --help ] [ --version ] [ host ] -e or --exports,显示远程服务器,可供使用的挂载列表
showmount在客户端和服务端都可以使用,只要安装了nfs-utils包,就有该命令,基本上用法就上面
一种,可以看到172.16.1.31这台NFS服务器共享了其/data目录给大家使用。大家可能会感觉上面给
出的共享参数信息,有点少啊,比如不知道是否对该目录有写入权限,如果看到的信息类似与在服务
端cat /etc/exports
那样,似乎更好一些,但是本人多方查找资料,基本确定没有办法在客户端
看到共享参数权限的详细信息,要想知道自己是否对某个共享目录具有写入权限,只能靠人工测试,
如果没有写入权限,会得到如下所示的提示:
[root@web-client ~]# echo '123' > /data/f1.txt
bash: /data/f1.txt: Read-only file system
showmount命令不加参数直接回车,无任何效果并且Xshell会卡住,CTRL+C无效,只能等待。
挂载共享目录
知道是哪个远端机器,共享的什么目录,就可以开始挂载了:
[root@web-client ~]# mkdir /data
[root@web-client ~]# mount -t nfs 172.16.1.31:/data /data
注意远端的/data目录不一定必须挂载在本地的/data目录,其它目录也一样可以。挂载完毕之后, 可以使用df -h命令查看一下:
这样挂载之后,我们就可以正常在本地/data目录里使用远程/data目录里的文件了,本地和远端
操作的都是同一块硬盘,对客户端来说远程NFS服务器仿佛是透明的。
卸载共享目录
回顾一下正常挂载的语法:mount -t type device dir
正常卸载的语法:umount dir
完整卸载的语法:
umount [-dflnrv] {dir|device}... ,其中-lf参数比较常用,下面分别进行介绍。
-l, --lazy 直接翻译是懒惰卸载,可以理解为平滑卸载。请看下面的示例:
[root@web-client data]# pwd
/data
[root@web-client data]# umount /data
umount.nfs4: /data: device is busy
我们知道这种情况无法卸载的原因显而易见,就是我们当前处于/data目录,从而提示设备 被占用,导致我们无法卸载。但有时提示设备被占用,我们也不知道到底是哪个程序占用了 /data目录里的文件,或者我们不关注/data是不是还有文件正在使用,就是要直接卸载 /data,此时就可以添加-l,如下:
[root@web-client data]# umount -l /data
此时我们使用df -h,已经看不到/data目录的挂载信息了,但是/data目录并没有被立即 卸载,系统会自动判断当我们已经没有程序占用/data目录时,自动卸载/data目录
-f, --force 强制卸载,通常用在NFS系统上,经常配合-l参数使用。比如当前系统/data目录是NFS 远程共享的,若果远程的NFS服务器,突然故障,此时我们无法访问/data目录,并且正常 卸载/data目录也无法卸载,则可以使用:
[root@web-client /]# umount -lf /data
强制进行卸载。
参考资料
http://cn.linux.vbird.org/linux_server/0330nfs.php#exec http://docs.etiantian.org/15323330999008.html#toc_5 https://en.wikipedia.org/wiki/Network_File_System https://en.wikipedia.org/wiki/Open_Network_Computing_Remote_Procedure_Call 跟老男孩学Linux运维-Web集群实战-老男孩著
作者:阿胜4K 出处:https://www.cnblogs.com/asheng2016/p/9613065.html