docker初识

docker

Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。
Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。
Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。
总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样

Docker使用场景:
  • web应用的自动化打包和发布;
  • 自动化测试和持续集成、发布;
  • 在服务型环境中部署和调整数据库或其他的后台应用;
  • 从头编译或者扩展现有的OpenShift或Cloud Foundry平台来搭建自己的PaaS环境。
docker构成:

Docker系统有两个程序:docker服务端docker客户端

  • 其中docker服务端是一个服务进程,管理着所有的容器。
  • docker客户端则扮演着docker服务端的远程控制器,可以用来控制docker的服务端进程。
  • 大部分情况下,docker服务端和客户端运行在一台机器上。
Docker 的用途

Docker 的主要用途,目前有三大类。
(1)提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。
(2)提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。
(3)组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

Docker 的安装

Docker 是一个开源的商业产品,有两个版本:

  • 社区版(Community Edition,缩写为 CE));
  • 企业版(Enterprise Edition,缩写为 EE)
    企业版包含了一些收费服务,个人开发者一般用不到。故目前只学习社区版。mac安装docker

安装成功后,需要验证一下:

$ docker version
$ docker info
 //上面两个方法都可以验证
  • docker version 打印结果
Client:
 Version:           18.06.1-ce
 API version:       1.38
 Go version:        go1.10.3
 Git commit:        e68fc7a
 Built:             Tue Aug 21 17:21:31 2018
 OS/Arch:           darwin/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.1-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.3
  Git commit:       e68fc7a
  Built:            Tue Aug 21 17:29:02 2018
  OS/Arch:          linux/amd64
  Experimental:     true
  • docker info 打印结果
Containers: 8
 Running: 2
 Paused: 0
 Stopped: 6
Images: 5
Server Version: 18.06.1-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 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 logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 468a545b9edcd5932818eb9de8e72413e616e86e
runc version: 69663f0bd4b60df09991c08812a60108003fa340
init version: fec3683
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.9.93-linuxkit-aufs
Operating System: Docker for Mac
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.952GiB
Name: linuxkit-025000000001
ID: 4J63:BVUQ:VXQ7:PT5T:KJEQ:HTKK:VNK2:V6NC:YEUH:C4QK:IYSW:VJWP
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
 File Descriptors: 24
 Goroutines: 51
 System Time: 2018-08-31T01:36:05.427042926Z
 EventsListeners: 2
HTTP Proxy: gateway.docker.internal:3128
HTTPS Proxy: gateway.docker.internal:3129
Registry: https://index.docker.io/v1/
Labels:
Experimental: true
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

或者可以指定查询特定项

$ docker --version
Docker version 18.03, build c97c6d6

$ docker-compose --version
docker-compose version 1.22.0, build 8dd22a9

$ docker-machine --version
docker-machine version 0.14.0, build 9ba6da9

hello world 实例

在确定了docker的版本之后,就可以尝试打印一下hello-world:

$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

image文件

Docker 把应用程序及其依赖,打包在 image 文件里面。只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。
image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。
举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。
image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库 Docker Hub 是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

  • 在打印hello-world成功之后,可以操作生成的image文件:
# 列出本机的所有 image 文件。
$ docker image ls

# 删除 image 文件
$ docker image rm [imageName]
  • 列出本机所有image文件
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              71c43202b8ac        34 hours ago        109MB
docker              latest              8f769f924e65        8 days ago          153MB
ubuntu              latest              16508e5c265d        8 days ago          84.1MB
hello-world         latest              2cb0d9787c4d        7 weeks ago         1.85kB
learn/tutorial      latest              a7876479f1aa        5 years ago         128MB

容器文件

image 文件生成的容器实例,本身也是一个文件,称为容器文件。也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。

  • 查询和删除容器文件
# 列出本机正在运行的容器
$ docker container ls

# 列出本机所有容器,包括终止运行的容器
$ docker container ls --all

// 在输入run后的信息后,hello world 就会停止,但有些容器不会自动停止,需要手动让它终止
$ docker container kill [containID]

# 终止运行的容器文件,依然会占据硬盘空间,使用rm删除
$ docker container rm -f [containerID]
  • docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
  • docker container ls --all
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                           PORTS                NAMES
27dfef4ff215        hello-world         "/hello"                 20 minutes ago      Exited (0) 20 minutes ago                             romantic_haibt
5e2962fe617b        nginx               "nginx -g 'daemon of…"   16 hours ago        Created                                               webserver
24826d2c60fe        hello-world         "/hello"                 16 hours ago        Exited (0) 16 hours ago                               suspicious_stallman
6c0cf43f6a08        ubuntu              "bash"                   16 hours ago        Exited (255) About an hour ago                        gifted_hopper
04f15a35f737        hello-world         "/hello"                 16 hours ago        Exited (0) 16 hours ago                               vigorous_goldwasser
830d2efcbab8        docker              "docker-entrypoint.s…"   16 hours ago        Exited (1) 16 hours ago                               loving_darwin
8cb11c08eb7d        learn/tutorial      "echo 'hello world'"     17 hours ago        Exited (0) 17 hours ago                               pensive_raman
4a8dd3b1ab62        nginx               "nginx -g 'daemon of…"   18 hours ago        Exited (255) About an hour ago   0.0.0.0:80->80/tcp   websever
c7106877f963        hello-world         "/hello"                 18 hours ago        Exited (0) 18 hours ago                               cocky_lewin

