资讯专栏INFORMATION COLUMN

使用docker搭建开发环境

caozhijian / 2546人阅读

摘要:我的主力机是下面有太多提升效率的软件但是开发的时候不得不使用就单单开发而言我还是喜欢使用所以就造成了我得在下面使用虚拟机这是最开始的办法后面得知有这个东西之后用了一阵子感觉还不错但是我使用的时候动不动就会出现一些问题所以一怒之下决定学学然后

我的主力机是windows,windows下面有太多提升效率的软件.但是开发的时候不得不使用linux.就单单开发而言.我还是喜欢使用linux.所以就造成了我得在windows下面使用虚拟机.这是最开始的办法.后面得知有vagrant这个东西之后,用了一阵子感觉还不错.但是我使用的时候动不动就会出现一些问题,所以一怒之下决定学学docker.然后使用docker来作为开发环境.

使用docker作为开发环境大概我有这几点要求

部署快,不要换台机子装了一天的环境

稳定...

轻轻轻!

container得可以访问本机所在局域网

可以实现文件共享

在我接触了一阵子docker之后,发现docker可以满足我大部分意淫出来的美好开发环境.折腾一番之后终于搞定,于是祭出本文.希望可以帮助到需要的人.

学习本篇之前希望你对docker有一丢丢的了解,一丢丢就可以了.

安装.

我一般不喜欢讲如何安装一个软件,但是介于docker的一些问题.还是讲讲.

如果是windows10之前的用户,那么安装docker比较麻烦. 你可能需要一个Docker Toolbox的东西,具体安装方式请自行google.因为我的机子是Windows10的.

如果你是Windows10的用户,恭喜你.你只要点这里下载一个exe文件,然后就可以无脑安装了.但是要保证开启Hyper-V功能.如何开启看这里.注意,这个开启之后就不能使用virtualbox虚拟机了.

安装好之后,启动docker在左下角就可以看到docker的logo了.之后我们的操作都是在PowerShell/CMD下面执行的了.执行docker info会看到下面的内容

PS C:Windowssystem32WindowsPowerShellv1.0> docker info
Containers: 1
 Running: 1
 Paused: 0
 Stopped: 0
Images: 4
........

由于docker主机在外国,安装好之后我们需要更改下源,不然下载image的时候会很慢.这里使用daoCloud提供的镜像,你需要注册登录之后,获取到每个人独一无二的url.然后粘贴要下面就可以了.记得重启啊喂...

基本概念

在使用docker之前你要明白两个概念,两个学docker过程中一定会一直强调的概念

image

container (这种术语直接使用英文,不做翻译)

这两个是整个docker的基础概念,这里本着不负责任的侥幸心理大概的说一下这两个的区别.

image是静态的,类比为面向对象就是一个类

container是动态运行的,类比为面向对象就是一个实例化的对象.

一般,container是可运行的,我们启动一个container之后,这个container里面就是我们的linux环境.

懂得了上面的意思,你就明白了我们要做的事情很简单:找一个合适的image,这个image里面应该包含一切开发时候所需要的东西, 然后启动它,我们就可以在这个container环境上工作了.当然这个时候container应该可以跟宿主共享文件.并且可以在本局域网内可以被访问到.

在继续搭建我们的开发环境之前,我们还是要先学一点docker的命令和概念的.

id&&name

每个image都有一个唯一的id来标识,同样container也有.这个唯一的id一般很长,比如:c59dc2dfad95,但是一般我们输入的时候只要输入若干位能标识当前系统内唯一标识某一个image就可以了.比如只要输入c59d可能就可以标识这个image.除了id,还可以给一个image起名字,这样子也可以通过name来操作一个image.

run

通过docker run image_name可以直接启动本地的一个image.这个命令后面可以加很多子参数来开启其他功能.如果本地不存在这个image,那么docker会去官方的仓库去下载,这个仓库你可以理解为github一样的网站,上面存放了许多别人push上去的image.

tag

每个image都有一个名称.除了名称之外还有一个叫做tag的东西,这个称之为标签的东西可以用来标识同一个image的不同版本.如果你没有给一个image指定一个tag,那么docker会默认为这个iamge添加一个名为:latest的tag.如果你使用docker run ubuntu,那么就会默认运行ubuntu:latest.如果本地没有这个image,那么就会去从仓库下载ubuntu:latest的iamge.很多时候你会看到ubuntu:14.04的image.这个14.04就是代表这个image的tag.只是很多时候image制作者把tag用来标记version了而已.

docker images

这个命令会列出本地所有的images.每个image都会有一个独一无二的id.如下面 IMAGE ID字段.

PS C:Windowssystem32WindowsPowerShellv1.0> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu-ok           latest              5f93b91bc208        26 hours ago        423.7 MB
ubuntu              latest              a421b4d8494d        27 hours ago        423.1 MB
ubuntu              14.04               3f755ca42730        2 days ago          188 MB
docker ps

这个命令会列出所有在运行的container.当运行docker ps -a就会列出所有的container.包括已经退出的container.

docker commit

这个命令可以把一个container制作成一个image.

docker rm && docker rmi

docker rm container_id可以用来删除一个container.docker rmi image_id/image_name可以用来删除一个image.

AUFS

很多文章讲docker都会把这个放到后面一点讲.反正不会在类似"使用docker做开发环境"的文章里面讲. 但是我觉得这个东西是理解docker的关键.所以一定要讲.

AUFS比不是docker独有的,很多Linux的发行版中都用到了这个特性.说起AUFS,这个东西是UFS的升级版,前面的A就是代表advanced的意思.那AUFS/UFS到底是个什么东西?

所谓AUFS,Advanced Union File System 就是把不同物理位置的目录合并mount到同一个目录中.这种技术有一点典型的应用:有些linux发行版只要插入一个光盘就可以直接运行.不用进行安装.你对系统文件进行的增删改只是反映在电脑的硬盘上面,不会影响到光盘的内容.即对光盘只读不写.那么docker是如何使把这个技术应用到docker上?

docker把一个镜像分成了很多层layer.这些层合并在一起才成为了一个完整的image.这样子有什么好处?最直观的一点就是,ubuntu15.04跟ubuntu16.04的image可能只有一点点差别.这点差别体现在第四层layer上.那么ubuntu15.04跟16.04就可以共享前三层layer.这样子如果你本地有了ubuntu15.04的image.那么再pull ubuntu16.06的时候只要把第四层的pull下来就可以了.

而且,image的所有层都是只读的,当你启动一个image当做container运行的时候,docker会在image的只读层上加一层薄薄的可写层.你在container里面做的所有操作都是反映在可写层.当你退出container之后,下次启动同一个image,之前操作的所有东西都会没有掉.一个重新做人的image.

这个时候有一个问题就来了,我们pull一个image,启动了container.好不容易把该安装的软件都安装好了,然后退出了container.之前安装的软件就都没有了!这个时候我们就要使用commit命令了.commit命令可以把当前的可写层合并到image的只读层里面.这样子这个image又多了一层.下次我们启动这个image的时候安装的软件就都还在了.

一个image由好几层layer构成.每个layer都是一个只读层

当启动一个container之后,就会在iamge的只读层基础上添加一个可写层.所有对container执行的操作都反映在container上.(以上图片都来自docker文档.)

这里提一点,当使用docker images命令查看iamge信息的时候,后面的SIZE是表示当前iamge所占用的大小,但是不意味着所有SIZE相加起来就是占用磁盘空间的总大小.一定要注意,可能有image共享若干层layer.这些layer在相加的时候被计算了好几遍.

PS C:Windowssystem32WindowsPowerShellv1.0> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
                            12e32b701daa        25 minutes ago      188 MB
ubuntu              14.04               3f755ca42730        3 days ago          188 MB
centos              6                   8315978ceaaa        6 weeks ago         194.6 MB
删除

上面的命令提到删除有rm跟rmi两个,rm是用来删除一个已经退出的container.rmi是用来删除一个image的.有了上面AUFS的概念之后,要明白的是我们使用docker rm container_id的时候,其实只是删除掉了一层可写层的数据.因为只读层是container跟image共享的.只要iamge没有被删掉,那么只读层的数据一定也不会被删除掉.

同样,当多个image共享若干层只读层的时候,删除掉一个image.只是删除掉了这个image独有的一层只读层数据.其他共享的数据并没有被删除掉,只有当删除掉所有的image之后,共享的layer层才会被删掉.

