ROS 2 基本命令总结

引言

截止到 2019年5月,ROS 2 已经正式发布了 4 个版本(以及更早期的几个 alpha, beta 测试版本)。ROS 2 版本的命名方式依然延续了 ROS 的规则(实际上也是 Ubuntu 版本的命名规则),按照字母顺序依次命令,而且都是 ×... ×... 格式的:

  • Ardent Apalone
  • Bouncy Bolson
  • Crystal Clemmys
  • Dashing Diademata

我们在学习和做项目过程中,逐渐感觉到 ROS 2 趋于成熟,参考的很多项目已经改用 ROS 2 实现。就像 python 2 向 python 3 的转换,随着开发者贡献的 package 越来越多,新的平台越来越成熟,功能逐渐完善,最后彻底的转换就成了大势所趋。

ROS 2 不是 ROS 的简单扩展,而是全新的架构,如下所示:

architecture.png

关于 ROS 2 新框架有如下描述 (来源):

For ROS 2 the decision has been made to build it on top of an existing middleware solution (namely DDS). The major advantage of this approach is that ROS 2 can leverage an existing and well developed implementation of that standard.

ROS 2 几乎所有的优点 (无 master节点、实时性好、安全性高等) 都得益于站在了 DDS (Data Distribution Service) 这个巨人的肩膀上。

DDS 只是一个协议标准,它有多种具体的实现 (implementation),例如:

在 ROS 2 的安装过程中可以设定使用哪个 DDS 实现。一般情况下,使用默认的 Fast-RTPS 即可。

如果只是将 ROS 2 作为工具来用,在 ROS 2 client library 的基础上搭建自己的项目,可以暂时忽略掉底层的 DDS 等新机制,掌握 ROS 2 的常用命令应该就够了。回想一下,在之前使用 ROS 的时候,我们也并不需要了解底层的 TCPROS 和 UDPROS 通讯协议。

相比于 ROS 整洁条理的 wiki,ROS 2 的文档稍显粗糙。

本文的目的是尝试整理总结 ROS 2 的基本操作命令,方便以后使用时查阅。

如果用 Debian package 方式安装,即 sudo apt install 方式, Ubuntu 16.04 只能安装 Ardent 版本,Ubuntu 18.04 可以安装 Bouncy,Crystal 和 Dashing。
如果用源码编译的方式安装,Ubuntu 16.04 可以安装 Ardent,Bouncy 和 Crystal 。

我们选择的平台:

  • Ubuntu 18.04
  • ROS 2 (Dashing 版本)

这两个都是 LTS (Long Term Support) 版本,在未来较长的一段时间内应该是主流选择。

ROS 2 安装

ROS 2 官网上有详细的安装步骤。为了方便查阅,抄录到这里。

  1. 设置环境变量 LC_ALL 和 LANG,这两个变量描述了用户所在地区、使用的语言、日期、货币格式等。网站上并没有说为什么必须设置这两个变量。猜测可能是 ROS 2 中的某些显示结果 (例如报错信息、日期、货币格式等) 会受到这两个环境变量的影响。这里按照网站上的例子设置就可以了。
    sudo locale-gen en_US en_US.UTF-8
    
    sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
    
    export LANG=en_US.UTF-8
    
  2. 添加 ROS 2 的 repo 以及对应的 key
    sudo apt update
    
    sudo apt install curl gnupg2 lsb-release
    
    curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
    
    sudo sh -c 'echo "deb [arch=amd64,arm64] http://packages.ros.org/ros2/ubuntu `lsb_release -cs` main" > /etc/apt/sources.list.d/ros2-latest.list'
    
  3. 安装 ROS 2
    sudo apt update
    
    sudo apt install ros-dashing-desktop
    
  4. 添加命令自动补全功能,即在命令行中用 TAB 键可以自动补全 ROS 2 的命令。
    sudo apt install python3-argcomplete
    
  5. 添加环境变量。这样每次打开 Terminal 就可以自动加载 ROS 2 相关的环境变量,进而使用 ROS 2 相关的命令
    echo "source /opt/ros/dashing/setup.bash" >> ~/.bashrc
    
  6. 如果涉及到 ROS 节点与 ROS 2 节点通讯,还要安装 ros1_bridge
    sudo apt install ros-dashing-ros1-bridge
    

