【ROS1】08-ROS通信机制——服务通信

发布于:2025-07-22 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

一、概念

二、何时使用服务

三、话题通信与服务通信的区别

四、案例

4.1 C++实现

4.1.1 服务端

4.1.2 客户端

4.1.3 测试执行

4.2 Python实现

4.2.1 服务端

4.2.2 客户端

4.2.3 客户端优化——动态传参

4.2.4 客户端优化——等待服务端启动后再发起请求


一、概念

服务通信涉及两个角色:

  • 服务服务器 (Service Server)

    • 提供特定功能或数据的节点。

    • 它会“打广告”说:“我能提供XX服务!”(例如,“我能计算两个整数的和”)。

    • 它平时处于待命状态,一旦接收到请求,就执行相应的任务,并返回一个结果。

  • 服务客户端 (Service Client)

    • 需要某个特定功能或数据的节点。

    • 它会向服务器发送一个具体的请求(例如,“请帮我计算 3 和 5 的和”)。

    • 发送请求后,它会暂停自己的工作,一直等待,直到收到服务器的响应。

 

二、何时使用服务

当你需要立即得到一个确切的答复触发一个必须完成的远程操作时,就应该使用服务。

典型应用场景:

  • 查询数据: “机器人,你现在的坐标是多少?”

  • 触发动作: “机械臂,移动到指定位置。” / “相机,拍一张照片。”

  • 执行计算: “路径规划器,帮我计算一条从A到B的最优路径。”

  • 更改状态: “机器人,切换到自动驾驶模式。”

 

三、话题通信与服务通信的区别

服务通信 (Service) 话题通信 (Topic)
通信模型 请求/响应 发布/订阅
数据流向 双向 单向 
同步性 同步 - 客户端会阻塞等待 异步 - 发布后立即返回
连接关系 一对一  一对多/多对多
数据流类型 离散的、事务性的 连续的数据流
主要目的 执行远程调用、获取确切结果 持续广播状态、传感器数据

四、案例

客户端发送两个整数,服务端计算它们的和并返回

4.1 C++实现

4.1.1 服务端

进入到工作空间的src目录下,输入如下指令来创建一个名为“plumbing_server_client”的功能包

catkin_create_pkg plumbing_server_client roscpp rospy std_msgs

在功能包中创建一个srv目录

在srv目录中创建一个.srv文件,这里命名为“AddTwoInts.srv”

“AddTwoInts.srv”内容如下:

int64 a
int64 b
---
int64 sum

打开功能包中的“package.xml”,添加如下两行内容

<build_depend>message_generation</build_depend>

<exec_depend>message_runtime</exec_depend>

打开功能包下的“CMakeLists.txt”,添加如下内容:

添加“message_generation”

取消注释并添加“AddTwoInts.srv”

取消注释

取消注释

Ctrl+Shift+B编译一下

在功能包的src目录中新建一个.cpp文件,这里命名为“server.cpp” 

在“server.cpp” 中添加如下代码

#include "ros/ros.h"
#include "plumbing_server_client/AddTwoInts.h"

