Docker 基础

服务器

浏览数:5

2019-5-16

1. Docker 简介

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中, 然后发布到任何流行的 Linux 机器上 
Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。Docker 容器通过 Docker 镜像来创建。容器与镜像的关系类似于面向对象编程中的对象与类

Docker daemon 作为服务端接受来自客户的请求,并处理这些请求(创建、运行、分发容器)。 客户端和服务端既可以运行在一个机器上,也可通过 socket 或者RESTful API 来进行通信 
Docker daemon 一般在宿主主机后台运行,等待接收来自客户端的消息。 Docker 客户端则为用户提供一系列可执行命令,用户用这些命令实现跟 Docker daemon 交互. 
vms 和 docker 的区别

Docker 是如何工作: 
如下图:

 

 

Docker 守护进程 
如图所示, Docker 守护进程运行在一台主机上。用户并不直接和守护进程进行交互,而是通过 Docker 客户端间接和其通信

Docker 客户端 
Docker 客户端,实际上是 docker 的二进制程序,是主要的用户与 Docker 交互方式。它接收用户指令并且与背后的Docker 守护进程通信,如此来回往复

Docker 内部 
理解 Docker 内部构建,需要理解以下三种部件: 
Docker 镜像 – Docker images 
Docker 仓库 – Docker registeries 
Docker 容器 – Docker containers

2. 基本概念

Docker 包括三个基本概念 
镜像(Image ) 
容器(Container ) 
仓库(Repository )

2.1 Docker 镜像

Docker 镜像是 Docker 容器运行时的只读模板,每一个镜像由一系列的层 (layers) 组成。Docker 使用 UnionFS 来将这些层联合到单独的镜像中。 UnionFS 允许独立文件系统中的文件和文件夹(称之为分支)被透明覆盖,形成一个单独连贯的文件系统。正因为有了这些层的存在, Docker 是如此的轻量。当你改变了一个 Docker 镜像,比如升级到某个程序到新的版本,一个新的层会被创建。因此, 不用替换整个原先的镜像或者重新建立, 只是一个新 的层被添加或升级了。现在你不用重新发布整个镜像,只需要升级,层使得分发 Docker 镜像变得简单和快速。

2.2 Docker 仓库

Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。同样的, Docker 仓库也有公有和私有的概念。公有的Docker 仓库名字是 Docker Hub。 Docker Hub 提供了庞大的镜像集合供使用。这些镜像可以是自己创建,或者在别人的镜像基础上创建。 Docker 仓库是Docker 的分发部分。

2.3 Docker 容器

Docker 容器和文件夹很类似, 一个Docker容器包含了所有的某个应用运行所需要的环境。每一个 Docker 容器都是从Docker 镜像创建的。 Docker 容器可以运行、开始、停止、移动和删除。每一个 Docker 容器都是独立和安全的应用平台, Docker 容器是 Docker 的运行部分。

3. 安装Docker

当前的系统版本 centos 7.2 配置了aliyun的yum 源 和epel 源 ,关闭了selinux 和firewalld ,当前IP:10.0.0.160

安装:

[root@localhost ~]# yum -y install docker-io

  

获取docker 当前版本:

[root@localhost ~]# docker version
Client:
 Version:         1.13.1
 API version:     1.26
 Package version: 
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

  

启动docker daemon后台守护进程

[root@localhost ~]# systemctl enable docker
[root@localhost ~]# systemctl start docker

  

3.1 镜像加速

Docker HUB : Docker镜像首页, 包括官方镜像和其它公开镜像 
https://hub.docker.com/explore/ 
因为国情的原因,国内下载 Docker HUB 官方的相关镜像比较慢,可以使用国内的一些镜像进行加速 
这里我使用的是 Docker 中国的registry mirror 
配置文件如下

[root@localhost ~]# vim /etc/docker/daemon.json 
[root@localhost ~]# cat /etc/docker/daemon.json 
{
"registry-mirrors": [
"https://registry.docker-cn.com"
]
}

  之后重新启动服务。

[root@localhost ~]# systemctl daemon-reload 
[root@localhost ~]# systemctl restart docker

  

4. Docker 一些常用命令

docker 命令的帮助:

attach      Attach to a running container  # 当前 shell 下 attach 连接指定运行镜像
  build       Build an image from a Dockerfile  # 通过 Dockerfile 定制镜像
  commit      Create a new image from a container's changes # 提交当前容器为新的镜像
  cp          Copy files/folders between a container and the local filesystem   # 从容器中拷贝指定文件或者目录到宿主机中
  create      Create a new container  # 创建一个新的容器,同 run,但不启动容器
  diff        Inspect changes on a container's filesystem  # 查看 docker 容器变化
  events      Get real time events from the server  # 从 docker 服务获取容器实时事件
  exec        Run a command in a running container  # 在已存在的容器上运行命令
  export      Export a container's filesystem as a tar archive  # 导出容器的内容流作为一个 tar 归档文件[对应import ]
  history     Show the history of an image  # 展示一个镜像形成历史
  images      List images  # 列出系统当前镜像
  import      Import the contents from a tarball to create a filesystem image   # 从tar包中的内容创建一个新的文件系统映像[对应 export]
  info        Display system-wide information  # 显示系统相关信息
  inspect     Return low-level information on Docker objects  # 查看容器详细信息
  kill        Kill one or more running containers  # kill 指定 docker 容器
  load        Load an image from a tar archive or STDIN  # 从一个 tar 包中加载一个镜像[对应 save]
  login       Log in to a Docker registry  # 注册或者登陆一个 docker 源服务器
  logout      Log out from a Docker registry # 从当前 Docker registry 退出
  logs        Fetch the logs of a container  # 输出当前容器日志信息
  pause       Pause all processes within one or more containers    # 暂停容器
  port        List port mappings or a specific mapping for the container # 查看映射端口对应的容器内部源端口
  ps          List containers  # 列出容器列表
  pull        Pull an image or a repository from a registry   # 从docker镜像源服务器拉取指定镜像或者库镜像 
  push        Push an image or a repository to a registry   # 推送指定镜像或者库镜像至docker源服务器
  rename      Rename a container   #重命名容器
  restart     Restart one or more containers # 重启运行的容器
  rm          Remove one or more containers  ## 移除一个或者多个容器
  rmi         Remove one or more images  # 移除一个或多个镜像[无容器使用该镜像才可删除,否则需删除相关容器才可继续或 -f 强制删除]
  run         Run a command in a new container  # 创建一个新的容器并运行一个命令
  save        Save one or more images to a tar archive (streamed to STDOUT by default)  # 保存一个镜像为一个 tar 包[对应 load]
  search      Search the Docker Hub for images  # 在 docker hub 中搜索镜像
  start       Start one or more stopped containers # 启动容器 
  stats       Display a live stream of container(s) resource usage statistics  #显示容器资源使用统计
  stop        Stop one or more running containers # 停止容器
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE  # 给源中镜像打标签
  top         Display the running processes of a container  # 查看容器中运行的进程信息
  unpause     Unpause all processes within one or more containers  # 取消暂停容器
  update      Update configuration of one or more containers   
  version     Show the Docker version information  # 查看 docker 版本号
  wait        Block until one or more containers stop, then print their exit codes  # 截取容器停止时的退出状态值

  

查找 star 数至少为 100 的镜像

docker search -s 100 ubuntu

  

默认不加 s选项找出所有相关 ubuntu 镜像

5. 使用镜像

5.1 拉取镜像 和 推送镜像

#拉取镜像相关操作
[root@localhost ~]# docker pull --help
Usage:  docker pull [OPTIONS] NAME[:TAG|@DIGEST]  #用法
Pull an image or a repository from a registry
Options:
  -a, --all-tags                Download all tagged images in the repository
      --disable-content-trust   Skip image verification (default true)
      --help                    Print usage

#推送镜像相关操作
[root@localhost ~]# docker push --help
Usage:  docker push [OPTIONS] NAME[:TAG]
Push an image or a repository to a registry
Options:
      --disable-content-trust   Skip image verification (default true)
      --help                    Print usage

  

例:

# 下载官方 ubuntu docker 镜像
[root@localhost ~]# docker pull ubuntu

# 下载指定版本 ubuntu 官方镜像
[root@localhost ~]# docker pull ubuntu:14.04 

 # 推送镜像库到私有源[可注册 docker 官方账户,推送到官方自有账
户]   省略 

  

5.2 列出镜像

显示当前系统镜像,不包括过渡层镜像

[root@localhost ~]# docker images

  

显示当前系统所有镜像