colcon 编译工具

colcon 的设计理念可以在这里查到。概括来说,colcon 的目标就是成为一个通用的编译工具,现在主要用来编译 ROS,ROS 2 以及 Gazebo,未来可能使用更广泛。所以尽管 colcon 最早的开发动力来自 ROS 2,但它的定位并不是 ROS 2 的附属品。 colcon 有非常详细的文档,可以在这里查阅。

在 ROS 2 Ardent 版本中编译工具是 ament_tools,从 ROS 2 Bouncy 版本开始,colcon 就成了默认的编译工具。

安装 colcon

colcon 包含在了 ROS 2 的 repo 中,前边已经添加过了这个 repo,所以这里直接安装即可:

sudo apt install python3-colcon-common-extensions

由于 colcon 本质上独立于 ROS 2,我们并不一定先添加 ROS 2 的 repo 再安装 colcon,也可以直接用 pip 方式安装,要求 python 3.5 及以上版本

sudo pip3 install -U colcon-common-extensions

创建工作空间

跟 ROS 相同,ROS 2 也是建议创建一个工作空间 workspace,方便管理同一个项目的 packages,而且也是将 package 源文件都放在 src 文件夹中。这里我们用 ROS 2 tutorial 中的例子,创建工作空间 ros2_example_ws 并进入 src

mkdir -p ~/ros2_example_ws/src   

cd ~/ros2_example_ws/src

现在我们先关注 colcon 的编译过程,所以 package 源文件就先借用官网的。

git clone https://github.com/ros2/examples 

git checkout $ROS_DISTRO     # 切换到与本机版本对应的 branch 上

目前文件路径结构如下:

$ tree -L 3
.
└── src
    └── examples
        ├── CONTRIBUTING.md
        ├── LICENSE
        ├── rclcpp
        ├── rclpy
        └── README.md

4 directories, 3 files

可以用 colcon list 命令列出 src 文件夹中的所有 packages,即所有包含 package.xmlsetup.py/CMakeLists.txt 的文件夹。

$ colcon list 

examples_rclcpp_minimal_action_client   /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_action_client (ros.ament_cmake)
examples_rclcpp_minimal_action_server   /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_action_server (ros.ament_cmake)
examples_rclcpp_minimal_client  /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_client    (ros.ament_cmake)
examples_rclcpp_minimal_composition /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_composition   (ros.ament_cmake)
examples_rclcpp_minimal_publisher   /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_publisher (ros.ament_cmake)
examples_rclcpp_minimal_service /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_service   (ros.ament_cmake)
examples_rclcpp_minimal_subscriber  /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_subscriber    (ros.ament_cmake)
examples_rclcpp_minimal_timer   /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_timer (ros.ament_cmake)
examples_rclpy_executors    /home/automan/ros2_example_ws/src/examples/rclpy/executors  (ros.ament_python)
examples_rclpy_minimal_action_client    /home/automan/ros2_example_ws/src/examples/rclpy/actions/minimal_action_client  (ros.ament_python)
examples_rclpy_minimal_action_server    /home/automan/ros2_example_ws/src/examples/rclpy/actions/minimal_action_server  (ros.ament_python)
examples_rclpy_minimal_client   /home/automan/ros2_example_ws/src/examples/rclpy/services/minimal_client    (ros.ament_python)
examples_rclpy_minimal_publisher    /home/automan/ros2_example_ws/src/examples/rclpy/topics/minimal_publisher   (ros.ament_python)
examples_rclpy_minimal_service  /home/automan/ros2_example_ws/src/examples/rclpy/services/minimal_service   (ros.ament_python)
examples_rclpy_minimal_subscriber   /home/automan/ros2_example_ws/src/examples/rclpy/topics/minimal_subscriber  (ros.ament_python)

用 colcon 编译下载的 package

colcon build --symlink-install

上边命令中 --symlink-install 表示编译时如果 install 中文件已经存在于 src 或者 build 文件夹中,就用超链接指向该文件,避免浪费空间,也可以实现同步更新。
例如,在 install 文件夹的 examples_rclcpp_minimal_publisher 中有如下超链接:

