12.写一个简单的发布器与订阅器(C++)

写一个简单的发布器与订阅器(C++)(rawmeat:http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber(c%2B%2B))

注意:本教程翻译于2018.9.25,英文原版教程来自于wiki.ros.org,,内容有可能更新,请以英文原版教程为准。

描述:这个教程包括如何用C++写一个发布器与订阅器
教程等级:初学者
下一个教程检查简单的发布器与订阅器

内容 (只介绍catkin版本)
  1.写一个发布器节点
    1.节点
    2.节点解释()
  2.写一个订阅器节点
    1.节点
    2.节点解释
  3.构建你的节点
  4.额外的资源
    1.视频教程

1.写发布器节点

”节点“是ROS术语,可执行的,连接到ROS网络。这儿我们将会创造一个发布器(”谈论者“)节点,将会持续地广播消息。

改变目录到你之前教程catkin workspace创建的beginner_tutorials:

roscd beginner_tutorials

1.1 代码

在beginner_tutorials package目录中创造一个src目录:

mkdir -p src

这个目录将会为我们的beginner_tutorials package包含任何源文件。

在beginner_tutorials package内创造src/talker.cpp然后黏贴以下的内容进入:
https://raw.github.com/ros/ros_tutorials/kinetic-devel/roscpp_tutorials/talker/talker.cpp