执行删除命令的时候会看到如下的信息,这里每一次deleted都是代表删除掉了一层layer.

PS C:Windowssystem32WindowsPowerShellv1.0> docker rmi ubuntu-fin
Untagged: ubuntu-fin:latest
Deleted: sha256:9e0728e8edbaf72846c43c629590fba5f46b1d705111d3fb1d79b9cf03a6c50c
Deleted: sha256:d53e457ca7161cd6f2d1b6678ecaafd19043dcaeb1363471867e1047819268fa
Deleted: sha256:496ef4fa137e03d80cf821745f875860d3d3120447326b8609938aa70f2edbd9
Deleted: sha256:12e32b701daa90c435176a273b2b41b4bfb219523c1ae396dc2f7068bbb6c088
Deleted: sha256:e8f29656cf54ad60a17d4b38362d9207b52a846cce3cc13e245fc3b799ff53e9
Deleted: sha256:48f6b521c809e40468886b0a159040503d00a0abb1eabf310451edfea562b459
Deleted: sha256:e94abc94ab1aff00280016eaf0649a75270886a2b60c8fe862ca549a0601949f
Deleted: sha256:3f755ca4273009a8b9d08aa156fbb5e601ed69dc698860938f36b2109c19cc39
Deleted: sha256:565903b66233d5576592815ca4d499bd6fe09a9b4baf83f345aaf64544f1cd78
Deleted: sha256:b653e4373a4b35aa760ff67cfa3de2c9fe3c089823b63ec797eb04de256f86ba
Deleted: sha256:362e536c4e530b94ce4204461e4f8c998705bcb98c91be54dd40b22f50531f3a
Deleted: sha256:b69ad682d83af6a6962b4a60a0b5f033c8d39efcd20dbdf320b6dd8136e50aae
Deleted: sha256:bc224b1b676d12be2a49f99778dda08b90d22747244d0a0afcdf4cfeb7db5d89

我们再删除iamge的时候有时候不能成功删除.大概原因有一下几点:

container正在运行,你删除这个container会失败.应该使用docker stop container_id退出当前container再尝试删除.

container退出了,删除当前image也会失败.因为container虽然退出,当前container保存着运行环境等数据.container是在iamge的基础上添加了一层可写层.所以他们是共享只读层的.

删除一个iamge会有Untagged: ubuntu:14.04.这个不是没有删除成功.这个是因为有其他image跟这个ubuntu:14.04共享layer层.所以删除时候并没有真正删除掉layer层的数据.


ok,有了上面的预备知识,我们现在可以开始准备我们的环境了.刚刚说过,我们退出一个container之后在container所安装的软件,添加的文件等等数据都会丢失掉,所以正确的办法应该是:在一个container环境中配置好所有开发要用到的东西之后,使用docker commit命令来把当前这个container制作成一个image.然后下次我们启动这个image的时候环境就是我们所需要的了.但是这样子会存在三个问题:

当别人给你一个image之后,你知道这个image里面安装了哪些文件,修改了哪些数据么?

每次commit都会形成一个新的只读层.commit次数多了会使得image变得越来越臃肿.

再着,一个image动辄2,3G.带着这么大个文件跑也不优雅.

要解决上面的这些问题,就要使用Dockerfile了.所以我们开始之前还要做点功课.

Dockerfile

Dockerfile是用来描述如何构建一个image的,Dockerfile由一些指令构成,全部指令大概有20个左右,这里不全部讲解.只讲一些我们下面会用到的.具体Dockerfile的全部用法参考Docker官方出的最佳实践.

FROM

我们要制作的image必然是基于某个现有image的基础,from命令就是用来指定使用哪个基础iamge的.像很多ubuntu官方在Docker Hub上维护由官方的image.我们下面开发环境的搭建就是基于ubuntu:14.04的环境下完成的.

COPY && ADD

copy命令是把宿主机上的文件拷贝到image中.add可以是copy的高级版.

copy要求拷贝的文件在宿主机上存在

add可以指定一个url座位源文件,docker会自动去下载这个url的文件, 然后拷贝到image中.

我们待会儿就会用到add指令,因为我们需要使用163的ubuntu源来替换ubuntu原生的apt-get源.所以我们的Dockerfile会有类似的指令 : ADD http://mirrors.163.com/.help/sources.list.trusty /etc/apt/sources.list.