.
├── lib
│   └── examples_rclcpp_minimal_publisher
│       ├── publisher_lambda -> /home/automan/ros2_example_ws/build/examples_rclcpp_minimal_publisher/publisher_lambda
│       ├── publisher_member_function -> /home/automan/ros2_example_ws/build/examples_rclcpp_minimal_publisher/publisher_member_function
│       └── publisher_not_composable -> /home/automan/ros2_example_ws/build/examples_rclcpp_minimal_publisher/publisher_not_composable
└── share
    ├── ament_index
    │   └── resource_index
    ├── colcon-core
    │   └── packages
    └── examples_rclcpp_minimal_publisher
        ├── cmake
        ├── environment
        ├── hook
        ├── local_setup.bash -> /home/automan/ros2_example_ws/build/examples_rclcpp_minimal_publisher/ament_cmake_environment_hooks/local_setup.bash
        ├── local_setup.sh -> /home/automan/ros2_example_ws/build/examples_rclcpp_minimal_publisher/ament_cmake_environment_hooks/local_setup.sh
        ├── local_setup.zsh -> /home/automan/ros2_example_ws/build/examples_rclcpp_minimal_publisher/ament_cmake_environment_hooks/local_setup.zsh
        ├── package.bash
        ├── package.ps1
        ├── package.sh
        ├── package.xml -> /home/automan/ros2_example_ws/src/examples/rclcpp/minimal_publisher/package.xml
        └── package.zsh

如果去掉 --symlink-install 参数,仅用命令 colcon build 来编译,则上述超链接文件全都变成实体拷贝的文件,得到如下结果:

.
├── lib
│   └── examples_rclcpp_minimal_publisher
│       ├── publisher_lambda
│       ├── publisher_member_function
│       └── publisher_not_composable
└── share
    ├── ament_index
    │   └── resource_index
    ├── colcon-core
    │   └── packages
    └── examples_rclcpp_minimal_publisher
        ├── cmake
        ├── environment
        ├── hook
        ├── local_setup.bash
        ├── local_setup.sh
        ├── local_setup.zsh
        ├── package.bash
        ├── package.ps1
        ├── package.sh
        ├── package.xml
        └── package.zsh

编译之后,得到的文档结构如下:

.
├── build
├── install
├── log
└── src

即 colcon 编译产生了 build, install, log 三个新文件夹。

编译之后还可以测试一下 packages

$ colcon test-result --all

build/examples_rclpy_executors/pytest.xml: 3 tests, 0 errors, 0 failures, 0 skipped
build/examples_rclpy_minimal_action_client/pytest.xml: 0 tests, 0 errors, 0 failures, 0 skipped
build/examples_rclpy_minimal_action_server/pytest.xml: 0 tests, 0 errors, 0 failures, 0 skipped
build/examples_rclpy_minimal_client/pytest.xml: 3 tests, 0 errors, 0 failures, 0 skipped
build/examples_rclpy_minimal_publisher/pytest.xml: 3 tests, 0 errors, 0 failures, 0 skipped
build/examples_rclpy_minimal_service/pytest.xml: 3 tests, 0 errors, 0 failures, 0 skipped
build/examples_rclpy_minimal_subscriber/pytest.xml: 3 tests, 0 errors, 0 failures, 0 skipped

Summary: 15 tests, 0 errors, 0 failures, 0 skipped

如果要单独编译某一个 package,可以用如下命令:

colcon build --packages-select  PACKAGE_NAME

如果不希望编译某一个 package,可以在该 package中创建名为 COLCON_IGNORE 的空文件,colcon 就会忽略掉该 package,不但不编译,连 colcon list 都不显示,这个 package 对 colcon 就是透明的。

完成编译之后,要 source 一下 setup.bash 文件,确保系统能够找到当前编译生成的可执行文件和库

source install/setup.bash

或者将 source 命令放入 .bashrc 文件,这样每次打开 terminal 就可以自动加载路径信息

echo "source ~/ros2_example_ws/install/setup.bash" >> ~/.bashrc

实例测试:
首先启动一个 publisher

ros2 run examples_rclcpp_minimal_publisher publisher_member_function

再启动一个 subscriber

 ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function 