Dockerfile 文件

制作 image 文件。
学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。
这就需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件。

创建Dockerfile文件

下面将介绍如何通过Dockerfile的定义文件和docker build命令来构建镜像。
Dockerfile使用基本的基于DSL语法的指令来构建一个Docker镜像,之后使用docker build命令基于该Dockerfile中的指令构建一个新的镜像。

# mkdir /opt/static_web
# cd /opt/static_web/
# vim Dockerfile

首先创建一个名为static_web的目录用来保存Dockerfile,这个目录就是我们的构建环境(build environment),Docker则称此环境为上下文(context)或者构建上下文(build context)。Docker会在构建镜像时将构建上下文和该上下文中的文件和目录上传到Docker守护进程。这样Docker守护进程就能直接访问你想在镜像中存储的任何代码、文件或者其他数据。这里我们还创建了一个Dockerfile文件,我们将用它构建一个能作为Web服务器的Docker镜像。
直接将这一段粘贴到Dockerfile中:

# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER Bourbon Tian "bourbon@1mcloud.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
EXPOSE 80

Dockerfile由一系列指令和参数组成。每条指令都必须为大写字母,切后面要跟随一个参数。Dockerfile中的指令会按照顺序从上到下执行,所以应该根据需要合理安排指令的顺序。每条指令都会创建一个新的镜像层并对镜像进行提交。Docker大体上按照如下流程执行Dockerfile中的指令。

Docker从基础镜像运行一个容器。
执行第一条指令,对容器进行修改。
执行类似docker commit的操作,提交一个新的镜像层。
Docker再基于刚提交的镜像运行一个新的容器。
执行Dockerfile中的下一条命令,直到所有指令都执行完毕。
从上面可以看出,如果你的Dockerfile由于某些原因(如某条指令失败了)没有正常结束,那你也可以得到一个可以使用的镜像。这对调试非常有帮助:可以基于该镜像运行一个具备交互功能的容器,使用最后创建的镜像对为什么你的指令会失败进行调试。

Dockerfile也支持注释。以#开头的行都会被认为是注释,# Version: 0.0.1这就是个注释

  • FROM:
    每个Dockerfile的第一条指令都应该是FROM。FROM指令指定一个已经存在的镜像,后续指令都是将基于该镜像进行,这个镜像被称为基础镜像(base iamge)。在这里ubuntu:latest就是作为新镜像的基础镜像。也就是说Dockerfile构建的新镜像将以ubuntu:latest操作系统为基础。在运行一个容器时,必须要指明是基于哪个基础镜像在进行构建。

  • MAINTAINER:
    MAINTAINER指令,这条指令会告诉Docker该镜像的作者是谁,以及作者的邮箱地址。这有助于表示镜像的所有者和联系方式

  • RUN:
    在这些命令之后,我们指定了三条RUN指令。RUN指令会在当前镜像中运行指定的命令。这里我们通过RUN指令更新了APT仓库,安装nginx包,并创建了一个index.html文件。像前面说的那样,每条RUN指令都会创建一个新的镜像层,如果该指令执行成功,就会将此镜像层提交,之后继续执行Dockerfile中的下一个指令。
    默认情况下,RUN指令会在shell里使用命令包装器/bin/sh -c 来执行。如果是在一个不支持shell的平台上运行或者不希望在shell中运行(比如避免shell字符串篡改),也可以使用exec格式的RUN指令,通过一个数组的方式指定要运行的命令和传递给该命令的每个参数:

RUN ["apt-get", "install", "-y", "nginx"]
  • EXPOSE指令
    是告诉Docker该容器内的应用程序将会使用容器的指定端口。这并不意味着可以自动访问任意容器运行中服务的端口。出于安全的原因,Docker并不会自动打开该端口,而是需要你在使用docker run运行容器时来指定需要打开哪些端口。
    可以指定多个EXPOSE指令来向外部公开多个端口,Docker也使用EXPOSE指令来帮助将多个容器链接,在后面的学习过程中我们会接触到。
基于Dockerfile构建新镜像

执行docker build命令时,Dockerfile中的所有指令都会被执行并且提交,并且在该命令成功结束后返回一个新镜像。

# cd static_web
# docker build -t="test/static_web" .

也可以在构建镜像的过程当中为镜像设置一个标签:

# docker build -t="test/static_web:v1" .

上面命令中最后的“.”告诉Docker到当前目录中去找Dockerfile文件。也可以指定一个Git仓库地址来指定Dockerfile的位置,这里Docker假设在Git仓库的根目录下存在Dockerfile文件:

# docker build -t="test/static_web:v1" git@github.com:test/static_web
  • 构建缓存:
    在上面执行构建镜像的过程中,我们发现当执行apt-get update时,返回Using cache。Docker会将之前的镜像层看做缓存,因为在安装nginx前并没有做其他的修改,因此Docker会将之前构建时创建的镜像当做缓存并作为新的开始点。然后,有些时候需要确保构建过程不会使用缓存。可以使用docker build 的 --no-cache标志。