CMD

这个是指定启动一个container之后,默认执行的命令.我们执行docker run ubuntu:14.04启动一个container之后,默认就进入了bash界面.这就说明这个ubuntu:14.04的CMD就是bash.

这里要澄清一个概念.使用docker run之后默认进入了bash会让很多人以为启动container跟启动一个虚拟机没什么区别.其实不是的.docker的container就是为了某个进程而存在的,这个进程就是CMD所指定的程序.比如:CMD /bin/bash就是启动了bash.当我们退出了bash之后,整个container也就退出了.如果你的CMD写成:CMD service nginx start.你会发现container执行之后就马上结束了.这是因为整个container只是为了service nginx start这条命令而存在的,它不会管你这条命令启动了什么.默认启动的bash正好是一直在前台运行,只有你使用exit命令退出bash的时候才结束bash进程.这个时候container才结束.才会让人有container跟虚拟机差不多的错觉.

上面的这个概念很重要,一定要理解透彻.如果没有搞清楚这点.你会一直觉得docker跟虚拟机没有什么区别.

RUN

这个命令指定了在构建image时候image中药执行的命令.这么说可能有点蹩脚.举个例子,我们希望我们的镜像构建好的时候就安装好了git.那么我们就可以在Dockerfile里面写RUN apt-get -y install git.这样子在构建镜像的时候就会去安装git了.待会儿我们要安装的软件都是通过这个命令指定的.也是有了RUN指令,我们就可以知道一个image构建过程中做了一些什么操作.

好了.Dockerfile我们目前只需要这些指令.下面我们就根据上面学到的东西来快速的搭建我们所需要的开发环境.

实战--编写Dockerfile

我知道,上面那样子好像很随意的讲了一下Dockerfile,肯定也不会写.所以,这里我给出我构建iamge使用的Dockerfile作为参考.

FROM ubuntu:14.04
ADD http://mirrors.163.com/.help/sources.list.trusty /etc/apt/sources.list
COPY install.sh /usr/local/src/install.sh
COPY supervisord.conf /usr/local/src/supervisord.conf

RUN apt-get  update && 
    apt-get -y install build-essential && 
    apt-get -y install supervisor && 
    cp /usr/local/src/supervisord.conf /etc/supervisor/supervisord.conf && 
    apt-get -y install openssh-server && 
    apt-get -y install git && 
    apt-get -y install vim && 
    apt-get -y install lrzsz && 
    apt-get -y install libxml2-dev && 
    apt-get -y install  pkg-config libssl-dev libsslcommon2-dev && 
    apt-get -y install libbz2-dev && 
    apt-get -y install libcurl4-gnutls-dev && 
    apt-get -y install libjpeg8-dev && 
    apt-get -y install libpng-dev && 
    apt-get -y install libfreetype6-dev && 
    apt-get -y install libmcrypt-dev && 
    apt-get -y install libxslt-dev && 
    apt-get -y install libgmp-dev && 
    apt-get -y install libreadline-dev && 
    ln -s /usr/include/x86_64-linux-gnu/gmp.h /usr/include/gmp.h && 
    bash /usr/local/src/install.sh && 
    adduser --gecos "" --disabled-password chenjiayao &&  
    echo -e "1111
1111" | passwd chenjiayao && 
    echo -e "11
11" | passwd root

CMD supervisord -n

上面的Dockerfile其实相当的简单,指令都是我们上面用到的,这里再解释一下每一行的作用.

第一行FROM ubuntu:14.04指明了使用ubuntu官方维护的14.04的image作为基础image来构建自己的image.执行这条指令之后,如果你的本地没有ubuntu:14.04这个image的话, 那么就会去hub docker下载

第二行ADD指令上面提到了,这里就是使用163的源代替ubuntu内置的源,这样子下载软件的速度就会比较快.

接着是两个copy指令.这里从宿主机拷贝了两个文件到镜像中.其中install.sh是我自己写的编译安装php+apache的脚本文件,这里根据自己需要来决定.后面的supervisord是linux下面用来管理进程的软件.你会发现CMD启动的就是supervisord.后面-n参数说明是以前台的方式启动.而不是后台启动.这样子就避免了container运行一下就退出了.

RUN 里面都是在安装软件.执行一些必要的操作.你会发现我把所有的软件安装都写成了一个RUN指令.你可能会有疑问为什么不使用很多个RUN来编写.为什么要再一个RUN里面安装全部软件.这里就要说明一点 : 每执行一个Dockerfile的指令都会让我们的image增加一层只读层.所以,写很多指令的话,我们的image就会有太多的layer.所以尽量要克制命令的个数.

CMD命令.这里我没有使用默认的bash作为启动命令是因为:如果使用bash作为默认的启动进程之后,当前container就只会有一个进程bash.那么其他的apache.ssh等服务都不会自动启动.*每次运行container都得手动启动这些服务很麻烦.所以这里使用supervisor来管理.配置好supervisor之后,只要启动了supervisor,supervisor就会自动帮我们启动其他进程.比如apache.ssh等等.这样就比较方便.所以如果还不知道supervisor的童鞋,赶紧学起来,而且相当的简单.如果就是不学的同学,也不要急,后面我会给出我的Dockerfile和其他配置文件.可以直接clone我的.

好了,Dockerfile我们已经准备好了,下面使用docker build -t ubuntu-php .来构建自己的image了.但是在开始之前要强调一下build的命令.

build命令 接着 -t ubuntu-php表示构建好的image的名称.注意后面的.,这参数表示的是当前目录.很多时候我们在一个目录下创建了Dockerfile,编写好之后.使用powershell进入这个目录. 然后执行docker build -t image_name .就开始编译.很容易就以为最后一个参数是指定Dockerfile所在的目录.其实不是这样子的.这个目录指定的是当前docker编译这个image的工作目录.

要先明白,docker是一个C/S的软件,我们使用powerShell输入命令 .之后命令是被发送到服务端执行,然后返回结果的.这跟MySQL一样.只是我们把客户端和服务端安装在一台主机上.

当我们构建image的时候,执行类似COPY指令,那么把文件拷贝到image中,但是构建文件是在服务端完成的,如何让docker服务端得到拷贝的文件?这里我们就要指定一个docker构建的工作目录了.当构建开始的时候,docker会把工作目录下的所有文件都发送到服务端.然后开始构建.这样子他就可以得到我们要copy到image的文件了.

所以我们构建的时候指定.是想把当前目录下的文件等发送到docker服务端进行构建.只是在上面,我们的Dockerfile正好是放在了docker构建image的工作目录中了.

那么,既然上面的参数不是指定Dockerfile所在的目录.那如果我的机子上有多个Dockerfile的话,那么docker会使用哪个?我编写这个Dockerfile的目的就是希望使用这个Dockerfile.这个不用担心. 如果你在build的时候没有指定使用哪个Dockerfile.默认会使用构建iamge的工作目录下名字为Dockerfile的那个Dockerfile....听着有点晕...如果不想理清楚这些问题.每次构建的时候使用powerShell进入Dockerfile所在的目录下,然后执行docker build image_name .就可以了.

在构建过程中会输出类似下面的内容

PS D:codedockerubuntu> docker build -t ubuntu-fin .
Sending build context to Docker daemon 8.192 kB
Step 1 : FROM ubuntu:14.04
 ---> 3f755ca42730
Step 2 : ADD http://mirrors.163.com/.help/sources.list.trusty /etc/apt/sources.list
Downloading [==================================================>]    872 B/872 B
 ---> 386d7ab302b9
Removing intermediate container f183c42cf864
Step 3 : COPY supervisord.conf /usr/local/src/supervisord.conf
 ---> 8ce5250f8498
Removing intermediate container 2c6d89b3be22
Step 4 : COPY install.sh /usr/local/src
 ---> efa055e7d1b3