// 服务处理函数。当收到请求时,ROS会调用这个函数。
// 函数的返回值是 bool 类型,如果成功处理返回 true,否则返回 false。
// 参数是请求对象(req)和响应对象(res)的引用。
bool add(plumbing_server_client::AddTwoInts::Request  &req,
         plumbing_server_client::AddTwoInts::Response &res)
{
    // 从请求对象中取出数据
    res.sum = req.a + req.b;

    // ROS_INFO 用于在终端打印日志信息,类似于C++的cout
    ROS_INFO("请求: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
    ROS_INFO("发送响应: sum=%ld", (long int)res.sum);

    return true; // 表示服务成功执行
}

int main(int argc, char **argv)
{
    setlocale(LC_ALL,"");
    // 1. 初始化ROS节点
    ros::init(argc, argv, "add_two_ints_server");

    // 2. 创建节点句柄
    ros::NodeHandle n;

    // 3. 创建一个名为 "add_two_ints" 的服务
    //    它会调用 add 函数来处理请求
    ros::ServiceServer service = n.advertiseService("add_two_ints", add);

    ROS_INFO("服务已就绪,等待客户端请求...");

    // 4. 进入循环,等待回调函数的触发
    ros::spin();

    return 0;
}

4.1.2 客户端

在功能包的src目录下添加一个.cpp文件,这里命名为“client.cpp”

在“client.cpp”中添加如下代码

#include "ros/ros.h"
#include "plumbing_server_client/AddTwoInts.h"
#include <cstdlib> // 用于 atoll 函数

int main(int argc, char **argv)
{
    setlocale(LC_ALL,"");
    // 初始化ROS节点
    ros::init(argc, argv, "add_two_ints_client");

    // 检查命令行参数是否正确
    if (argc != 3)
    {
        ROS_INFO("用法: add_two_ints_client X Y");
        return 1;
    }

    // 创建节点句柄
    ros::NodeHandle n;

    // 创建一个客户端,连接到名为 "add_two_ints" 的服务
    // serviceClient 会一直尝试连接,直到成功
    ros::ServiceClient client = n.serviceClient<plumbing_server_client::AddTwoInts>("add_two_ints");

    // 创建一个服务对象 srv
    plumbing_server_client::AddTwoInts srv;
    // 将命令行参数转换为 long long (int64) 并填充到请求中
    srv.request.a = atoll(argv[1]);
    srv.request.b = atoll(argv[2]);

    // 调用服务
    // client.call() 是一个阻塞操作。它会发送请求并等待,直到收到响应。
    // 如果服务调用成功,call() 返回 true,响应数据会填充到 srv.response 中。
    // 如果失败,call() 返回 false。
    if (client.call(srv))
    {
        ROS_INFO("响应 Sum: %ld", (long int)srv.response.sum);
    }
    else
    {
        ROS_ERROR("调用服务失败");
        return 1;
    }

    return 0;
}

打开功能包中“CMakeLists.txt”文件,添加如下内容

编译一下

4.1.3 测试执行

开启三个终端,分别输入如下指令来依次启动ros核心、启动服务端、启动客户端

roscore  //启动ros核心

rosrun plumbing_server_client server  //启动服务端

rosrun plumbing_server_client client 15 20  //启动客户端

可以看到服务端成功计算并返回计算结果。

但是如果我们先开启客户端再开启服务端就会导致客户端抛出异常:

为了避免这个问题,我们可以给客户端添加如下一行代码,这样就可以客户端就可以等服务端启动后再请求

client.waitForExistence();
// 或
ros::service::waitForService("add_two_ints_server");  //add_two_ints_server为服务器节点名称

可以看到客户端在服务端未启动时一直等待 

等服务端启动后再执行请求

4.2 Python实现

4.2.1 服务端

打开“settings.json”,如果没有如下配置则需要补充

{
    "editor.tabSize": 4,
    "cmake.sourceDirectory": "/home/chaochao/demo02_ws/src/helloworld",
    "files.associations": {
        "sstream": "cpp"
    },
    "python.autoComplete.extraPaths": [
        "/opt/ros/noetic/lib/python3/dist-packages",
        "/home/chaochao/demo02_ws/devel/lib/python3/dist-packages"
    ]
}

新建一个文件夹“scripts”

添加一个Python文件,这里命名为“server_py.py”

在“server_py.py”添加如下代码:

#! /usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
from plumbing_server_client.srv import AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse
# from plumbing_server_client.srv import *

"""
服务端:解析客户端请求,产生响应
    1. 导包
    2. 初始化 ROS 节点;
    3. 创建服务端对象;
    4. 处理逻辑(回调函数)
    5. spin()
"""

# 4. 处理逻辑
def call_doInt(request):
    num1 = request.a
    num2 = request.b
    sum = num1 + num2

    response = AddTwoIntsResponse()  # 将结果封装进response
    response.sum = sum

    rospy.loginfo("服务器收到:num1=%d, num2=%d, 响应:sum=%d", num1, num2, sum)

    return response


if __name__ == "__main__":
    # 2. 初始化 ROS 节点;
    rospy.init_node("server")

    # 3. 创建服务端对象;
    server = rospy.Service("AddTwoInts", AddTwoInts, call_doInt)  # "AddTwoInts":服务的名称; AddTwoInts:服务的类型; call_doInt:回调函数;
    
    # 5. spin()
    rospy.spin()

cd到“scripts”目录下,输入如下指令来给python文件添加可执行权限

chmod +x *.py

打开“CMakeLists.txt”,添加如下内容 

编译一下,然后输入如下指令测试

source ./devel/setup.bash
rosrun plumbing_server_client server_py.py

source ./devel/setup.bash
rosservice call AddTwoInts "a: 12 b: 14"

4.2.2 客户端

在“scripts”目录下新建一个python文件,这里命名为“client_py.py”

在“client_py.py”中添加如下代码

#! /usr/bin/env python
# -*- coding: utf-8 -*-

import rospy
from plumbing_server_client.srv import AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse

"""
客户端:组织并提交请求,处理服务端响应。
    1. 导包;
    2. 初始化 ROS 节点;
    3. 创建客户端对象;
    4. 组织请求数据,并发送请求;
    5. 处理响应。
"""

if __name__ == "__main__":
    # 2. 初始化 ROS 节点;
    rospy.init_node("client")

    # 3. 创建客户端对象;
    client = rospy.ServiceProxy("AddTwoInts", AddTwoInts)

    # 4. 组织请求数据,并发送请求;
    response = client.call(12, 24)

    # 5. 处理响应。
    rospy.loginfo("响应的数据:%d", response.sum)

给新建的python文件添加可执行权限 

打开“CMakeLists.txt”,添加如下内容

编译后通过如下命令启动服务端和客户端,可以看到客户端正常接收到了服务端响应的数据

rosrun plumbing_server_client server_py.py  //启动服务端

rosrun plumbing_server_client client_py.py  //启动客户端

4.2.3 客户端优化——动态传参

如果我们希望客户端的参数是通过命令动态传入的,可以对客户端代码做如下修改:

#! /usr/bin/env python
# -*- coding: utf-8 -*-

import rospy, sys
from plumbing_server_client.srv import AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse

"""
客户端:组织并提交请求,处理服务端响应。
    1. 导包;
    2. 初始化 ROS 节点;
    3. 创建客户端对象;
    4. 组织请求数据,并发送请求;
    5. 处理响应。
"""

if __name__ == "__main__":
    # 判断参数个数
    if len(sys.argv) != 3:
        rospy.logerr("传入参数个数有误...")
        sys.exit(1)

    # 2. 初始化 ROS 节点;
    rospy.init_node("client")

    # 3. 创建客户端对象;
    client = rospy.ServiceProxy("AddTwoInts", AddTwoInts)

    # 4. 组织请求数据,并发送请求;
    num1 = int(sys.argv[1])
    num2 = int(sys.argv[2])
    response = client.call(num1, num2)

    # 5. 处理响应。
    rospy.loginfo("响应的数据:%d", response.sum)

此时执行效果如下

4.2.4 客户端优化——等待服务端启动后再发起请求

如果在服务端未启动的情况下启动客户端,会抛出异常

为了解决这个问题,我们可以添加如下一行代码,这样客户端就可以等待服务端启动后再发起请求

client.wait_for_service()

# 或

rospy.wait_for_service("AddTwoInts")

网站公告

今日签到

点亮在社区的每一天
去签到