docker build --no-cache -t="test/static_web" .
  • 查看新镜像:
    现在来看一下新构建的镜像,使用docker image命令,如果想深入探求镜像如何构建出来的,可以使用docker history命令看到新构建的test/static_web镜像的每一层,以及创建这些层的Dockerfile指令。
# docker images test/static_web
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
test/static_web     latest              94728651ce15        20 hours ago        212.1 MB
 
# docker history 94728651ce15
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
94728651ce15        20 hours ago        /bin/sh -c #(nop) EXPOSE 80/tcp                 0 B                
09e999b131f4        20 hours ago        /bin/sh -c echo 'Hi, I am in your container'    27 B               
4af2ef04fb91        20 hours ago        /bin/sh -c apt-get install -y nginx             56.52 MB           
e98d2c152d1d        20 hours ago        /bin/sh -c apt-get update                       38.29 MB   
从新镜像启动容器

下面基于新构建的镜像启动一个新容器,来检查之前的构建工作是否一切正常:

# docker run -d -p 80 --name static_web test/static_web nginx -g "daemon off;"
  • -d选项,告诉Docker以分离(detached)的方式在后台运行。这种方式非常适合运行类似Nginx守护进程这样的需要长时间运行的进程。
  • 这里也指定了需要在容器中运行的命令:nginx -g "daemon off;"。这将以前台运行的方式启动Nginx,来作为我们的Web服务器。
  • -p选项,控制Docker在运行时应该公开哪些网络端口给外部(宿主机)。运行一个容器时,Docker可通过两种方法在宿主机上分配端口。
    Docker可以在宿主机上通过/proc/sys/net/ipv4/ip_local_port_range文件随机一个端口映射到容器的80端口。
    -可以在Docker宿主机中指定一个具体的端口号来映射到容器的80端口上。

这将在Docker宿主机上随机打开一个端口,这个端口会连接到容器中的80端口上。可以使用docker ps命令查看容得的端口分配情况:

# docker ps -l
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                   NAMES
0b422bbcce10        test/static_web     "nginx -g 'daemon of   5 seconds ago       Up 5 seconds        0.0.0.0:32772->80/tcp   static_web
运行自己的Docker Registry

前面我们已经介绍了Docker有公共的Docker Registry就是Docker Hub。但是有时我们可能希望构建和存储包含不想被公开的信息或数据的镜像。这时候我们有以下两种选择:

  • 利用Docker Hub上的私有仓库;
  • 在防火墙后面运行自己的Registry。
    从Docker容器安装一个Registry非常简单
## 拉去registry镜像
# docker pull registry
 
## 搭建本地镜像源
# docker run -d -v /opt/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest
 
## 查看容器状态
# docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                     PORTS                    NAMES
f570fab5d67d        registry:latest     "/entrypoint.sh /etc   3 seconds ago       Up 3 seconds               0.0.0.0:5000->5000/tcp   registry

接下来将我们的镜像上传到本地的Docker Registry

## 找到我们要上传的镜像
# docker images test/apache2
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
test/apache2       latest              9c30616364f4        7 days ago          254.4 MB
 
## 使用新的Registry给该镜像打上标签
# docker tag 9c30616364f4 docker.example.com:5000/test/apache2
 
## 通过docker push 命令将它推送到新的Registry中去
# docker push docker.example.com:5000/test/apache2
The push refers to a repository [docker.example.com:5000/test/apache2] (len: 1)
9c30616364f4: Image already exists
f5bb94a8fac4: Image successfully pushed
2e36b30057ab: Image successfully pushed
0346cecb4e51: Image successfully pushed
274da7f89b05: Image successfully pushed
b5ce920a148c: Image successfully pushed
576b12d1aa01: Image successfully pushed
Digest: sha256:0c22a559f8dea881bca046e0ca27a01f73aa5f3c153b08b8bdf3306082e48b72
 
## 测试我们上传的镜像
# docker run -it docker.example.com:5000/test/apache2 /bin/bash
root@5088a0fd20e8:/#

基本概念

  • 镜像是一种轻量级、可执行的独立软件包,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件。
  • 容器是镜像的运行时实例 - 实际执行时镜像会在内存中变成什么。默认情况下,它完全独立于主机环境运行,仅在配置为访问主机文件和端口的情况下才执行此操作。

容器在主机内核上以本机方式运行应用。与仅通过管理程序对主机资源进行虚拟访问的虚拟机相比,它们具有更好的性能特征。容器可以获取本机访问,每个容器都在独立进程中运行,占用的内存不超过任何其他可执行文件。

虚拟机(virtual machine)

就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。
虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点。
(1)资源占用多
虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。
(2)冗余步骤多
虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。
(3)启动慢
启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。

Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

由于容器是进程级别的,相比虚拟机有很多优势。

(1)启动快
容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。

(2)资源占用少
容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。

(3)体积小
容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。

总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容