Removing intermediate container e0c7dacd9136
Step 5 : RUN apt-get  update &&     apt-get -y install build-essential &&     apt-get -y install supervisor &&  cp /usr/local/src/supervisord.conf /etc/supervisor/supervisord.conf &&     apt-get -y install openssh-server &&     apt-get -y install git &&     apt-get -y install vim &&     apt-get -y install lszrz &&     apt-get -y install libxml2-dev &&     apt-get -y install  pkg-config libssl-dev libsslcommon2-dev &&     apt-get -y install libbz2-dev &&     apt-get -y install libcurl4-gnutls-dev &&     apt-get -y install libjpeg8-dev &&     apt-get -y install libpng-dev &&     apt-get -y install libfreetype6-dev &&     apt-get -y install libmcrypt-dev &&     apt-get -y install libxslt-dev &&     apt-get -y install libgmp-dev &&     apt-get -y install libreadline-dev &&     ln -s /usr/include/x86_64-linux-gnu/gmp.h /usr/include/gmp.h &&     bash /usr/local/src/install.sh &&     adduser --gecos "" --disabled-password chenjiayao &&     echo -e "1111
1111" | passwd chenjiayao &&     echo -e "11
11" | passwd root
 ---> Running in 1dd5ade41249

发现,每一个Step其实就是执行Dockerfile中的每一个指令.好了,构建已经开始,等待构建结束之后,我们的环境也就搭建好了,建议把Dockerfile等构建必须的文件放到github上面,以后换一个环境.只要下载文件.然后就可以构建了.

这里我放出我构建环境时写的Dockerfile,有需要自取.传送门.

最后我们还有三个问题需要解决:

文件共享

端口映射

commit制作镜像

这些问题,考虑到文章篇幅应该够多,所以将再开一篇文章简介.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/27989.html

相关文章

  • 使用 Docker 搭建 Laravel 本地环境

    摘要:本文就介绍如何使用搭建本地环境。讲座介绍是现在火热的一个容器技术,作为开发人员有必要掌握它的使用,无论你是用来搭建本地环境,还是部署应用。 (原文地址:https://blog.tanteng.me/2017/...) Laravel 官方提供 Homestead 和 Valet 作为本地开发环境,Homestead 是一个官方预封装的 Vagrant Box,也就是一个虚拟机,但是跟...

    StonePanda 评论0 收藏0
  • 使用 Docker 搭建 Laravel 本地环境

    摘要:本文就介绍如何使用搭建本地环境。讲座介绍是现在火热的一个容器技术,作为开发人员有必要掌握它的使用,无论你是用来搭建本地环境,还是部署应用。 (原文地址:https://blog.tanteng.me/2017/...) Laravel 官方提供 Homestead 和 Valet 作为本地开发环境,Homestead 是一个官方预封装的 Vagrant Box,也就是一个虚拟机,但是跟...

    lscho 评论0 收藏0
  • 使用 Docker 搭建开发环境

    摘要:做了一次分享,主题使用搭建开发环境,简单介绍了一下的概念,演示了使用构建全套环境。应场景通常于如下场景应的动化打包和发布动化测试和持续集成发布在服务型环境中部署和调整数据库或其他的后台应从头编译或者扩展现有的或平台来搭建的环境。 做了一次分享,主题《使用 Docker 搭建开发环境》,简单介绍了一下 Docker 的概念,演示了使用 Docker-compose 构建全套 PHP 环境...

    zxhaaa 评论0 收藏0
  • 使用 Docker 搭建开发环境

    摘要:做了一次分享,主题使用搭建开发环境,简单介绍了一下的概念,演示了使用构建全套环境。应场景通常于如下场景应的动化打包和发布动化测试和持续集成发布在服务型环境中部署和调整数据库或其他的后台应从头编译或者扩展现有的或平台来搭建的环境。 做了一次分享,主题《使用 Docker 搭建开发环境》,简单介绍了一下 Docker 的概念,演示了使用 Docker-compose 构建全套 PHP 环境...

    kycool 评论0 收藏0
  • 使用 docker 搭建 web 服务环境

    摘要:国内的镜像仓库由于地理位置的原因,国内访问的官方仓库是比较慢的,所以在这里介绍一个国内的仓库灵雀云。灵雀云镜像仓库中汇集了大量来自社区的优质作品,让用户组合复用容器化微服务,轻松搭建新一代云端应用。 本文目的 做过开发的人对开发环境的安装、配置应该都不会太陌生,不管你做什么开发,对开发环境都会具有一定的依赖性的。对于 PHP 的 Web 开发来说,开发环境至少要有一个 Web 服务器(...

    Jeffrrey 评论0 收藏0

发表评论

0条评论

caozhijian

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<