ROS2入门教程:从环境搭建到DDS通信机制详解
2018年我第一次接触ROS的时候啃了一个月的网盘资源最后连小乌龟都没跑起来。不是因为资料少而是资料太杂有讲ROS1老版本的有直接甩源码让你自己编译的还有一上来就扔给你一堆TF变换和SLAM数学公式的。学了几天就劝退了。后来真正做机器人项目从ROS1迁到ROS2把节点、话题、服务、动作、DDS这一整套机制理清楚之后回头看新手最容易卡住的根本不是智商问题而是没有人告诉你“这个东西到底在干嘛”。这篇教程我就把自己从零入门ROS2的完整路径整理出来覆盖环境搭建、核心概念、消息传递机制、小乌龟实操、DDS详解、回调组原理、常用仿真工具和问题排错。标题叫“看这一篇就足够”意思是把你入门阶段百分之八十会踩的坑、百分之六十必需的概念都装进来。我不太喜欢写那种“点个星星然后去看官方文档”的水文这篇会用能跑通的代码、能直接复制执行的命令和真实踩坑记录来说话。1. 环境搭建先让ROS2能在你电脑上跑起来学ROS2第一道坎永远是环境。很多新手下了一堆资料结果安装完一秒就直接卡在ros2: command not found然后就开始怀疑人生。其实ROS2环境搭建本身并不难难在“版本匹配”和“环境变量”这两件事上。版本选对了后面会顺很多。1.1 版本怎么选别追新选LTS长期支持版ROS2的版本更新节奏跟Ubuntu发行版绑定大致是一年两个版本但是每个发布时间点对应的支持周期不一样。ROS2目前常用的版本里最推荐新手使用的是Humble Hawksbill对应Ubuntu 22.04它是LTS长期支持版本2024年4月之前有官方支持实际社区维护还更久。选LTS的原因很实际你的同事、教材、中文社区的报错帖、GitHub上的issue大多是基于Humble的。真遇到问题搜一搜基本都有现成答案。如果电脑上装的是Ubuntu 20.04那就用ROS2 Foxy这也是一个用得非常多的版本。Ubuntu 24.04对应的是Jazzy目前也比较稳了但中文资料和第三方包生态还在积累中。总之我的建议是看自己的Ubuntu版本装一个对应的LTS别去装中间的那些非长期维护版本。1.2 三种安装方式apt、Docker还有一键脚本ROS2的标准官方安装方式是通过apt源安装这是最稳妥也最常见的做法。核心步骤就三条添加ROS2的软件源更新索引然后安装ros-humble-desktop。桌面版包含图形化工具、RVIZ、Gazebo这些东西新手装desktop版就够了不需要折腾base版。命令大概是这样的sudo apt update sudo apt install software-properties-common curl sudo add-apt-repository universe sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release echo $UBUNTU_CODENAME) main | sudo tee /etc/apt/sources.list.d/ros2.list /dev/null sudo apt update sudo apt install ros-humble-desktop如果你用的是国内网络环境访问packages.ros.org可能会比较慢这时候可以考虑用国内镜像源把packages.ros.org替换成镜像站地址即可。第二种方式是用Docker。如果你不想把宿主机的系统搞乱、需要多版本共存或者想在苹果电脑上跑Linux容器Docker是很好的选择。ROS2官方提供了镜像仓库ros:humble拉下来之后配合VS Code的Dev Containers插件开发体验也不差。但Docker方案对新手稍微有一点门槛因为它涉及文件/GPU/网络端口映射需要先懂一点Docker基础知识。第三种是我特别想说的一键安装脚本也就是很多人熟知的“鱼香ROS一键安装”。这种方式对新手非常友好它会自动帮你装好依赖、添加源、安装ROS2本体还会顺手帮你配置环境变量省掉很多劝退操作。使用方式也很简单wget http://fishros.com/install -O fishros . fishros然后跟着菜单选择安装ROS2 Humble即可。我第一次给人推荐的是官网流程后来发现很多零基础的朋友在“添加公钥”“写源”这几步会卡住改用一键脚本之后基本十分钟之内就能跑完。这不是什么玄学就是省掉了手动操作步骤里的潜在出错点。1.3 安装完先验证环境和环境变量ROS2装完之后不要马上去写代码先打开一个终端执行ros2 --help如果提示找不到命令先说三遍没有source环境。安装之后你需要告诉shellROS2的可执行文件在哪个路径下。执行这条命令source /opt/ros/humble/setup.bash然后就能用了。但问题是每次开新终端都要重新source一遍太麻烦。所以把它写进bash的配置里echo source /opt/ros/humble/setup.bash ~/.bashrc source ~/.bashrc这一步做完以后再开终端就直接能用ROS2命令了。我见过太多新手装了ROS2结果每个新终端第一行都要手动source一旦忘了就到处报错。环境变量这个问题最好在一开始就养成习惯。2. 核心概念节点、话题、服务、动作、参数一次讲透ROS2最核心的机制可以概括成一张“计算图”图上的每个单元负责干一件事彼此之间通过标准化的接口互相通信。这种方式最大的好处是写代码的时候各写各的运行的时候可以自由组合。理解这张图比整天背概念有用得多。2.1 节点可执行程序运行起来的最小单位节点就是程序运行起来之后在ROS2网络里的一个实体。一个机器人的系统可以拆成很多节点激光雷达驱动节点负责发点云数据里程计节点负责发位置变换导航节点负责决策电机控制节点负责执行指令。每个节点都是独立的可以单独启停、替换、重用。命令层面查看当前有哪些节点在运行用ros2 node list查看某个节点的详细信息包括它接收和发布的接口ros2 node info /节点名新建节点的方式我们先不展开后面实操部分会演示。这里先记住一句话节点是ROS2世界里“干活的人”而节点之间怎么配合靠的是下面这些通信接口。2.2 话题点对多异步通信数据的广播电台话题是ROS2用的最多的通信方式模型是发布-订阅。一个节点往某个话题上发布数据其他订阅了这个话题的节点就能收到。比如小乌龟仿真器里的/turtle1/cmd_vel话题就是用来发布乌龟速度指令的。话题适合传感器数据、状态消息这种持续流式的数据。它的特点是异步发布者发完就完事了不关心订阅者有没有收到就像广播电台播音员播完新闻不会等听众反馈。常用命令ros2 topic list # 查看当前所有话题 ros2 topic echo /话题名 # 实时打印话题内容 ros2 topic info /话题名 # 查看话题类型、发布者和订阅者数量 ros2 topic hz /话题名 # 查看话题发布频率很常用2.3 服务同步请求响应像打电话叫客服话题解决的是“发布-订阅”的问题但有些场景是“我去请求一个东西你给我一个答复”。比如让机械臂去某个位置、让相机保存一张图片、查询当前机器人电量。这种场景用服务。服务模型有三个角色服务端、客户端、数据类型。客户端发送请求服务端处理完返回响应整个过程是同步的客户端要等服务端回复。跟话题最大的区别就在这话题是数据一直在流服务是一问一答。命令ros2 service list # 查看所有服务 ros2 service type /服务名 # 查看服务类型 ros2 interface show 服务类型 # 查看服务的请求和响应结构2.4 动作长时程任务的专属通信方式服务适合短任务但有些任务需要执行很长时间而且在执行过程中还要不断上报进度比如“让机器人导航到A点”“让机械臂抓取一个杯子”也许要花十秒钟甚至更久。如果用服务客户端就得一直阻塞等在那里中间什么都干不了。ROS2专门为这种场景设计了动作。动作相当于“服务 话题 反馈”的组合。调用动作后服务端会持续发送反馈消息进度、状态客户端可以随时查看中途也能发起取消请求。这在机器人运动控制、导航任务里非常常见。实际操作中如果你在命令行看到ros2 action list ros2 action info /action名就说明这个节点提供了动作接口。后面实操部分我们会专门跑一个动作示例。2.5 参数节点的运行配置项参数就是一个节点的配置项存储方式是一个键值对。比如摄像头节点有个参数frame_id用来设置坐标系的名称PID控制节点有Kp、Ki、Kd参数。命令ros2 param list ros2 param get /节点名 /参数名 ros2 param set /节点名 /参数名 值这里有一个新手容易忽略的细节ROS2的参数默认是运行时可以通过ros2 param set动态修改的但节点退出后就恢复默认值。如果你想让参数永久生效一般通过launch文件在启动时传入。2.6 工作空间与功能包ROS2工程的组织方式理解了节点和通信方式还差最后一块拼图你的代码放哪ROS2的组织单位叫功能包。包可以理解成“一个独立功能的文件夹”里面存放源代码、配置文件、消息定义和启动文件。你可以在工作空间里放很多包然后统一编译。创建包的命令ros2 pkg create 包名 --build-type ament_python --dependencies rclpy std_msgsbuild-type就是你要用哪种语言来写Python用ament_pythonC用ament_cmake。dependencies是依赖的库写多少编译时就会自动处理多少。编译整个工作空间colcon build source install/setup.bash这里有一个非常重要的操作习惯每次编译之后都要重新source一下install目录里的setup.bash否则你新增的包不会被识别到。我见过太多人colcon build之后直接跑ros2 run结果提示找不到包然后一脸茫然。3. DDS与消息传递机制ROS2为什么比ROS1快且稳定聊完概念就不得不提ROS2最底层的通信实现DDS。很多人知道ROS2用的是DDS但不知道DDS到底干了什么也不知道QoS是什么。这一节把它讲明白因为这部分理解了后面排查通信问题就会非常有底气。3.1 从ROS1的master到ROS2的DDSROS1的通信方式是中心化的所有节点先连到一个叫roscore的“总机”上节点间要传递数据先问总机“对方在哪”经过总机牵线之后再建立点对点连接。这个设计的最大痛点是一旦总机挂了整个机器人系统全挂而且总机的存在也让跨机器通信变得很麻烦。ROS2直接把底层通信换成DDS彻底去掉了中心节点。每个节点自己就是通信域里的一个成员发现彼此之后直接通信不需要中央服务器。这意味着两件事一是系统更坚韧某个节点挂了不会带崩整个系统二是天然支持分布式机器人上的节点、工控机上的节点、远程电脑上的节点可以天然组网通信。3.2 DDS是什么一套完整的发布订阅中间件规范DDSData Distribution Service数据分发服务。它不是某个公司的闭源协议而是一套由对象管理组织OMG维护的规范。ROS2可以接多种DDS实现比如FastDDS、CycloneDDS、RTI Connext DDS它们就像“不同品牌的网卡”功能都遵循同一套标准日常开发你不需要关心具体用哪个系统默认配置就行。每个DDS数据域里参与者之间通过话题名称来匹配数据流。这个“话题”和ROS2上层的话题概念是相通的。发布方有一个DataWriter订阅方有一个DataReader两端靠“兼容的QoS策略”才能建立连接。有个细节经常被忽略如果你用DDS做多机通信所有机器必须在同一个DDS域也就是默认的ROS_DOMAIN_ID要一致。默认情况下它是0如果你有两台机器建议设成同一个非零值避免和旁边其他团队的设备互相串数据export ROS_DOMAIN_ID423.3 消息传递机制与QoSQoS服务质量是ROS2通信里非常重要也是新手最容易忽略的概念。它决定了数据在传递过程中到底有多可靠、多及时。可以把它理解为“快递服务等级”有的数据是普通的包裹丢了就丢了不用重发有的数据是重要文件必须签收并确认还有的数据是直播画面只关心最新状态不关心历史内容。ROS2里QoS有几种常用策略Reliability可靠性分为RELIABLE数据必须送达丢了会重传BEST_EFFORT丢了就丢了适合对实时性要求高但对完整度不敏感的传感器数据。History历史保留KEEP_LAST只保留最近N条适合持续流式数据KEEP_ALL全部保留适合重要但低频的数据。Durability持续性设置后加入的订阅者是否能收到发布者过去发过的消息一般在合并地图、静态地图这种场景才会用到。你可能会想默认用RELIABLE不就行了不一定。比如激光雷达数据量非常大如果要求每条都可靠传输一旦网络波动就会大量积压重传包反而导致延迟飙升。所以激光雷达通常用BEST_EFFORT而控制指令、任务指令这种低频但关键的数据用RELIABLE。实际开发中我会在代码里显式声明QoS策略尤其是写传感器驱动和接收端的时候两个节点的QoS必须兼容才能通信。如果不兼容运行时会报错经常出现“Publisher和Subscriber连不上”的诡异情况。排查这种事第一反应就去看双方的QoS设置。3.4 怎么选通信方式一张表帮你定夺话题、服务、动作各司其职实际工程里最忌讳的是用错工具。比如导航任务如果非要用话题手动发目标点而不是用动作包一层那么暂停、取消、进度反馈这些功能全都要自己写。反过来如果只是一个简单的查询请求也没有必要动用动作平白增加复杂度。通信方式模型典型场景例子话题发布-订阅异步持续传感器数据、状态流雷达点云、图像、速度指令服务请求-响应同步短任务查询/触发一次性操作查询电量、保存图片动作请求-响应-反馈长任务长时间执行且需进度反馈导航、机械臂运动4. 实操跑通从安装验证到自写节点的完整闭环理论说再多不如跑一个完整的小项目。这一节我们从安装完ROS2开始一步步体验小乌龟、命令行工具、编写Python节点、自定义消息到动作调用完成一个入门阶段最经典的闭环。4.1 五分钟跑通小乌龟ROS2自带了一个演示用的小乌龟仿真器它的作用相当于编程界的“Hello World”。打开一个终端运行ros2 run turtlesim turtlesim_node这会启动一个带着一只小乌龟的蓝色窗口。再打开一个新终端运行ros2 run turtlesim turtle_teleop_key然后用键盘上的方向键控制乌龟移动。能动了说明你的ROS2核心通信链路已经通了。接着说几个基本功在另外一个终端执行ros2 node list会看到当前有两个节点在运行执行ros2 topic list能看到一堆话题其中/turtle1/cmd_vel就是键盘节点发速度指令的话题/turtle1/pose是乌龟自己上报位置的话题。再看数据长什么样ros2 topic echo /turtle1/pose终端里会不断刷新乌龟的坐标和角度。看完就知道所谓的“通信”一点也不神秘就是几个节点之间传来了结构化的数据。4.2 手动发布指令不用写代码也能让乌龟动在没写任何代码的情况下命令行本身就能当作一个发布节点用。发送一条线速度给乌龟ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist {linear: {x: 2.0}, angular: {z: 1.8}}执行后乌龟会转着圈前进。这条命令的作用是向/turtle1/cmd_vel话题发布一条geometry_msgs/msg/Twist消息里面给定线速度和角速度。这个操作想说明一件事ROS2里所有的通信都可以在命令行工具层面直接观察、直接模拟。4.3 用Python写一个发布者节点命令行玩明白了那就开始写自己的节点。先在终端建一个工作空间mkdir -p ~/ros2_ws/src cd ~/ros2_ws colcon build然后创建我们的第一个功能包cd ~/ros2_ws/src ros2 pkg create study_pkg --build-type ament_python --dependencies rclpy geometry_msgs在study_pkg/study_pkg目录下新建一个文件velocity_publisher.pyimport rclpy from rclpy.node import Node from geometry_msgs.msg import Twist class VelocityPublisher(Node): def __init__(self): super().__init__(velocity_publisher) self.publisher self.create_publisher(Twist, /turtle1/cmd_vel, 10) self.timer self.create_timer(0.5, self.timer_callback) def timer_callback(self): msg Twist() msg.linear.x 2.0 msg.angular.z 1.0 self.publisher.publish(msg) self.get_logger().info(fPublishing: linear.x{msg.linear.x}, angular.z{msg.angular.z}) def main(argsNone): rclpy.init(argsargs) node VelocityPublisher() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()这段代码做的事情很清晰创建一个节点节点里建一个发布者和一个定时器每0.5秒给乌龟发一次线速度和角速度。然后修改setup.py把入口点注册进去entry_points{ console_scripts: [ velocity_publisher study_pkg.velocity_publisher:main, ], },回到工作空间根目录编译并运行cd ~/ros2_ws colcon build --packages-select study_pkg source install/setup.bash ros2 run study_pkg velocity_publisher这时候哪怕你没有在键盘节点上按任何键乌龟也会自行转圈。这就是“发布节点”的作用节点一启动就开始持续往话题上写数据。4.4 用Python写一个订阅者节点有了发布者再来一个订阅者。我们订阅/turtle1/pose把乌龟的位置实时打印出来。在study_pkg/study_pkg下新建pose_subscriber.pyimport rclpy from rclpy.node import Node from turtlesim.msg import Pose class PoseSubscriber(Node): def __init__(self): super().__init__(pose_subscriber) self.subscription self.create_subscription( Pose, /turtle1/pose, self.listener_callback, 10) def listener_callback(self, msg): self.get_logger().info(fx{msg.x:.2f}, y{msg.y:.2f}, theta{msg.theta:.2f}) def main(argsNone): rclpy.init(argsargs) node PoseSubscriber() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()同样在setup.py里加上pose_subscriber study_pkg.pose_subscriber:main,然后编译运行。这时能看到终端里不断刷新乌龟的实时坐标和ros2 topic echo是一条数据来源只是这次我们已经把数据接入到了自己的程序里。至此你已经走通了一个完整的“发布-订阅”开发链路创建包、声明依赖、写节点、注册入口点、编译、运行。后面无论做多复杂的机器人系统骨架都是一样的。4.5 自定义消息当内置类型不够用的时候ROS2自带了很多消息类型比如std_msgs/msg/String、geometry_msgs/msg/Twist但实际项目里总有“自定义结构”的需求比如你需要一次性发送机器人ID、坐标和置信度。这个时候就要自定义消息。在功能包目录下创建msg/文件夹新建RobotState.msgint32 robot_id float64 x float64 y float64 confidence然后在CMakeLists.txt里把消息文件注册进去编译时ROS2会自动为你生成对应的Python和C接口。生成的接口名格式是包名/msg/消息名使用起来跟内置消息完全一样。这里要提醒一句使用自定义消息时如果包是C编译型包编译之后一定要重新source环境否则你运行节点时Python解析器找不到新生成的消息模块会报ModuleNotFoundError。4.6 动作实操体验带反馈的长任务动作机制听起来抽象但用ROS2自带的示例跑一下就明白了。启动一个动作服务端ros2 run action_tutorials_py fibonacci_action_server在另一个终端查看动作列表会发现一个名为/fibonacci的动作服务。然后用客户端去调用ros2 run action_tutorials_py fibonacci_action_client终端里会显示客户端不断收到服务端返回的斐波那契序列中间结果直到任务完成。这个过程就是动作的标准流程发送目标、接收反馈、返回结果。你去操作导航任务时本质走的就是这样一套流程。5. 回调组与执行器搞懂它才能写出不卡顿的节点这个话题说的人少但新手写了一个“一执行就卡死”的节点十有八九是回调执行机制没搞明白。这里我尽量用大白话讲透。5.1 回调不是“同时执行”的ROS2节点里定时器、订阅消息、服务请求这些事件发生时都会触发对应的回调函数。这些回调不是被“即时并行”执行的而是被一个叫Executor的调度器统一排队执行。默认情况下一个节点只有一个Executor而且是单线程的。这意味着如果有一个回调函数里写了阻塞代码比如sleep等待5秒、或者一个极其耗时的大循环那么其他所有回调都会排队等着程序看起来就像“卡死”了一样。我处理过一个典型问题一个机器人节点既要订阅激光雷达发数据又要响应按钮服务启动动作。原本看起来是两个独立的事但按钮服务的回调函数里有个很慢的直线插值算法导致激光雷达数据的订一回调一直被阻塞机器人的避障功能直接失效。5.2 回调组是什么两种类型的区别在哪为了解决这个卡顿问题ROS2提供了回调组CallbackGroup机制。回调组分为两种Mutually Exclusive互斥组组内的回调同一时间只执行一个这是默认行为。Reentrant可重入组组内的回调可以在不同线程同时执行互不阻塞。同时你还需要把Executor换成多线程版本from rclpy.executors import MultiThreadedExecutor executor MultiThreadedExecutor() executor.add_node(node) executor.spin()5.3 实际配置一步到位配置思路是把“耗时任务”和“高频实时数据”分开放在不同的回调组里再让多线程执行器去跑。比如fast_group reentrant_callback_group MutuallyExclusiveCallbackGroup() # 传感器高频数据 slow_group MutuallyExclusiveCallbackGroup() # 服务、动作等低频任务 self.create_subscription(..., callback_groupfast_group) self.create_service(..., callback_groupslow_group)这样高频订阅和低频服务各有各的执行通道互不拖累。这个知识点虽然偏进阶但在你接触真实机器人项目时几乎是必然要遇到的。我强烈建议所有想认真做ROS2开发的同学在写完第一个完整节点之后就主动读一遍官方关于Executors的说明不需要很深入但要把“单线程串行”和“多线程回调组”这两个概念刻进脑子里。6. 可视化、仿真与进阶方向从入门迈向实战学到这里你的基础已经足够去看一些真实项目了。我再补充几个常用的配套工具和方向方便你接下来的路不至于迷茫。6.1 RVIZ2机器人开发的窗口RVIZ2是ROS2最常用的3D可视化工具。它能显示激光点云、相机图像、机器人模型、坐标变换TF甚至可以给导航目标点。启动方式ros2 run rviz2 rviz2RVIZ2本质上也是一个ROS2节点你可以在它的界面里添加各种显示插件。我见过很多新手以为RVIZ2是“装好了就能看到3D模型”实际上它只是一个“数据显示器”你要先把机器人模型和传感器数据节点跑起来它才有东西可显示。调试机器人系统时打开RVIZ2比对着终端数据流直观得多。6.2 Gazebo与仿真工具链Gazebo是目前ROS2生态里集成度最高的物理仿真器适合做机器人的物理运动仿真在无实机的情况下验证算法。启动方式一般是ros2 launch gazebo_ros gazebo.launch.py如果嫌Gazebo重也有轻量一些的选择。比如Mujoco本身不是ROS2官方默认集成的但通过mujoco_ros这类桥接包也能接入ROS2话题适合做机械臂、足式机器人的强化学习环境仿真。工具没有绝对的好坏重点是选一个能帮你快速迭代算法的环境。6.3 导航与建图的下一步方向入门之后大多数人会想做自主导航。ROS2的导航栈叫Nav2基本工作流是先用激光雷达SLAM建图再在地图上设定目标点Nav2负责全局路径规划和局部避障。这里会接触到“八叉树地图导航”之类的概念比如用octomap_server发布三维八叉树占据地图配合MoveIt做机械臂避障。一上来不必苛求完全理解知道这个技术地图的存在对整体认知很有帮助。7. 常见问题与排查技巧实录最后把我这些年遇到的新手高频问题整理成一张速查表。这些问题我几乎每隔几天就会被问一次你提前看完能省下不少自己瞎琢磨的时间。问题现象根本原因解决办法ros2: command not found没source环境或没写入bashrcsource /opt/ros/humble/setup.bashros2 run 提示找不到包编译后没source install目录source ~/ros2_ws/install/setup.bash两个节点话题连不上QoS策略不兼容检查双方的reliability/history设置编译时报找不到rclpy缺少依赖或没选对build-type创建包时用--dependencies rclpy跑起来的节点一执行就卡死单线程executor被阻塞改用MultiThreadedExecutor回调组多机通信相互看不到话题ROS_DOMAIN_ID不一致手动设成同一个非零值colcon build非常慢每次全量编译用--packages-select只编译指定包再分享一个排查套路先ros2 node list看节点在不在再ros2 topic list看话题在不在再ros2 topic hz看数据发不发如果这个链路都正常那问题多半出在你的代码逻辑里。这套三步排查法能解决百分之九十的“没反应”问题。另外一个很容易踩的坑是一个终端里同时source了多个版本的环境。比如先source了ROS1的setup.bash又source了ROS2的环境变量互相覆盖命令行为就会变得非常怪。建议每个终端只source一个ROS版本环境。最后再分享一个我自己习惯的小技巧写ROS2程序时不要一上来就搭很大的架构先从一个最小节点跑通确认话题通了再加功能、拆模块、加参数。有一次我带新人做项目他花了一整天搭了一个十几层目录的框架最后连一个最简单的数据都没跑通。我印象特别深因为他沮丧地说“ROS2太难了”。其实不是ROS2难是步子迈太大了。先让一个节点跑起来再让两个节点说话从最简单的闭环开始所有复杂系统都是这么一点点搭出来的。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →