VSCode云端开发环境搭建 (Remote-Containers)

为什么要使用云端开发环境

2019年5月份,微软发布一组VSCode插件“Remote-Development”。它可以让开发者在VSCode中直接访问远程的目录进行开发工作。这样我们的代码和开发环境就可以和终端电脑分离了 ,并且可以随意在远端搭建多个不同的开发环境随时切换。 听起来是不是有点小激动呢?

其实远程开发的模式并不新鲜。很久以前我就通过FTP或SFTP链接,直接在服务器上进行开发。但这种方式成本比较高,需要一台远程服务器支持,而且多人同时使用的时候可能产生版本依赖的冲突。这几年容器技术和应用场景犹如获得神速力的加持般飞速发展。结合容器技术可以有效的将不同的开发环境进行区隔,并且以容器为单位,进行复制、迁移变得前所未有的简单。

所以使用云端开发环境有以下几个优点:

  • 有多语言、多环境的开发需求时,可以避免对本机环境的污染
  • 方便迁移、复制,甚至可以在小组内对同样的环境需求进行打包、分发
  • 在局域网内搭建开发服务器可以节省笔记本的负载,更加有效的提升笔记本的使用效率

先介绍一下 Remote-Developement 插件组

微软发布了3个远程开发插件,分别是 “Remote-SSH”、“Remote-Containers”、“Remote-WSL”,并将它们放入了插件包 “Remote-Developement” 中一同发布。

  • Remote-SSH:通过ssh,连接远程服务器。(平平无奇)
  • Remote-Containers:连接Docker容器。(非常惊艳)
  • Remote-WSL:连接“Windows Subsystem for Linux”(就是在Win10中安装的Linux)。

今天我们着重介绍如何使用“Remote-Containers”,开始吧。

准备Docker环境

环境说明

我的桌面系统是MacOS,和Windows的差异,小伙伴们可以自行脑补。

在安装Docker的时候,我们并不需要安装官网提供的标准安装包,因为那包括了Docker EngineDocker Client

所以我们需要安装的是docker-toolbox。MacOS可以通过brew search docker-toolbox找到,其他系统可以通过github下载 https://github.com/docker/toolbox/releases

$ brew cask install docker-toolbox     

docker-toolbox包含以下几部分内容

  • docker-cli : 客户端命令行,目前的版本是19.03.1
  • docker-machine : 可以在本机启动用于Docker Engine虚拟机并管理他们
  • docker-compose : docker提供的编排工具,支持compose文件,这个并不常用。
  • Kitematic : Docker的客户端GUI,官方已经废弃了。
  • Boot2Docker ISO : 用于创建Docker Engine虚拟机的镜像。由于包中的这个版本并不是最新的,所以创建虚拟机的时候可能会需要重新下载。
  • VirtualBox : 虚拟机

创建Docker Machine

$ docker-machine create --driver virtualbox \
    --virtualbox-cpu-count 2 \
    --virtualbox-memory 2048 \
    default
Running pre-create checks...
Creating machine...
(default) Copying ${HOME}/.docker/machine/cache/boot2docker.iso to ${HOME}/.docker/machine/machines/default/boot2docker.iso...
(default) Creating VirtualBox VM...
(default) Creating SSH key...
(default) Starting the VM...
(default) Check network to re-create if needed...
(default) Waiting for an IP...
Waiting for machine to be running, this may take a few minutes...
Detecting operating system of created instance...
Waiting for SSH to be available...
Detecting the provisioner...
Provisioning with boot2docker...
Copying certs to the local machine directory...
Copying certs to the remote machine...
Setting Docker configuration on the remote daemon...
Checking connection to Docker...
Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env default

创建成功后,连接到Machine

$ eval $(docker-machine env default)
$ docker version

Client: Docker Engine - Community
 Version:           19.03.1
 API version:       1.40
 Go version:        go1.12.5
 Git commit:        74b1e89
 Built:             Thu Jul 25 21:18:17 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.5
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.12
  Git commit:       633a0ea838
  Built:            Wed Nov 13 07:28:45 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.2.10
  GitCommit:        b34a5c8af56e510852c35414db4c1f4fa6172339
 runc:
  Version:          1.0.0-rc8+dev
  GitCommit:        3e425f80a8c931f88e6d94a8c831b9d5aa481657
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
$
$ # OK,连接成功!

从官方Sample开始

先取得官方的Sample项目。在github上查找 vscode-remote-try 我们可以找到一堆项目,都是微软官方提供的不同语言环境的Sample。这里我们用 vscode-remote-try-python 作为例子。

$ git clone https://github.com/microsoft/vscode-remote-try-python.git
$ cd vscode-remote-try-python/
$ 
$ # 打开项目目录
$ /Applications/Visual\ Studio\ Code.app/Contents/MacOS/Electron ./
$ # windows 使用命令 "code .\"

Sample的目录结构

[workspace]
|- .devcontainer :              开发环境配置目录
|  |- devcontainer.json :       环境配置文件
|  |- Dockerfile :              环境的Docker镜像生成文件
|- .vscode :                    vscode使用的配置文件(容器端使用)
|  |- launch.json :             debuger 配置文件(容器端使用)
|- static :                     Sample项目的静态页面目录
|  |- index.html :              Sample项目首页
|- .gitattributes :             git 文件属性定义
|- .gitignore :                 git 忽略文件
|- app.py :                     flask项目入口
|- LICENSE
|- README.md
|- requirements.txt :           项目的环境所需要的python模块,通过pip安装

下面我们着重介绍 devcontainer.jsonDockerfile两个文件

.devcontainer/devcontainer.json

这个文件是用于启动开发容器的配置。点击查看官方文档。下面我们介绍一下配置属性。

属性 类型 描述
通用参数
name 字符串 容器显示名称
extensions 数组 需要安装到容器中的vscode扩展。 缺省值"[]"
settings json对象 添加到容器中的vscode settings.json
postCreateCommand 字符串,数组 容器创建后第一次启动时执行的一组命令。命令执行目录是容器中workspaceFolder指定的目录。多条命令之间使用&&进行连接。 缺省值 none
devPort 整数 允许给vscode server指定一个端口。缺省为一个随机可用端口。
Dockerfile或Image
image 字符串 必填 使用已存在镜像时必填。 vscode会使用镜像名称来创建开发容器。
dockerFile 字符串 必填 使用Dockerfile时必填。 指定一个用来生成Docker镜像的Dockerfile文件。路径相对于devcontainer.json文件。 可以在这个地址找到各种Dockerfile样例。
context 字符串 指定运行docker build命令时的上下文目录。 路径是基于devcontainer.json文件的相对路径。 缺省值"."
appPort 整数,字符串,数组 容器运行时发布到Host的端口。多个端口用数组表示。 缺省值"[]"
workspaceMount 字符串 覆盖缺省的mount参数。语法参见Docker文档Docker CLI --mount flag。 可以使用${localWorkspaceFolder}引用本地的工作区目录,或使用${env:VARNAMEHERE}应用环境变量
workspaceFolder 字符串 设置vscode连接到容器后缺省的工作目录。 通常结合workspaceMount属性使用。
runArgs 数组 运行容器时的命令行参数Docker CLI arguments。 缺省值"[]"。 可以使用${localWorkspaceFolder}引用本地的工作区目录,或使用${env:VARNAMEHERE}应用环境变量
overrideCommand 布尔 告诉容器在启动时是否执行命令 /bin/sh -c "while sleep 1000; do :; done",用以覆盖缺省的启动执行命令。 缺省值"true"
shutdownAction 枚举: none,stopContainer 指定在vscode断开连接或者关闭时,是否停止容器。 缺省值"stopContainer"
Docker Compose
dockerComposeFile 字符串,数组 必填 指定一个Docker Compose文件,路径相对于devcontainer.json文件。 当需要扩展Docker Compose配置时,可以使用数组。数组的顺序和重要,后面的文件内容会覆盖之前的设置。 缺省的.env文件会在项目的根路径下寻找,但可以通过Docker Compose文件中的env_file指定另外的路径。
service 字符串 必填 指定启动后vscode连接哪个service。
runServices 数组 指定Docker Compose文件中的哪些services需要启动。同时在断开连接后,这些services将会根据shutdownAction的设置决定是否关闭。 缺省值为所有的services。
workspaceFolder 字符串 连接到容器后进入的工作目录。缺省值"/"
shutdownAction 枚举: none,stopCompose 指定在vscode断开连接或者关闭时,是否停止容器。 缺省值"stopCompose"

那么我们看看Sample中的.devcontainer文件内容。(为了方便显示,我过滤的原文件中的注释)

{
    "name": "Python Sample",
    "dockerFile": "Dockerfile",

    "appPort": [9000],

    "runArgs": ["-u", "vscode"],

    "settings": { 
        "terminal.integrated.shell.linux": "/bin/bash",
        "python.pythonPath": "/usr/local/bin/python",
        "python.linting.pylintEnabled": true,
        "python.linting.pylintPath": "/usr/local/bin/pylint",
        "python.linting.enabled": true
    },

    "postCreateCommand": "sudo pip install -r requirements.txt",

    "extensions": [
        "ms-python.python"
    ]
}

根据这个配置,我们可以知道。

  • "dockerFile": 开发容器根据Dockerfile创建。Dockerfile的路径是.devcontainer/Dockerfile
  • "appPort": 容器启动时publish9000端口到Host。这里的Host就是我们创建的Docker Machine。
  • "runArgs": 容器启动时,使用vscode用户进行登录。(vscode用户在Dockerfile中创建)
  • "settings": vscode连接到容器后,会应用如下配置:
    • 终端使用bash
    • 指定python命令的路径
    • 启用pylint
  • "postCreateCommand": 容器启动时安装requirements.txt文件中的python modules
  • 在容器中安装vscode扩展,"ms-python.python"

上述内容基本上是在说,容器启动时需要做的事情。
这里需要强调一点: Remote-Containers是通过"镜像"来管理环境的 。容器只是运行时环境,容器是可以随时删除、重建,并同时要保证环境是持续可用的。
所以,容器运行时的配置都放到了devcontainer.josn文件中。

下面我们来看看镜像的生成 - Dockerfile

.devcontainer/Dockfile

关于Dockerfile的格式,参见官方文档
看看Sample中的内容。


# 基于官方的"python:3"镜像
FROM python:3

# 切换到非交互模式避免警告
ENV DEBIAN_FRONTEND=noninteractive

# 指定创建的非root用户
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# 首先更新系统
RUN apt-get update \
    # 安装vscode server需要的基础软件
    && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
    && apt-get -y install git procps lsb-release \
    #
    # 安装 pylint
    && pip --disable-pip-version-check --no-cache-dir install pylint \
    #
    # 创建一个非root用户 (为啥需要这个,请看 - https://aka.ms/vscode-remote/containers/non-root-user)
    && groupadd --gid $USER_GID $USERNAME \
    && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
    # [可选] 添加sudo命令
    && apt-get install -y sudo \
    # 将新创建的非root用户添加到sudoers
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME \
    #
    # 打扫卫生
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*

# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=

依靠上述两个文件,vscode会创建指定的镜像,和容器。并连接容器进入工作区。
但由于在墙内,我们安装的速度会比较慢,所以我们需要随上述文件做些修改。提高下载速度。

更改安装源,提高下载速度

修改Dockerfile

...
# 将debian的更新源改为aliyun的镜像
RUN sed -i -e 's/\w\+\.debian\.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
    # 首先更新系统
    && apt-get update \
...
    # 安装 pylint (使用aliyun镜像)
    && pip --disable-pip-version-check --no-cache-dir install pylint -i https://mirrors.aliyun.com/pypi/simple/ \

...

修改devcontainer.json

{
  "postCreateCommand": "sudo pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/",
}

现在配置都已经准备好了,下面我们需要连接安装好的Docker Engine

现在需要通过vscode连接Docker Engine

vscode连接Docker Engine是通过settings进行配置的。我们当然可以直接修改全局的settings.json但这样会污染全局参数。我建议通过workspace级别的settings进行设置。
我们要创建一个新的workspace
在Sample项目的目录下,新建一个文件,命名为python.code-workspace,内容如下:

{
    "folders":[
        {
            "name":"Python Sample",
            "path":"."
        }
    ],
    "settings": {
        "docker.host": "${DOCKER_HOST}",
        "docker.tlsVerify": "1",
        "docker.certPath": "${DOCKER_CERT_PATH}"
    }
}

执行"docker-machine env default"会输出Docker Machine的连接地址和证书目录。

export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://xxx.xxx.xxx.xxx:2376"
export DOCKER_CERT_PATH="/xxx..."
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell: 
# eval $(docker-machine env default)

使用DOCKER_HOST,DOCKER_CERT_PATH对应的值填入上面的json文件。

然后打开Remote-Containers扩展,选择Reopen in Container

vscode-remote-containers

图解Remote-Containers的部署过程

vscode连接容器的过程中都做了什么工作呢?我们可以分析一下连接过程输出的日志信息。
总结后的内容参见下图。

Remote-Containers的部署过程

1. 连接到Docker Engine:

vscode通过在python.code-workspace文件中的三项配置连接Docker Engine。

  • "docker.host": 服务器地址及端口
  • "docker.tlsVerify": 使用启动TLS验证
  • "docker.certPath": 启动TLS的话,证书存放的路径