[root@localhost ~]# docker images -a

  

5.3 删除镜像

docker rmi 
删除一个或者多个镜像

[root@localhost ~]# docker rmi --help
Usage:  docker rmi [OPTIONS] IMAGE [IMAGE...]
Options:
  -f, --force      Force removal of the image  #强制移除镜像不管是否有容器使用该镜像 
      --help       Print usage
      --no-prune   Do not delete untagged parents # 不要删除未标记的父镜像

  

6. 操作容器

参数太多可以任意组合,可以查看帮助信息

6.1 运行容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态 
(stopped ) 的容器重新启动。 
一般随时删除和新创建容器。

1. 新建并启动

[root@localhost ~]# docker run ubuntu /bin/echo 'hello world';
hello world

  

跟在本地直接执行 /bin/echo ‘hello world’ 几乎感觉不出任何区别,但是是在ubuntu里面执行的这个echo。

启动一个 bash 终端,允许用户进行交互

[root@localhost ~]# docker run -it ubuntu /bin/bash
root@50f0b7fa748a:/# echo 'hello world'
hello world

  

利用 docker run 来创建容器时,Docker 在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从公有仓库下载

  • 利用镜像创建并启动一个容器

  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层

  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去

  • 从地址池配置一个 ip 地址给容器

  • 执行用户指定的应用程序

  • 执行完毕后容器被终止

2. 启动已终止容器 

可以利用 docker container start 命令,直接将一个已经终止的容器启动运行。 
先查看容器的id

[root@localhost ~]# docker container start 9b0fc -i
hello world

  

 查看容器

#查看正在运行的容器
docker ps
#查看所有容器
docker ps -a

  

#查看指定容器信息
[root@localhost ~]# docker inspect  容器ID或者容器NAME

  

注:

后台运行容器 
上面的容器退出,就都停止了,所以有时候容器需要一直运行了

[root@localhost ~]# docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"

  

此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 
docker logs 查看)。 

获取容器的输出信息,可以通过 docker container logs 容器id命令。

容器是否后台运行跟-d 参数 无关, 而是跟后面启动的命令有关

6.2 终止容器

使用 docker container stop 容器id来终止一个运行中的容器。 
当 Docker 容器中指定的应用终结时,容器也自动终止。

[root@localhost ~]# docker container stop 2bf0f

  

处于终止状态的容器,想启动怎么办

启动容器
docker container start 容器id或者容器NAME
重启容器
docker container restart 容器id或者容器NAME

  

6.3 进入容器

在使用 -d 参数时,容器启动后会进入后台。某些时候需要进入容器进行操作, 
这个时候 docker exec 命令 

进入容器的前提,这个容器必须要是运行的状态

[root@localhost ~]# docker exec -it 2bf0 bash

  

6.5 导出和导入

导出 
导出本地某个容器,可以使用 docker export 命令

[root@localhost ~]# docker export 50f0b7fa748a > ubuntu.tar

  

 这样将导出容器快照到本地文件。

导入容器快照 
可以使用 docker import 从容器快照文件中再导入为镜像,例如

[root@localhost ~]# cat ubuntu.tar | docker import - test/ubuntu:v1.0

  

 也可以通过指定 URL 或者某个目录来导入,例如

docker import http://example.com/exampleimage.tgz example/imagerepo

  

注:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker 
import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历 
史记录和元数据信息(即仅保存容器当时的快照状态) ,而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

6.6 删除容器

可以使用 docker container rm 来删除一个处于终止状态的容器。例如

1. 删除指定容器

[root@localhost ~]# docker container rm distracted_franklin

  

删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

2. 清理所有处于终止状态的容器 

用 docker container ls -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太 

多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

[root@localhost ~]# docker container prune 

  

7. 数据管理

如何在 Docker 内部以及容器之间管理数据,在容器中管理数据主要有两种方式:

  • 数据卷(Volumes)

  • 挂载主机目录 (Bind mounts)

7.1 数据卷

数据卷 是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特 
性:

    • 数据卷 可以在容器之间共享和重用

    • 对 数据卷 的修改会立马生效

    • 对 数据卷 的更新,不会影响镜像

    • 数据卷 默认会一直存在,即使容器被删除 

注意: 数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷 。

 1. 创建一个数据卷

[root@localhost ~]# docker volume create my-vol
my-vol

  

2. 查看所有的 数据卷

[root@localhost ~]# docker volume ls

  

3. 查看指定 数据卷 的信息

[root@localhost ~]# docker volume inspect my-vol

  

启动一个挂载数据卷的容器 

创建一个名为 web 的容器,并加载一个 数据卷 到容器的 /wepapp 目录。

[root@localhost ~]# docker run -d -P \
--name web \
-v my-vol:/wepapp \
training/webapp \
python app.py

  

-d 后台运行 ,-P 把docker内部的端口,随机映射到主机的某端口

进入容器看看是否有这个/wepapp 这个目录

[root@localhost ~]# docker exec -it web bash

  

查看数据卷的具体信息 
主机里使用以下命令可以查看 web 容器的信息

[root@localhost ~]# docker inspect web

  

数据卷 信息在 “Mounts” Key 下面

 删除数据卷 

数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷 ,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷 。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令。

 docker volume rm 数据卷名

  

无主的数据卷可能会占据很多空间,要清理请使用以下命令

docker volume prune

  

7.2 挂载主机目录

挂载一个主机目录作为数据卷 
创建数据目录绑定/src/webapp/到到新建容器,

[root@localhost ~]# mkdir -p /src/webapp/
[root@localhost ~]# docker run -it --name ubuntu-webapp  -v /src/webapp:/src/webapp ubuntu:14.04 /bin/bash

  

查看数据卷的具体信息

 Docker 挂载主机目录的默认权限是 读写,用户也可以通过增加ro 指定为 只读

例:

docker run -d -P \
-v /src/webapp:/opt/webapp:ro \
training/webapp \
python app.py

  

 挂载一个本地主机文件作为数据卷

[root@localhost ~]# docker --help > docker_help.txt
[root@localhost ~]# docker run --rm -it \
-v /root/docker_help.txt:/root/docker_help \
ubuntu:17.10 \
bash

  

8. Docker 网络

Docker 允许通过外部访问容器或容器互联的方式来提供网络服务。

8.1 外部访问容器

容器中可以运行一些网络应用,要让外部也可以访问这些应用,可以通过 -P 或 -p 参数来 
指定端口映射。

当使用 -P标记时,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。 

使用 docker container ls 可以看到,本地主机的 49155 被映射到了容器的 5000 端口。此 
时访问本机的 32769 端口即可访问容器内 web 应用提供的界面。

 

 同样的,可以通过 docker logs 命令来查看应用的信息。

-p 则可以指定要映射的端口,并且,在一个指定端口上只可以绑定一个容器。支持的 
有 ip:hostPort:containerPort 或 ip::containerPort 或 hostPort:containerPort 。

1. 映射所有接口地址

使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口,可以执行

[root@localhost ~]# docker run -d -p 5000:5000 training/webapp python app.py

  

此时默认会绑定本地所有接口上的所有地址给映射

2. 映射到指定地址的指定端口 
可以使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址,比如 localhost 地址 
127.0.0.1

[root@localhost ~]# docker container stop zen_galileo 
[root@localhost ~]# docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

  

 只有本机才能访问

3. 映射到指定地址的任意端口
使用 ip::containerPort 绑定 localhost 的任意端口到容器的 5000 端口,本地主机会自动分 
配一个端口。

[root@localhost ~]# docker run -d -p 127.0.0.1::5000 training/webapp python app.py

  

 还可以使用 udp 标记来指定 udp 端口

9. Dockerfile 构建镜像

Docker 可以通过 Dockerfile 的内容来自动构建镜像。 Dockerfile 是一个包含创建镜像所有命令的文本文件,通过docker build命令可以根据 Dockerfile 的内容构建镜像,

9.1 Dockerfile 的基本语法结构。

Dockerfile 有以下指令选项: 

FROM 
MAINTAINER 
RUN 
CMD 
EXPOSE 
ENV 
ADD 
COPY 
ENTRYPOINT 
VOLUME 
USER 
WORKDIR 
ONBUILD

  

FROM 
用法:

FROM  <image>
或者
FROM指定构建镜像的基础源镜像,如果本地没有指定的镜像,则会自动从 Docker 的公共库 pull 镜像下来。
FROM必须是 Dockerfile 中非注释行的第一个指令,即一个 Dockerfile 从FROM语句开始。
FROM可以在一个 Dockerfile 中出现多次,如果有需求在一个 Dockerfile 中创建多个镜像。
如果FROM语句没有指定镜像标签,则默认使用latest标签。

  

MAINTAINER 
用法:

MAINTAINER <name>
指定创建镜像的用户

  

RUN 
RUN 有两种使用方式

    • shell 格式 
      就像直接在命令行中输入的命令一样。

    • exec 格式:

RUN ["可执行文件", "参数1", "参数2"] 
这更像是函数调用中的格式。

  

每条RUN指令将在当前镜像基础上执行指定命令,并提交为新的镜像,后续的RUN都在之前RUN提交后的镜像为基础,镜像是分层的,可以通过一个镜像的任何一个历史提交点来创建,类似源码的版本控制

注:镜像是只读的

CMD 
CMD有三种使用方式:

  • shell 格式: CMD <命令>

  • exec 格式: CMD [“可执行文件”, “参数1”, “参数2”…]

  • 参数列表格式: CMD [“参数1”, “参数2”…] 。在指定了 ENTRYPOINT 指令后,用 CMD 指 定具体的参数。 

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。 
CMD指定在 Dockerfile 中只能使用一次,如果有多个,则只有最后一个会生效。

CMD的目的是为了在启动容器时提供一个默认的命令执行选项。如果用户启动容器时指定了运行的命令,则会覆盖掉CMD指定的命令。 
CMD会在启动容器的时候执行, build 时不执行,而RUN只是在构建镜像的时候执行,后续镜像构建完成之后,启动容器就与RUN无关了

EXPOSE 
声明端口

EXPOSE <port> [<port>...]

告诉 Docker 服务端容器对外映射的本地端口,需要在 docker run 的时候使用-p或者-P选项生效。

ENV 
设置环境变量格式有两种:

ENV <key> <value> # 只能设置一个变量
ENV <key1>=<value1> <key2>=<value2>...  # 允许一次设置多个变量

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN ,还是运行时的应用,都可以直接使用这里定义的环境变量。 

ADD

COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]

ADD复制本地主机文件、目录或者远程文件 URLS 从 并且添加到容器指定路径中 。

如果<源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip , bzip2 以及 xz 的情况下, ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去 

COPY

COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]

COPY复制新文件或者目录从 并且添加到容器指定路径中 。不能指定远程文件 URLS。

ADD和COPY 差异

  1. ADD 的 源路径 可以是一个 URL ,COPY 的不可以

  2. ADD 源路劲 为一个 tar 压缩文件的话,压缩格式为 gzip , bzip2 以及 xz 的情况下, ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。 COPY 不会自动解压到目标路劲

使用场合注意
因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 
COPY 指令,仅在需要自动解压缩的场合使用 ADD 。 
尽可能的使用 COPY ,因为 COPY 的语义很明确

 

ENTRYPOINT 
ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。 
配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖,而CMD是可以被覆盖的。如果需要覆盖,则可以使用docker run –entrypoint选项。 
每个 Dockerfile 中只能有一个ENTRYPOINT,当指定多个时,只有最后一个生效。

VOLUME 
格式为:

VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

创建一个可以从本地主机或其他容器挂载的挂载点

USER 
USER 指定当前用户

格式: USER <用户名>

指定运行容器时的用户名或 UID,后续的RUN、 CMD、 ENTRYPOINT也会使用指定用户。

注:USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

  

WORKDIR 
格式为

WORKDIR <工作目录路径> 

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录) ,以后各层的当前目录就被改为指定的目录,如该目录不存在, WORKDIR 会帮你建立目录 
为后续的RUN、 CMD、 ENTRYPOINT指令配置工作目录。可以使用多个WORKDIR指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径

  

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
最终路径是/a/b/c。

  

ONBUILD 
格式:

ONBUILD <其它指令> 。

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN , COPY 等,而这些指令, 
在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候 
才会被执行。 
Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人 
定制自己而准备的。

一个简单的案例

构建java环境

[root@localhost jdk]# ls
Dockerfile  jdk-8u191-linux-x64.tar.gz
[root@localhost jdk]# pwd
/data/docker/jdk
[root@localhost jdk]# docker build -t ubuntu:jdk1.8 .
[root@localhost jdk]# docker images
[root@localhost jdk]# docker run -it ubuntu:jdk1.8 bash
root@766e70fb47c2:/# java

  

 

 

作者:keme