如果一切顺利,应该会有如下的界面:

sub_pub.png

这里与 ROS 的最大区别是不需要启动 ROS master 节点,即不需要类似 roscore 的命令。ROS 2 是真正的分布式系统,不需要中心节点,这样系统的鲁棒性更强,不会因为中心节点失效而影响整个系统。

terminal 命令

分门别类

ROS 2 根据命令的作用对象划分成多个类别,其中常用的几个类别:

Commands:
  launch     Run a launch file
  node       Various node related sub-commands
  param      Various param related sub-commands
  pkg        Various package related sub-commands
  run        Run a package specific executable
  service    Various service related sub-commands
  srv        Various srv related sub-commands
  topic      Various topic related sub-commands

在调用时都是采用如下命令格式:

ros2 COMMAND ...

我们可以对比 ROS 和 ROS 2 中的几个命令,应该很容易找到其中的规律

  • 运行 ROS node
    - ROS: rosrun <package> <executive>
    - ROS 2:ros2 run <package> <executive>
  • 查看当前运行的 node
    - ROS : rosnode list
    - ROS 2: ros2 node list

修改 node 名字

在前边的例子中,默认 node 名字分别为 /minimal_publisher/minimal_subscriber。我们可以在启动 node 时用 __node:=NEW_NAME 命令修改他们的名字,例如修改 /minimal_publisher/my_publisher :

ros2 run examples_rclcpp_minimal_publisher publisher_member_function __node:=my_publisher

还可以通过 __ns:=NEW_NAMESPACE 修改 node 的 namespace。有些 node 的名字形式为 /a/b/c/d,其中 d 称为 basename,前边的 /a/b/c 被称为 namespace。
例如,我们给刚才的 node /my_publisher 加个 namespace /mynode,即最后 node 的名字为 /mynode/my_publisher:

 ros2 run examples_rclcpp_minimal_publisher publisher_member_function __ns:=/mynode __node:=my_publisher

修改 topic, service 的名字

如果要修改 node 文件中的某个 topic 或者 service 的名字,则用 OLD_NAME:=NEW_NAME 的形式,例如原本 /minimal_publisher 中的 topic 名字为 topic ,这里修改成 my_topic:

ros2 run examples_rclcpp_minimal_publisher publisher_member_function topic:=my_topic

编写自己的 node 文件 (基于 rclpy)

在 ROS 2 的 wiki 和 github 中提供了一些例子,这里整理总结一下,方便以后查阅。
参考文献:
https://github.com/ros2/examples
https://github.com/ros2/demos/tree/master/demo_nodes_py

Talker 与 Listener

publish 与 subscribe 是 ROS / ROS 2 中最基本的场景。下边是一个简单的 publish 的例子。定义了一个 node 名为 talker,以 0.5s 每次的频率向 chatter 这个 topic 上发布消息,同时也在屏幕上显示消息内容:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class Talker(Node):

    def __init__(self):
        super().__init__('talker')   # 继承 Node class 的初始化函数,生成 node 名为 talker
        self.pub = self.create_publisher(String, 'chatter', 10)
        timer_period = 0.5  
        self.timer = self.create_timer(timer_period, self.timer_callback)  # 设定计时器,到时间就调用 callback 函数
        self.i = 1

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello World: %d' % self.i
        self.pub.publish(msg)    # 向 topic 上发布数据
        self.get_logger().info('Publishing: "%s"' % msg.data)   # 显示 log 信息
        self.i += 1


def main(args=None):   # 这里的 args 是从命令行中接收的参数
    rclpy.init(args=args)     # 在初始化时不再设定 node 的名字
    talker = Talker()
    rclpy.spin(talker)


if __name__ == '__main__':
    main()

与之对应的,下边是 listener node 的内容:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class Listener(Node):

    def __init__(self):
        super().__init__('listener')
        self.subscription = self.create_subscription(String, 'chatter', self.listener_callback, 10)

    def listener_callback(self, msg):
        self.get_logger().info('I heard: "%s"' % msg.data)


def main(args=None):
    rclpy.init(args=args)
    listener = Listener()
    rclpy.spin(listener)


if __name__ == '__main__':
    main()

有了两个 python 文件,下边就是设置 ROS 2 package。