由于我们是使用Docker Machine创建的服务器和连接,证书的生成和配置docker-machine命令已经帮我们做好了。

2. 构建镜像

再强调一次 Remote-Containers是通过"镜像"来管理环境的

  • Dockerfile文件通过devcontainer.json中的"Dockerfile"属性指定。
    如果使用Compose文件,通过"dockerComposeFile"属性指定。
  • 镜像名称:
    缺省情况下,vscode会根据环境生成一个镜像名称,格式为vsc-<工作区目录名>-<UUID>
    我们可以通过在devcontainer.json中添加"image"属性,来指定一个具体的镜像名称。
    注1:如果"image"对应的镜像已经存在,可以不指定"Dockerfile"属性。
    注2:如果同时指定了"image""Dockerfile",vscode的语法检查会给出"warn"。不要担心,这个可以忽略。
  • build时的上下文目录:
    缺省为.devcontainer目录。
    可以通过devcontainer.json"context"`属性指定。
3. 创建并启动容器
  • 目录映射
    缺省情况下,vscode会将<本地工作区的目录>映射到容器的"/workspace"目录。
    这里需要强调一点,由于docker run实际上是在服务器端执行的,所以<本地工作区的目录>指的是服务器端的路径。
    docker-machine命令创建的虚拟机会自动将本地的"/Users"目录共享到服务器的"/Users"(MacOS上述"/Users",其他系统可能有所不同)。所以<本地工作区的目录>在本地和服务器上同样有效。

    如果服务器不是由docker-machine创建的,那么可以在devcontainer.json中指定"workspaceMount"属性。
    例如:
    先创建一个卷docker volume create v-sample-python
    然后设置"workspaceMount": "type=volume,source=v-sample-python,destination=/workspace"
    这样容器就挂载了卷,从而避免的本地目录的映射。
    这种操作的问题是,在容器中对文件的修改,不会直接和本地文件同步。我们需要使用git或其他SCM工具来管理代码。

  • 端口映射
    在Sample中"appPort"属性,指定了映射到服务器Host的端口。我们查看一下:

    $ docker ps
    CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
    8588365e7f73        sample-python       "/bin/sh -c 'echo Co…"   8 minutes ago       Up 7 minutes        127.0.0.1:9000->9000/tcp   sample-python     
    

    9000端口虽然打开了,但绑定的是127.0.0.1地址,这根本没啥用嘛(安装Docker Desktop版除外)。
    将属性值改为"appPort": ["0.0.0.0:9000:9000"]即可。

  • 启动后执行命令
    vscode有一个标准的启动后执行命令/bin/sh -c "while sleep 1000; do :; done"。这行命令让容器的主进程始终循环。这样可以保证容器不会自动停止。关于容器如何不停止的问题请看 Docker初学者问题 - 如何让容器启动后不会自动停止
    可以通过设置属性"overrideCommand"=false来禁止vscode使用这一命令。同时在"runArgs"属性中定制自己的启动命令。

  • 容器中的用户
    Dockerfile中创建了一个用户vscode,在容器启动时通过"runArgs"属性的"-u", "vscode"指定连接到容器的缺省用户。为什么要创建非root用户?可以阅读说明 Adding a non-root user to your dev container
    这里我简要的说明一下,很多基础镜像缺省用户是root,当容器中使用root用户创建文件时,文件的owner也是root这有可能导致本地的用户无法访问这些文件。具体根据目录映射的形式不同我们分为3种情况:

    1. vscode连接Docker Desktop安装的Docker Engine。
      这种情况下,缺省docker run会将本地的工作区目录mount到容器中,如果容器使用的是root用户,那么容器中创建的文件也相当于本机root创建的文件。而本机的操作用户(一般来说是非root用户)无法访问。
      这时,就需要根据本机的操作用户的idgroupidDockerfile中创建对应的用户,来解决这一问题。
      本地用户的idgroupid可以通过"id -u","id -g"命令查看。
    2. vscode连接Docker Machine创建的虚拟机
      这种情况下,容器映射的目录是虚拟机中的目录,而虚拟机的目录时通过VirtualBox来共享的本机目录,由于VirtualBox的共享会使用本机的当前操作用户来作为访问共享目录的用户。所以无论容器中使用什么用户,都不会产生问题。
    3. 使用卷或远程服务的目录做mount
      这种情况与本机的目录没有任何关系,所以不会产生问题。
4. 在容器中安装vscode扩展插件

vscode在连接到容器的环境后,会根据不同的容器加载不同的插件。这些插件是安装在容器中的,不会污染本机的插件环境。
安装哪些插件由devcontianer.json中的"extensions"属性指定。
插件会安装到容器中的${HOME}/.vscode-server/extensions目录。

5. 安装 "VS Code Server"

"VS Code Server"是做什么用的?官网有张图可以说明。


Architecture summary

按照我的理解,"VS Code Server"是用来管理容器中的插件并使其可以在本地的vscode中使用。

开发过程

重新构建镜像

由于样例中使用的是pythondebian镜像。个人感觉镜像比较大,安装慢。我比较喜欢Alpine镜像。python本身有3.7.5-alpine3.10镜像,但我更喜欢直接使用alpine:3.10,因为装完python环境后比官方的Python或小那么一点点。

添加alpine.Dockerfile

内容如下

FROM alpine:3.10

ENV DEBIAN_FRONTEND=noninteractive

ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

RUN sed -i -e 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/' /etc/apk/repositories\
    && apk --update add --no-cache \
        libuuid \
        gcc \
        libc-dev \
        linux-headers \
        make  \
        automake   \
        g++  \
        python3-dev \
        sudo \
        bash \
        git \
        curl \
        python3 \
    && curl https://bootstrap.pypa.io/get-pip.py| python3 - \
    && pip --disable-pip-version-check --no-cache-dir install pylint -i https://mirrors.aliyun.com/pypi/simple/ \
    && addgroup -g $USER_GID $USERNAME \
    && adduser -s /bin/bash -u $USER_UID -G $USERNAME -D $USERNAME \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

VOLUME [ "/workspace" ]

ENV DEBIAN_FRONTEND=


### 修改`devcontainer.json`
```json
{
  "dockerFile": "alpine.Dockerfile",
  "settings": { 
    "python.pythonPath": "/usr/bin/python3",
    "python.linting.pylintPath": "/usr/bin/pylint",
  }

启动Sample项目

启动容器后进入flask项目目录。然后启动开发进程。

$ FLASK_ENV=development flask run --host 0.0.0.0 --port 9000
 * Environment: development
 * Debug mode: on
 * Running on http://0.0.0.0:9000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 320-680-031

切换到本地笔记本,查看一下Docker主机的IP,由于我们使用Docker Machine建立的主机,可以通过下面命令查看。

$ docker-machine ls
NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER     ERRORS
default   *        virtualbox   Running   tcp://192.168.99.122:2376           v19.03.5  

192.168.99.122就是Docker主机IP了。在浏览器访问http://192.168.99.122:9000

由于之前在devcontainer.json文件中配置了"appPort": ["0.0.0.0:9000:9000"],所以容器在主机上映射了9000端口。
但这种配置在多人共享一台Docker主机,或者同时调试多个环境并产生端口冲突时会比较麻烦。
那么下面我介绍一下Forward端口

Forward端口

延续上一小节的环境,我们在容器中启动了开发进程,开放的9000端口。现在我们做如下操作

  1. devcontainer.json中删除"appPort"属性
  2. 在命令行中使用docker rm -f sample-python删除容器
  3. 重新在容器中启动项目
  4. 点击左下角状态栏绿色的部分
  5. 在出现命令菜单中选择Remote-Containers: Forward Port from Container...
  6. 在出现的下一步的菜单中选择Forwarding 9000
  7. 在浏览器打开http://localhost:9000地址就可以看到页面了。

停止Forward

  1. 点击左下角状态栏绿色的部分
  2. 在出现命令菜单中选择Remote-Containers: Forward Port from Container...
  3. 在出现的下一步的菜单中选择Stop Forwarding 9000->9000
  4. 就可以取消端口转发

Forward端口的好处是不会占用Docker主机的端口资源

如果两个容器环境同时需要Forward同样的端口怎么办么?
vscode在Forward的时候,如果发现本机端口被占用,则会随机找一个可用端口Fowrard到容器中

程序调试

在sample项目中有一个文件.vscode/launch.josn这个就是调试配置文件。sample中已经有了Flask的配置内容。我们将其中"FLASK_APP"项修改成准确的路径指向app.py。然后启动调试进程。

Remote-Containers的调试过程-step

启动进程后我们在app.pyhello()方法中加入断点。然后在浏览器访问http://127.0.0.1:9000(别忘了Forward端口)。
这是你应该可以看到,vscode停在了断点处。

到此为止VSCode使用Remote-Containers连接Docker容器的过程基本介绍完毕了。

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

推荐阅读更多精彩内容