一 Docker的基本信息
前面已经安装了Docker,现在看一下已安装Docker的安装环境以及其他信息
1.1 系统环境
[[email protected] ~]# uname -r 3.10.0-957.27.2.el7.x86_64 [[email protected]-server3 ~]# cat /etc/redhat-release CentOS Linux release 7.7.1908 (Core)
1.2 Docker版本
[[email protected] ~]# docker version
Client: Docker Engine - Community Version: 19.03.4 API version: 1.40 Go version: go1.12.10 Git commit: 9013bf583a Built: Fri Oct 18 15:52:22 2019 OS/Arch: linux/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 19.03.4 API version: 1.40 (minimum version 1.12) Go version: go1.12.10 Git commit: 9013bf583a Built: Fri Oct 18 15:50:54 2019 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.2.10 GitCommit: b34a5c8af56e510852c35414db4c1f4fa6172339 runc: Version: 1.0.0-rc8+dev GitCommit: 3e425f80a8c931f88e6d94a8c831b9d5aa481657 docker-init: Version: 0.18.0 GitCommit: fec3683
[[email protected] ~]# docker info
Client: Debug Mode: false Server: Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 Server Version: 19.03.4 Storage Driver: overlay2 Backing Filesystem: xfs Supports d_type: true Native Overlay Diff: true Logging Driver: json-file Cgroup Driver: cgroupfs Plugins: Volume: local Network: bridge host ipvlan macvlan null overlay Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog Swarm: inactive Runtimes: runc Default Runtime: runc Init Binary: docker-init containerd version: b34a5c8af56e510852c35414db4c1f4fa6172339 runc version: 3e425f80a8c931f88e6d94a8c831b9d5aa481657 init version: fec3683 Security Options: seccomp Profile: default Kernel Version: 3.10.0-957.27.2.el7.x86_64 Operating System: CentOS Linux 7 (Core) OSType: linux Architecture: x86_64 CPUs: 4 Total Memory: 1.777GiB Name: docker-server3 ID: YB6S:6D3D:477B:5UMR:IEX2:2PBD:D6BI:GDYI:22MD:GWSX:4TBX:2LLS Docker Root Dir: /var/lib/docker Debug Mode: false Registry: https://index.docker.io/v1/ Labels: Experimental: false Insecure Registries: 127.0.0.0/8 Live Restore Enabled: false
[[email protected] ~]# cd /etc/docker/
[[email protected] docker]# ls -l
-rw------- 1 root root 244 Nov 9 03:12 key.json
[[email protected] docker]# vim daemon.json
可以使用着文件设定参数
重启Dokcer
[[email protected] docker]# systemctl restart docker
二 拉去镜像,创建一个容器
2.1拉取镜像容器
[[email protected] ~]# docker pull nginx
Using default tag: latest latest: Pulling from library/nginx 8d691f585fa8: Pull complete 5b07f4e08ad0: Pull complete abc291867bca: Pull complete Digest: sha256:922c815aa4df050d4df476e92daed4231f466acc8ee90e0e774951b0fd7195a4 Status: Downloaded newer image for nginx:latest docker.io/library/nginx:latest
[[email protected] ~]# docker run -d -P nginx
-d表示放在后台运行
-P表示随机映射nginx的默认端口
7986a0c99e853a2fabaa3707943c32266946d0bd6b1bf45e07c5f6c2fc340499
2.2 查看容器
[[email protected] ~]# docker ps -a
镜像是nginx,运行的命令是nginx -g ‘daemon of ...‘,不指定名称,也会生成一个随机名称
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7986a0c99e85 nginx "nginx -g ‘daemon of…" 29 seconds ago Up 28 seconds 0.0.0.0:32769->80/tcp eloquent_pike
2.3 测试结果
访问192.168.132.131:32769
一个容器运行成功
然后可以取名webserver,使用80映射到宿主机的80,创建一个容器
[[email protected] ~]# docker run -d --name webserver -p 80:80 nginx
aabf34300d3f46dbc2201a9b925522906bed1cc7a6952042a1583323c96c1ff3
[[email protected] ~]# docker ps -a
ONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES aabf34300d3f nginx "nginx -g ‘daemon of…" 25 seconds ago Up 24 seconds 0.0.0.0:80->80/tcp webserver 7986a0c99e85 nginx "nginx -g ‘daemon of…" 9 minutes ago Up 9 minutes 0.0.0.0:32769->80/tcp eloquent_pike
访问192.168.132.131:80
将容器放在前台运行,不加-d参数就可以了
[[email protected] ~]# docker run -it --name webserver1 -P nginx
控制太卡住
[[email protected] ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8a2933d55a28 nginx "nginx -g ‘daemon of…" About a minute ago Exited (0) 5 seconds ago webserver1 aabf34300d3f nginx "nginx -g ‘daemon of…" 6 minutes ago Up 6 minutes 0.0.0.0:80->80/tcp webserver 7986a0c99e85 nginx "nginx -g ‘daemon of…" 15 minutes ago Up 15 minutes 0.0.0.0:32769->80/tcp eloquent_pike
2.4 容器的其他测试
# 启动一个ubuntu 16.04的容器,打印完"hello world"即退出
[[email protected] ~]# docker run ubuntu:16.04 /bin/echo " hello world "
Unable to find image ‘ubuntu:16.04‘ locally 16.04: Pulling from library/ubuntu e80174c8b43b: Pull complete d1072db285cc: Pull complete 858453671e67: Pull complete 3d07b1124f98: Pull complete Digest: sha256:bb5b48c7750a6a8775c74bcb601f7e5399135d0a06de004d000e05fd25c1a71c Status: Downloaded newer image for ubuntu:16.04 hello world
在前台运行容器并进入容器与容器交互
[[email protected] ~]# docker run -it ubuntu:16.04 /bin/bash
[email protected]:/# cat /etc/networks # symbolic names for networks, see networks(5) for more information link-local 169.254.0.0 [email protected]:/# exit exit
[[email protected] ~]# docker run -it --name webserver2 -P nginx /bin/bash [email protected]:/# ls bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
如果不指定/bin/bash,就执行的是前面的nginx -g ‘daemon of ...‘,使用/bin/bahs.就睡进入/bin/bash的环境
需要说明的是,容器是为任务而生的。一个容器建议只运行一个进程,而且这个进程需要在容器的前台运行,不能通过daemon的方式运行。如果进程退出,容器也会随之停止
退出后查看
[[email protected] ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES 8082a23dde26 nginx "/bin/bash" 4 minutes ago Exited (127) 3 minutes webserver2
在进入容器
[[email protected] ~]# docker exec -it webserver2 /bin/bash Error response from daemon: Container 8082a23dde2687bb8788a8cb7eac4f8df61256ab4cd9267b787a19636eff7863 is not running [[email protected]-server1 ~]# docker start webserver2 webserver2 [[email protected]-server1 ~]# docker exec -it webserver2 /bin/bash [email protected]:/#
打开另一个终端
[[email protected] ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8082a23dde26 nginx "/bin/bash" 7 minutes ago Up About a minute 0.0.0.0:32772->80/tcp webserver2
去访问192.168.132.131:32772
无法访问,是因为只是打开/bin/bash,并没有执行nginx
启动nginx再次访问
[email protected]:/# nginx [email protected]:/# 192.168.132.1 - - [09/Nov/2019:10:23:36 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36" "-" #还有日志输出
访问结果
可以停止nginx服务
[email protected]:/# /etc/init.d/nginx stop
就访问不到
三 容器的基本介绍
3.1 容器的启动过程说明
- 检查本地是否存在指定的镜像,如果没有就从指定的仓库下载
- 利用镜像启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层
- 从宿主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个IP给容器
- 执行用户指定的程序
- 执行完毕后停止容器
3.2 docker run常用选项说明
-t:配置一个伪终端并绑定到容器的标准输入上 -i:让容器的标准输入保持打开 -d:将容器放入后台运行 -c:指定分配该容器的cpu分片 -m:指定分配给该容器的内存大小,单位为B,K,M,G
查看当前节点上的容器状态
docker ps #查看当前正在运行的容器
选项: -a:查看所有容器,包括停止的 -q:只显示容器ID -l:显示最后一次创建的容器
[[email protected] ~]# docker ps -q aabf34300d3f 7986a0c99e85 [[email protected]-server1 ~]# docker ps -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2b16c02bc838 ubuntu:16.04 "/bin/bash" 3 minutes ago Exited (0) 2 minutes ago musing_ritchie [[email protected]-server1 ~]# docker ps -aq 8082a23dde26 2b16c02bc838 f7d3fa643a6f 7ac7b3caf01d 8a2933d55a28 aabf34300d3f 7986a0c99e85
docker的其他操作指令
[[email protected] ~]# docker rm 7ac7b3caf01d #删除 7ac7b3caf01d [[email protected]-server1 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8082a23dde26 nginx "/bin/bash" 32 minutes ago Up 2 seconds 0.0.0.0:32773->80/tcp webserver2 [[email protected]-server1 ~]# docker stop 808 #停止 808 [[email protected]-server1 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8082a23dde26 nginx "/bin/bash" 31 minutes ago Exited (0) 45 seconds ago webserver2 [[email protected]-server1 ~]# docker start 808 808 [[email protected]-server1 ~]# docker kill 808 #停止,相当与stop 808 [[email protected]-server1 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8082a23dde26 nginx "/bin/bash" 33 minutes ago Exited (137) 7 seconds ago webserver2 [[email protected]-server1 ~]# docker pause 808 #z暂停 808 [[email protected]-server1 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8082a23dde26 nginx "/bin/bash" 35 minutes ago Up 2 minutes (Paused) 0.0.0.0:32775->80/tcp webserver2 [[email protected]-server1 ~]# docker unpause 808 #启动 808 [[email protected]-server1 ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8082a23dde26 nginx "/bin/bash" 37 minutes ago Up 4 minutes 0.0.0.0:32775->80/tcp webserver2
容器生命周期管理
四 容器资源限制
一个docker host上会运行若干容器,每个容器都需要CPU、内存和 IO 资源。对于 KVM,VMware等虚拟化技术,用户可以控制分配多少 CPU、内存资源给每个虚拟机。对于容器,Docker 也提供了类似的机制避免某个容器因占用太多资源而影响其他容器乃至整个 host 的性能。
4.1 内存限制
启动一个ubuntu容器,限制内存为200M, 内存与swap的总和为300M:
docker run -it -m 200M --memory-swap 300M ubuntu:16.04
选项说明: -m:允许分配的内存大小 --memory-swap:允许分配的内存和swap的总大小 --memory-swapiness:控制内存与swap置换的比例
需要说明的是,如果启用了--memory-swap参数,相当于使用了swap,则实际内存限制并不生效,要想限制生效,可以不启动该参数,且将--memory-swappiness置为0
下面是一个压测示例:
docker run –it –m 200M –memory-swapiness 0 progrium/stress –-vm 1 –-vm-bytes 180M
选项: --vm:设置内存工作线程数 --vm-byptes:设置单个内存工作线程使用的内存大小
上面的示例中,--vm-bytes为180M,容器工作正常;如果将其修改为230M,则容器OOM退出
4.2 CPU限制
默认情况下,所有容器可以平等的使用宿主机cpu资源且没有限制。docker可以通过-c或--cpu-shares设置容器使用的cpu的权重。如果不指定,默认为1024。
与内存限额不同,通过 -c 设置的 cpu share 并不是 CPU 资源的绝对数量,而是一个相对的权重值。某个容器最终能分配到的 CPU 资源取决于它的 cpu share 占所有容器 cpu share 总和的比例。换句话说:通过cpu share可以设置容器使用CPU的优先级。
例如,在host中启动了两个容器:
docker run --name container_A -c 1024 ubuntu
docker run --name container_B -c 512 ubuntu
container_A的cpu share 1024,是 container_B 的两倍。当两个容器都需要 CPU 资源时,container_A可以得到的 CPU 是container_B的两倍。
需要特别注意的是,这种按权重分配CPU只会发生在CPU 资源紧张的情况下。如果container_A 处于空闲状态,这时,为了充分利用CPU资源,container_B 也可以分配到全部可用的 CPU。
下面是一个压测示例:
# --cpu用于设置cpu工作线程的数量,有几个核就设置为几 docker run --name "container_A" -c 1024 progrium/stress --cpu 1 docker run --name "container_B" -c 512 progrium/stress --cpu 1
两个容器运行起来之后,可以通过在宿主机上使用top查看cpu的资源消耗可以看到两个容器的cpu消耗。
关于cpu资源的更多限制可以参考这里:https://blog.opskumu.com/docker-cpu-limit.html
4.3 io 限制
Block IO 是另一种可以限制容器使用的资源。Block IO 指的是磁盘的读写,docker 可通过设置权重、限制 bps 和 iops 的方式控制容器读写磁盘的带宽,下面分别讨论。
需要说明的是,目前Block IO限额只对direct IO(不使用文件缓存)有效
下面是限制bps和iops的参数说明:
--device-read-bps,限制读某个设备的 bps。 --device-write-bps,限制写某个设备的 bps。 --device-read-iops,限制读某个设备的 iops。 --device-write-iops,限制写某个设备的 iops。 bps是byte per second,每秒读写的数据量 iops是io per second,每秒io的次数
简单示例:
# 创建一个容器,限制写的bps为30M
- docker run -it --device-write-bps /dev/sda:30MB ubuntu
# 容器中,执行如下操作查看效果,然后可以通过取消限制,来查看对比效果:
- time dd if=/dev/zero of=test.out bs=1M count=800 oflag=direct
五 镜像管理
5.1 查看镜像
[[email protected] ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu 16.04 5f2bf26e3524 8 days ago 123MB nginx latest 540a289bab6c 2 weeks ago 126MB
5.2 镜像命名规范
无论我们对镜像做何种操作,首先它得有个名字。我们在前面使用docker run来运行容器的时候,就需要传递一个镜像名称,容器基于该镜像来运行。
一个完整的镜像名称由两部分组成:
<image name> = <repository>:[tag]
其中repository包含如下内容:
[Docker Registry地址/][项目目录/]<名称>
所以一个完整的镜像命名如下:
[Docker Registry地址/][项目目录/]<名称>:[标签]
当没指明镜像tag时,默认为latest,但latest没有任何特殊含义,在docker hub上很多repository将latest作为最新稳定版本的别名,但这只是一种约定,不是强制规定,一个repository可以有多个tag,而多个tag也可能对应同一个镜像
5.3 镜像的基本操作
拉取镜像
docker pull 镜像名,默认是从dokcer的仓库中拉取
[[email protected] ~]# docker pull centos
Using default tag: latest latest: Pulling from library/centos 729ec3a6ada3: Pull complete Digest: sha256:f94c1d992c193b3dc09e297ffd54d8a4f1dc946c37cbeceb26d35ce1647f88d9 Status: Downloaded newer image for centos:latest docker.io/library/centos:latest
也可以指定位置拉取镜像
[[email protected] ~]# docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.1.3
- 指定仓库位置 registry.cn-hangzhou.aliyuncs.com
- 路径:google_containers
- 镜像名:coredns:1.1.3
1.1.3: Pulling from google_containers/coredns 88286f41530e: Pull complete 9e8fb813cddc: Pull complete 3ac8c130be3b: Pull complete Digest: sha256:d929e48a87979279307111fd28a52272d3fac0ed1dc8f2f53a9489be45e5f2eb Status: Downloaded newer image for registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.1.3 registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.1.3
[[email protected] ~]# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu 16.04 5f2bf26e3524 8 days ago 123MB nginx latest 540a289bab6c 2 weeks ago 126MB registry.cn-hangzhou.aliyuncs.com/google_containers/coredns 1.1.3 b3b94275d97c 17 months ago 45.6MB
5.4 获取镜像的详细信息
[[email protected] ~]# docker inspect centos:latest
[ { "Id": "sha256:0f3e07c0138fbe05abcb7a9cc7d63d9bd4c980c3f61fea5efa32e7c4217ef4da", "RepoTags": [ "centos:latest" ], "RepoDigests": [ "[email protected]:f94c1d992c193b3dc09e297ffd54d8a4f1dc946c37cbeceb26d35ce1647f88d9" ], "Parent": "", "Comment": "", "Created": "2019-10-01T23:19:57.105928163Z", "Container": "711572e3c0c1ac06d5c13c4e668ec170b8ad8786b5f0949f884a5f7fd350d856", "ContainerConfig": { "Hostname": "711572e3c0c1", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "/bin/sh", "-c", "#(nop) ", "CMD [\"/bin/bash\"]" ], "ArgsEscaped": true, "Image": "sha256:c0bda62fdbad65a3c6a1843d293a3a47d8233115cc6d384e3cb07c53580a2b43", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": { "org.label-schema.build-date": "20190927", "org.label-schema.license": "GPLv2", "org.label-schema.name": "CentOS Base Image", "org.label-schema.schema-version": "1.0", "org.label-schema.vendor": "CentOS" } }, "DockerVersion": "18.06.1-ce", "Author": "", "Config": { "Hostname": "", "Domainname": "", "User": "", "AttachStdin": false, "AttachStdout": false, "AttachStderr": false, "Tty": false, "OpenStdin": false, "StdinOnce": false, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ], "Cmd": [ "/bin/bash" ], "ArgsEscaped": true, "Image": "sha256:c0bda62fdbad65a3c6a1843d293a3a47d8233115cc6d384e3cb07c53580a2b43", "Volumes": null, "WorkingDir": "", "Entrypoint": null, "OnBuild": null, "Labels": { "org.label-schema.build-date": "20190927", "org.label-schema.license": "GPLv2", "org.label-schema.name": "CentOS Base Image", "org.label-schema.schema-version": "1.0", "org.label-schema.vendor": "CentOS" } }, "Architecture": "amd64", "Os": "linux", "Size": 219583055, "VirtualSize": 219583055, "GraphDriver": { "Data": { "MergedDir": "/var/lib/docker/overlay2/a067f0ea5f322c3ed3546084109cc747ba94b2f058b1b356e0a33bd643ebaf3e/merged", "UpperDir": "/var/lib/docker/overlay2/a067f0ea5f322c3ed3546084109cc747ba94b2f058b1b356e0a33bd643ebaf3e/diff", "WorkDir": "/var/lib/docker/overlay2/a067f0ea5f322c3ed3546084109cc747ba94b2f058b1b356e0a33bd643ebaf3e/work" }, "Name": "overlay2" }, "RootFS": { "Type": "layers", "Layers": [ "sha256:9e607bb861a7d58bece26dd2c02874beedd6a097c1b6eca5255d5eb0d2236983" ] }, "Metadata": { "LastTagTime": "0001-01-01T00:00:00Z" } } ]
5.5 修改镜像名,打标签
[email protected] ~]# docker tag nginx:latest hub.darren.com/library/nginx:version1
[[email protected] ~]# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu 16.04 5f2bf26e3524 8 days ago 123MB nginx latest 540a289bab6c 2 weeks ago 126MB hub.darren.com/library/nginx version1 540a289bab6c 2 weeks ago 126MB registry.cn-hangzhou.aliyuncs.com/google_containers/coredns 1.1.3 b3b94275d97c 17 months ago 45.6MB
拉取镜像
NAME DESCRIPTION STARS OFFICIAL AUTOMATED mysql MySQL is a widely used, open-source relation… 8787 [OK] mariadb MariaDB is a community-developed fork of MyS… 3080 [OK] mysql/mysql-server Optimized MySQL Server Docker images. Create… 652 [OK] centos/mysql-57-centos7 MySQL 5.7 SQL database server 64 centurylink/mysql Image containing mysql. Optimized to be link… 61 [OK] mysql/mysql-cluster Experimental MySQL Cluster Docker images. Cr… 56 deitch/mysql-backup REPLACED! Please use http://hub.docker.com/r… 41 [OK] bitnami/mysql Bitnami MySQL Docker Image 35 [OK] tutum/mysql Base docker image to run a MySQL database se… 34 schickling/mysql-backup-s3 Backup MySQL to S3 (supports periodic backup… 28 [OK] prom/mysqld-exporter 23 [OK] linuxserver/mysql A Mysql container, brought to you by LinuxSe… 22 centos/mysql-56-centos7 MySQL 5.6 SQL database server 17 circleci/mysql MySQL is a widely used, open-source relation… 16 mysql/mysql-router MySQL Router provides transparent routing be… 14 arey/mysql-client Run a MySQL client from a docker container 13 [OK] imega/mysql-client Size: 36 MB, alpine:3.5, Mysql client: 10.1.… 9 [OK] openshift/mysql-55-centos7 DEPRECATED: A Centos7 based MySQL v5.5 image… 6 fradelg/mysql-cron-backup MySQL/MariaDB database backup using cron tas… 4 [OK] ansibleplaybookbundle/mysql-apb An APB which deploys RHSCL MySQL 2 [OK] genschsa/mysql-employees MySQL Employee Sample Database 2 [OK] devilbox/mysql Retagged MySQL, MariaDB and PerconaDB offici… 2 jelastic/mysql An image of the MySQL database server mainta… 1 widdpim/mysql-client Dockerized MySQL Client (5.7) including Curl… 0 [OK] monasca/mysql-init A minimal decoupled init container for mysql 0
5.6 其他操作
删除镜像(注:如果镜像有容器生成,需要先删除容器) #如果一个镜像有多个tag,只会删除指定的tag,镜像本身不会删除,如果docker rmi后指定镜像ID,则所有tag都会被删除 docker rmi centos:6.6 # 删除无标签镜像(即为none) docker rmi $(docker images -q --filter "dangling=true") 导出和载入镜像 # 将本地镜像导出 docker save -o centos_6.6.tar centos:6.6 将本地文件导入镜像 docker load --input centos_6.6.tar 通过docker commit提交一个新镜像 docker commit -m "Add a new file" -a "Breeze" a925cb40b3f0 test #使用a925cb40b3f0容器生成一个名为test的镜像 -a:指定作者 -m:相关说明信息 -p:提交时暂停容器运行
博主声明:本文的内容来源主要来自誉天教育晏威老师,由本人实验完成操作验证,需要的博友请联系誉天教育(http://www.yutianedu.com/),获得官方同意或者晏老师(https://www.cnblogs.com/breezey/)本人同意即可转载,谢谢!
原文地址:https://www.cnblogs.com/zyxnhr/p/11830104.html