设置 ROS 2 package

假设现在的 ROS workspace 路径是 ~/ros2_example_ws/src。在其中新建 ROS 2 package 文件夹 pub_sub_pkg,内部文档目录结构如下:

.
├── package.xml
├── pub_sub_examples
│   ├── __init__.py
│   ├── listener.py
│   └── talker.py
└── setup.py

2 directories, 6 files

其中

  • __init__.py 是个空文件,主要作用是表明当前文件夹是 python package

  • package.xml 文件内容如下:

    <?xml version="1.0"?>
    <?xml-model href="http://download.ros.org/schema/package_format2.xsd"   schematypens="http://www.w3.org/2001/XMLSchema"?>
    <package format="2">
    <name>pub_sub_examples</name>
    <version>0.0.1</version>
    <description>Examples of publisher and subscriber using rclpy.</description>
    
    <maintainer email="myemail@address">myname</maintainer>
    <license>Apache License 2.0</license>
    
    <exec_depend>rclpy</exec_depend>
    <exec_depend>std_msgs</exec_depend>
    
    <export>
    <build_type>ament_python</build_type>
    </export>
    </package>
    

    其中关键是指定 node 文件中依赖的 package,我们的 talker 和 listener 文件都比较简单,只用到了 rclpystd_msgs 两个 python package.

  • setup.py 内容如下:

    from setuptools import setup
    
    package_name = 'pub_sub_examples'
    
    setup(
      name=package_name,
      version='0.0.1',
      packages=[package_name],
      install_requires=['setuptools'],    
      zip_safe=True,
      entry_points={
          'console_scripts': [
              'my_talker = pub_sub_examples.talker:main',
              'my_listener = pub_sub_examples.listener:main'
          ]
      }
    )
    

    其中的关键是指定 console_scripts 命令和对应的函数。colcon 编译之后可以直接在命令行中执行 my_talkermy_listener 实现 pub 和 sub 的功能。关于 python package 中 setup.py 的编写,可以参考我们之前的文章

使用 colcon 编译

设定好了上述文件之后,在 workspace ~/ros2_example_ws/ 目录下编译:

colcon build --symlink-install

如果一切顺利,会在 ~/ros2_example_ws/install/pub_sub_examples/bin 中生成可执行文件 my_talkermy_listener。此时还要再 source 一下 install 文件夹中的 setup.bash,更新 ROS 2 package 路径信息,如果之前已经将 source setup.bash 加入 ~/.bashrc 文件,则 source ~/.bashrc 即可。source 之后,~/ros2_example_ws/install/pub_sub_examples/bin 就被加入了 PATH 环境变量中,在命令行中可以直接执行程序,效果如下:

pub_sub.png

由于我们编译时用了 --symlink-install 参数,所以对 src 中 python 源文件 talker.pylistener.py 的修改可以马上反映到 install 中的可执行文件中。打开 install 中的 talker 文件可以看到如下内容:

#!/usr/bin/python3
# EASY-INSTALL-ENTRY-SCRIPT: 'pub-sub-examples','console_scripts','my_talker'
__requires__ = 'pub-sub-examples'
import re
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(
        load_entry_point('pub-sub-examples', 'console_scripts', 'my_talker')()
    )

实际上就是执行了我们之前在 setup.py 文件中设定 console_scripts 中的内容。

通过 ros2 run 方式执行

上述编译方式得到的 node 不能用标准的 ros2 run <pkg> <node> 方式执行,ROS 2 默认要调用 install/<pkg>/<lib> 中的文件。如果要用标准方式启动 node 文件,需要做一些额外设置。在 setup.py 同一目录中添加如下 setup.cfg 文件:

[develop]
script-dir=$base/lib/pub_sub_examples
[install]
install-scripts=$base/lib/pub_sub_examples

这里是将编译输出的目标文件夹设置为 install/pub_sub_examples/lib/pub_sub_examples 。再次用 colcon 编译,然后可以看到对应文件夹下有 my_talkermy_listener 两个文件。重新 source 一下 ~/.bashrc 文件,就可以用 ros2 run pub_sub_examples my_talker/my_listener 命令启用两个 node 了:

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

推荐阅读更多精彩内容