27 #include “ros/ros.h”
28 #include “std_msgs/String.h”
29
30 #include < sstream >
31
32 /**
33 * This tutorial demonstrates simple sending of messages over the ROS system.
34 */
35 int main(int argc, char argv)
36 {
37 / **
38 * The ros::init() function needs to see argc and argv so that it can perform
39 * any ROS arguments and name remapping that were provided at the command line.
40 * For programmatic remappings you can use a different version of init() which takes
41 * remappings directly, but for most command-line programs, passing argc and argv is
42 * the easiest way to do it. The third argument to init() is the name of the node.
43 *
44 * You must call one of the versions of ros::init() before using any other
45 * part of the ROS system.
46 /
47 ros::init(argc, argv, “talker”);
48
49 /

50 * NodeHandle is the main access point to communications with the ROS system.
51 * The first NodeHandle constructed will fully initialize this node, and the last
52 * NodeHandle destructed will close down the node.
53 /
54 ros::NodeHandle n;
55
56 /

57 * The advertise() function is how you tell ROS that you want to
58 * publish on a given topic name. This invokes a call to the ROS
59 * master node, which keeps a registry of who is publishing and who
60 * is subscribing. After this advertise() call is made, the master
61 * node will notify anyone who is trying to subscribe to this topic name,
62 * and they will in turn negotiate a peer-to-peer connection with this
63 * node. advertise() returns a Publisher object which allows you to
64 * publish messages on that topic through a call to publish(). Once
65 * all copies of the returned Publisher object are destroyed, the topic
66 * will be automatically unadvertised.
67 *
68 * The second parameter to advertise() is the size of the message queue
69 * used for publishing messages. If messages are published more quickly
70 * than we can send them, the number here specifies how many messages to
71 * buffer up before throwing some away.
72 */
73 ros::Publisher chatter_pub = n.advertise<std_msgs::String>(“chatter”, 1000);
74
75 ros::Rate loop_rate(10);
76
77 / **
78 * A count of how many messages we have sent. This is used to create
79 * a unique string for each message.
80 */
81 int count = 0;
82 while (ros::ok())
83 {
84 / **
85 * This is a message object. You stuff it with data, and then publish it.
86 */
87 std_msgs::String msg;
88
89 std::stringstream ss;
90 ss << “hello world " << count;
91 msg.data = ss.str();
92
93 ROS_INFO(”%s", msg.data.c_str());
94
95 / **
96 * The publish() function is how you send messages. The parameter
97 * is the message object. The type of this object must agree with the type
98 * given as a template parameter to the advertise<>() call, as was done
99 * in the constructor above.
100 */
101 chatter_pub.publish(msg);
102
103 ros::spinOnce();
104
105 loop_rate.sleep();
106 ++count;
107 }
108
109
110 return 0;
111 }

1.2 代码解释

现在,我们分解代码:

27 #include “ros/ros.h”
28

ros/ros.h是一个便利集合,包含了使用ROS系统的最常用的公共部分所有header。

Toggle line numbers

28 #include “std_msgs/String.h”
29

这个包括了std_msgs/String消息,存在于 std_msgs package中。这个header从那个package中的String.msg文件自动产生的。为了更多消息定义的信息,请见msg页面。

47 ros::init(argc, argv, “talker”);

初始化ROS。这个使得ROS可以通过命令行进行命名重映射–现在还不重要。这个也是我们指定节点名称的地方。在一个运行的系统中节点名称必须是独特的。

在这儿使用的名称必须是一个基本名称,也就是说,不能有/在其中。

54 ros::NodeHandle n;

向这个进程节点创造一个handle。第一个被创造的nodehandle将会初始化节点,最后一个被毁灭的将会清除节点正在使用的任何资源。

73 ros::Publisher chatter_pub = n.advertise<std_msgs::String>(“chatter”, 1000);

告诉节点管理器我们将会发布一条std_msgs/String类型的消息在话题chatter。这个让主人告诉任何正在收听chatter的节点我们将会在话题发布数据。第二个参数是我们发布队列的尺寸,这种情况下如果我们发布太快,它将会在丢弃旧消息之前缓冲最多1000条消息。

NodeHandle::advertise()返回了一个ros::Publisher对象,它实现了两个目的:(1)它包含一个publish()方式,让你在话题上发布消息(2)当它超出范围时,它将会自动unadvertise。

75 ros::Rate loop_rate(10);

一个ros::Rate目标让你可以指定一个你愿意循环的频率。它将会跟踪自从上次对Rate::sleep()的召唤之后有多久,然后休眠正确的时间。

在这样的情况下我们将会以10Hz运行。

81 int count = 0;
82 while (ros::ok())
83 {

通过默认的roscpp将会安装一个SIGINT handler,提供ctrl-c handling,这将会造成ros::ok()返回错误值,如果发生的话。

Ros::ok()将会返回错误值,如果:

  • 一个SIGINT被接收到(Ctrl-C)
  • 我们已经被另外一个有着相同名字的节点踢下网络
  • ros::shutdown()被应用的另外一部分召唤
  • 所有的ros::NodeHandles被摧毁

一旦ros::ok()返回错误值,所有的ROS召唤将会失败。

87 std_msgs::String msg;
88
89 std::stringstream ss;
90 ss << "hello world " << count;
91 msg.data = ss.str();

我们在ROS使用消息适应类广播消息,通常由一个msg文件产生。更复杂的数据类型也可以,但是现在我们只是使用标准的String 消息,有一个成员:”数据”。

101 chatter_pub.publish(msg);

现在我们实际上向任何连接的人广播消息。

93 ROS_INFO("%s", msg.data.c_str());

ROS_INFO和friends是我们对printf/cout的替代。可以看rosconsole documentation更多消息。

103 ros::spinOnce();

这儿召唤ros::spinOnce()对于这个简单的程序不是必需的,因为我们没有收到任何回复。然而,如果你在这个应用中增加订阅,但是在这儿没有ros::spinOnce(),你的回复将会永远不会被召唤。所以加上这个是一个好做法。

105 loop_rate.sleep();

现在我们使用ros::Rate目标来休眠一段剩下的时间,让我们满足10Hz的发布率。

这儿是正在进行的浓缩版本:

  • 初始化ROS系统
  • 通知我们将会在chatter话题发布std_msgs/String消息给主人
  • 循环,发布消息给chatter,10次一秒

现在我们需要写节点来接收消息。

2.写订阅器节点

2.1代码

在beginner_tutorials package中创造src/listener.cpp文件并黏贴下列进入:
https://raw.github.com/ros/ros_tutorials/kinetic-devel/roscpp_tutorials/listener/listener.cpp

28 #include “ros/ros.h”
29 #include “std_msgs/String.h”
30
31 / **
32 * This tutorial demonstrates simple receipt of messages over the ROS system.
33 * /
34 void chatterCallback(const std_msgs::String::ConstPtr& msg)
35 {
36 ROS_INFO(“I heard: [%s]”, msg->data.c_str());
37 }
38
39 int main(int argc, char argv)
40 {
41 / **
42 * The ros::init() function needs to see argc and argv so that it can perform
43 * any ROS arguments and name remapping that were provided at the command line.
44 * For programmatic remappings you can use a different version of init() which takes
45 * remappings directly, but for most command-line programs, passing argc and argv is
46 * the easiest way to do it. The third argument to init() is the name of the node.
47 *
48 * You must call one of the versions of ros::init() before using any other
49 * part of the ROS system.
50 * /
51 ros::init(argc, argv, “listener”);
52
53 /

54 * NodeHandle is the main access point to communications with the ROS system.
55 * The first NodeHandle constructed will fully initialize this node, and the last
56 * NodeHandle destructed will close down the node.
57 * /
58 ros::NodeHandle n;
59
60 / **
61 * The subscribe() call is how you tell ROS that you want to receive messages
62 * on a given topic. This invokes a call to the ROS
63 * master node, which keeps a registry of who is publishing and who
64 * is subscribing. Messages are passed to a callback function, here
65 * called chatterCallback. subscribe() returns a Subscriber object that you
66 * must hold on to until you want to unsubscribe. When all copies of the Subscriber
67 * object go out of scope, this callback will automatically be unsubscribed from
68 * this topic.
69 *
70 * The second parameter to the subscribe() function is the size of the message
71 * queue. If messages are arriving faster than they are being processed, this
72 * is the number of messages that will be buffered up before beginning to throw
73 * away the oldest ones.
74 * /
75 ros::Subscriber sub = n.subscribe(“chatter”, 1000, chatterCallback);
76
77 / **
78 * ros::spin() will enter a loop, pumping callbacks. With this version, all
79 * callbacks will be called from within this thread (the main one). ros::spin()
80 * will exit when Ctrl-C is pressed, or the node is shutdown by the master.
81 */
82 ros::spin();
83
84 return 0;
85 }

2.2代码解释

现在,让我们分解成一片一片的,忽略上文已经解释过的。

34 void chatterCallback(const std_msgs::String::ConstPtr& msg)
35 {
36 ROS_INFO(“I heard: [%s]”, msg->data.c_str());
37 }

这个是回调函数,将会在一条新的消息到达chatter话题的时候被召唤。消息在一个boost shared_ptr中被传递,这意味着你可以储存起来如果你愿意,而不用担心它没有复制潜在数据就被删除。

75 ros::Subscriber sub = n.subscribe(“chatter”, 1000, chatterCallback);

用节点管理器订阅Chatter话题。只要有消息到达,ROS将会召唤chatterCallback()函数。第二个参数是队列尺寸,防止我们不能快速处理数据。这种情况下,如果队列达到了1000条消息,我们将会在新信息到达时丢弃旧信息。

NodeHandle::subscribe()返回一个ros::Subscriber对象,你必须坚持下去,直到想退订为止。当订阅者被摧毁,它将会从chatter话题自动地退订。

这儿有NodeHandle::subscribe()函数的版本,让你可以指定类成员函数,或者甚至任何可以被一个Boost函数对象召唤的。roscpp概述包含更多消息。

82 ros::spin();

ros::spin()进入一个循环,尽可能快地调用消息的回调。如果没有任何事情要做,不用担心,它不会占用很多CPU。一旦ros::ok()返回false,则ros::spin()将退出,这意味着ros::shutdown()已被调用,或者是由默认的Ctrl-C处理程序调用,节点管理器告诉我们要关闭,或者被手动调用。

还有其他方法来回调,但我们不担心这些。roscpp_tutorialspackage有一些演示应用程序来演示这一点。roscpp 概述还包含更多信息。

又一次,这有一个正在运行的浓缩的版本:

  • 初始化ROS系统
  • 订阅chatter 话题
  • 旋转,等待消息到来
  • 当消息到来时,chatterCallback()函数被召唤

3.建立你的节点

你在之前的教程中使用了catkin_create_pkg,这个为你创造了一个package.xml和一个CMakeLists.txt

产生的CMakeList.txt应该看起来像这样(有从Creating Msgs and Srvs来的修改以及没有使用的注释和实例移除):
https://raw.github.com/ros/catkin_tutorials/master/create_package_modified/catkin_ws/src/beginner_tutorials/CMakeLists.txt

1 cmake_minimum_required(VERSION 2.8.3)
2 project(beginner_tutorials)
3
4 ## Find catkin and any catkin packages
5 find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs genmsg)
6
7 ## Declare ROS messages and services
8 add_message_files(DIRECTORY msg FILES Num.msg)
9 add_service_files(DIRECTORY srv FILES AddTwoInts.srv)
10
11 ## Generate added messages and services
12 generate_messages(DEPENDENCIES std_msgs)
13
14 ## Declare a catkin package
15 catkin_package()

别担心修改有注释的(#)例子,简单地增加这几行到CMakeLists.txt的底部:

add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
add_dependencies(talker beginner_tutorials_generate_messages_cpp)

add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(listener beginner_tutorials_generate_messages_cpp)

你最终的CMakeLists.txt应该看起来像这样:
https://raw.github.com/ros/catkin_tutorials/master/create_package_pubsub/catkin_ws/src/beginner_tutorials/CMakeLists.txt

1 cmake_minimum_required(VERSION 2.8.3)
2 project(beginner_tutorials)
3
4 ## Find catkin and any catkin packages
5 find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs genmsg)
6
7 ## Declare ROS messages and services
8 add_message_files(FILES Num.msg)
9 add_service_files(FILES AddTwoInts.srv)
10
11 ## Generate added messages and services
12 generate_messages(DEPENDENCIES std_msgs)
13
14 ## Declare a catkin package
15 catkin_package()
16
17 ## Build talker and listener
18 include_directories(include ${catkin_INCLUDE_DIRS})
19
20 add_executable(talker src/talker.cpp)
21 target_link_libraries(talker ${catkin_LIBRARIES})
22 add_dependencies(talker beginner_tutorials_generate_messages_cpp)
23
24 add_executable(listener src/listener.cpp)
25 target_link_libraries(listener ${catkin_LIBRARIES})
26 add_dependencies(listener beginner_tutorials_generate_messages_cpp)

这将会创造两个可执行文件, talker和listener,将会默认地进入你devel space的package目录,默认地址是 ~/catkin_ws/devel/lib/< package name>。

注意到你已经为可执行目标增加了依赖文件到信息产生目标:

add_dependencies(talker beginner_tutorials_generate_messages_cpp)

这个确保了这个package的信息header在使用之前产生。如果你使用你的catkin workspace的其他package的消息,你将会需要增加依赖文件到各自的生成目标,因为catkin 平行构建所有的文件,对于Groovy,可以使用以下变量来依赖所有必要的目标:

target_link_libraries(talker ${catkin_LIBRARIES})

你可以直接调用可执行文件或者你可以使用rosrun来调用。它们没有被放置在 ‘< prefix >/bin’ 因为在安装你的package到系统的时候将会污染PATH。如果你希望你的可执行文件在安装的时候在PATH,你可以建立一个安装目标,看:catkin/CMakeLists.txt

对于更多的CMakeList.txt的描述,请看catkin/CMakeLists.txt

现在运行catkin_make:

# In your catkin workspace
$ cd ~/catkin_ws
$ catkin_make

注意:如果你在增加作为一个新的pkg,你可能会需要告诉catkin强制制作–force-make选项。看catkin/Tutorials/using_a_workspace#With_catkin_make

既然你已经写了一个简单的发布者和订阅者,让我们检查examine the simple publisher and subscriber

4.额外的资源

这儿有一些额外的资源,由社区贡献:

4.1视频教程

以下是视频,展现一个小的教程,解释如何在ROS中写和测试一个发布者和订阅者,用C++和Python,基于上面提到的talker/listener(完整的视频在my Udemy Course on ROS(https://www.udemy.com/ros-essentials/?couponCode=ROSTUTORIALS))

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单C++ 代码示例,实现基于发布订阅模式的服务端和客户端: 服务端代码: ```cpp #include <iostream> #include <vector> #include <string> #include <thread> #include <mutex> #include <condition_variable> #include <chrono> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") // 服务端监听的端口号 const int PORT = 8888; // 最大连接数 const int MAX_CONN = 5; // 服务端接收消息的缓冲区大小 const int BUFFER_SIZE = 1024; // 发布-订阅模式中的“主题” std::string topic; // 已连接的客户端列表 std::vector<SOCKET> clients; // 客户端消息队列 std::vector<std::string> message_queue; // 互斥锁和条件变量,用于线程同步 std::mutex mtx; std::condition_variable cv; // 线程函数,用于接收客户端消息并广播给所有已连接的客户端 void broadcast_messages() { while (true) { // 等待条件变量被唤醒 std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, [] { return !message_queue.empty(); }); // 从消息队列中取出一条消息 std::string message = message_queue.front(); message_queue.erase(message_queue.begin()); // 广播消息给所有已连接的客户端 for (auto client : clients) { send(client, message.c_str(), message.size(), 0); } } } int main() { // 初始化 Winsock 库 WSADATA wsaData; int ret = WSAStartup(MAKEWORD(2, 2), &wsaData); if (ret != 0) { std::cerr << "WSAStartup failed with error: " << ret << std::endl; return -1; } // 创建一个 TCP socket 对象 SOCKET server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (server_socket == INVALID_SOCKET) { std::cerr << "Failed to create server socket with error: " << WSAGetLastError() << std::endl; WSACleanup(); return -1; } // 绑定 IP 地址和端口号 sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(PORT); ret = bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr)); if (ret == SOCKET_ERROR) { std::cerr << "Failed to bind server socket with error: " << WSAGetLastError() << std::endl; closesocket(server_socket); WSACleanup(); return -1; } // 开始监听,等待客户端连接 ret = listen(server_socket, MAX_CONN); if (ret == SOCKET_ERROR) { std::cerr << "Failed to listen on server socket with error: " << WSAGetLastError() << std::endl; closesocket(server_socket); WSACleanup(); return -1; } std::cout << "Server started listening on port " << PORT << std::endl; // 创建一个线程,用于接收客户端消息并广播给所有已连接的客户端 std::thread broadcast_thread(broadcast_messages); while (true) { // 接受客户端连接请求 sockaddr_in client_addr; int addr_len = sizeof(client_addr); SOCKET client_socket = accept(server_socket, (sockaddr*)&client_addr, &addr_len); if (client_socket == INVALID_SOCKET) { std::cerr << "Failed to accept client connection with error: " << WSAGetLastError() << std::endl; continue; } std::cout << "Accepted a client connection from " << inet_ntoa(client_addr.sin_addr) << std::endl; // 服务端向客户端发送欢迎消息 std::string welcome_message = "Welcome to the server!"; send(client_socket, welcome_message.c_str(), welcome_message.size(), 0); // 将客户端加入已连接客户端列表 clients.push_back(client_socket); // 循环接收客户端消息 while (true) { // 接收客户端消息 char buffer[BUFFER_SIZE] = { 0 }; int recv_len = recv(client_socket, buffer, BUFFER_SIZE, 0); if (recv_len == SOCKET_ERROR) { std::cerr << "Failed to receive data from client with error: " << WSAGetLastError() << std::endl; break; } else if (recv_len == 0) { std::cout << "Client disconnected." << std::endl; break; } // 打印客户端消息 std::cout << "Message from client: " << buffer << std::endl; // 将客户端消息加入消息队列 std::string message = "[" + topic + "] " + buffer; message_queue.push_back(message); // 唤醒线程,广播消息给所有已连接的客户端 cv.notify_all(); } // 从已连接客户端列表中删除该客户端 clients.erase(std::remove(clients.begin(), clients.end(), client_socket), clients.end()); // 关闭客户端连接 closesocket(client_socket); } // 关闭服务端 socket closesocket(server_socket); // 等待线程结束 broadcast_thread.join(); // 清理 Winsock 库 WSACleanup(); return 0; } ``` 客户端代码: ```cpp #include <iostream> #include <string> #include <thread> #include <mutex> #include <condition_variable> #include <chrono> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") // 服务端 IP 地址和端口号 const char* SERVER_IP = "127.0.0.1"; const int SERVER_PORT = 8888; // 客户端发送消息的缓冲区大小 const int BUFFER_SIZE = 1024; // 发布-订阅模式中的“主题” std::string topic; // 互斥锁和条件变量,用于线程同步 std::mutex mtx; std::condition_variable cv; // 线程函数,用于接收服务端广播的消息 void receive_messages(SOCKET client_socket) { while (true) { // 接收服务端广播的消息 char buffer[BUFFER_SIZE] = { 0 }; int recv_len = recv(client_socket, buffer, BUFFER_SIZE, 0); if (recv_len == SOCKET_ERROR) { std::cerr << "Failed to receive data from server with error: " << WSAGetLastError() << std::endl; break; } else if (recv_len == 0) { std::cout << "Server disconnected." << std::endl; break; } // 打印收到的消息 std::cout << "Message from server: " << buffer << std::endl; } } int main() { // 初始化 Winsock 库 WSADATA wsaData; int ret = WSAStartup(MAKEWORD(2, 2), &wsaData); if (ret != 0) { std::cerr << "WSAStartup failed with error: " << ret << std::endl; return -1; } // 创建一个 TCP socket 对象 SOCKET client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (client_socket == INVALID_SOCKET) { std::cerr << "Failed to create client socket with error: " << WSAGetLastError() << std::endl; WSACleanup(); return -1; } // 连接服务端 sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(SERVER_PORT); ret = connect(client_socket, (sockaddr*)&server_addr, sizeof(server_addr)); if (ret == SOCKET_ERROR) { std::cerr << "Failed to connect to server with error: " << WSAGetLastError() << std::endl; closesocket(client_socket); WSACleanup(); return -1; } std::cout << "Connected to server " << SERVER_IP << ":" << SERVER_PORT << std::endl; // 接收服务端欢迎消息 char buffer[BUFFER_SIZE] = { 0 }; int recv_len = recv(client_socket, buffer, BUFFER_SIZE, 0); if (recv_len == SOCKET_ERROR) { std::cerr << "Failed to receive data from server with error: " << WSAGetLastError() << std::endl; closesocket(client_socket); WSACleanup(); return -1; } else if (recv_len == 0) { std::cout << "Server disconnected." << std::endl; closesocket(client_socket); WSACleanup(); return -1; } // 打印服务端欢迎消息 std::cout << "Message from server: " << buffer << std::endl; // 循环发送消息 while (true) { // 从控制台读取用户输入的消息 std::cout << "请输入消息:"; std::string message; std::getline(std::cin, message); // 如果用户输入 quit,则退出循环 if (message == "quit") { break; } // 将消息发送给服务端 std::string full_message = "[" + topic + "] " + message; send(client_socket, full_message.c_str(), full_message.size(), 0); // 创建一个线程,用于接收服务端广播的消息 std::thread receive_thread(receive_messages, client_socket); // 等待线程结束 receive_thread.join(); } // 关闭客户端 socket closesocket(client_socket); // 清理 Winsock 库 WSACleanup(); return 0; } ``` 此代码实现了一个简单发布订阅模式,服务端接收客户端消息后会将其广播给所有已连接的客户端。如果您需要更灵活的订阅机制,可以使用一些成熟的消息队列中间件,例如 RabbitMQ 或 Apache Kafka。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值