概要:这篇主要介绍c++版本服务端和客户端的编写

环境:ubuntu20.04,ros2-foxy,vscode

最后如果没有陈述实操过程中碰到问题的话,则表示该章节都可被本人正常复现.

2.2.5编写一个简单的服务器和客户端(C++)(原文:https://docs.ros.org/en/foxy/Tutorials/Writing-A-Simple-Cpp-Service-And-Client.html

>>教程>>编写一个简单的服务器和客户端(C++)

你正阅读的是ros2较老版本(Foxy),但仍然支持的说明文档.想查看最新版本的信息,请看galactic版本链接( https://docs.ros.org/en/galactic/Tutorials.html

编写一个简单的服务器和客户端(C++)

目标:使用c++构建和运行一个服务器节点和一个客户端节点

课程等级:初级

时长:20min

目录

1.背景
2.预备知识
3.步骤
3.1创建一个包
3.2编写服务器节点
3.3编写客户端节点
3.4编译和运行
4.总结
5.下一步
6.相关内容

1.背景

当节点使用服务来通信时,发送请求信息的节点叫客户端节点,响应请求的节点叫服务器节点.请求-响应结构由.srv文件决定.

这里使用的案例是一个简单的整数加法系统,一个节点请求两个整数之和,另外节点响应结果.

2.预备知识

在前面课程中,你学习了如何创建工作空间以及包.

3.步骤

3.1创建一个包

新开一个终端并source一下ros2环境变量以保证ros2指令都是没有问题的.

进入前面课程创建的dev_ws工作空间目录

回忆一下,包是应该创建在src目录下的,而不是工作空间根目录的.进入dev_ws/src目录并且创建一个新包:

ros2 pkg create --build-type ament_cmake cpp_srvcli --dependencies rclcpp example_interfaces

你的终端会返回信息,表明你的包cpp_srvcli以及它所有的必须文件和文件夹都创建完成.

这个--dependencies参数自动添加到package.xmlCMakeLists.txt的必要的依赖(标签)中.

包里面的.srv.file文件,你需要利用example_interfaces在里面构建你的请求和响应.

int64 a
int64 b
---
int64 sum

上面两行是请求的参数,虚线下面是响应.

3.1.1更新package.xml文件

由于包创建时使用了--dependencies,你不必手动添加依赖到package.xml或者CMakeLists.txt里面

老规矩,确保package.xml里面description, maintainer email namelicense信息都填写完成.

<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>

3.2编写服务器节点

进入dev_ws/src/cpp_srvcli/src目录,创建名字为add_two_ints_server.cpp源文件,复制一下代码到里面:

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <memory>

void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
          std::shared_ptr<example_interfaces::srv::AddTwoInts::Response>      response)
{
  response->sum = request->a + request->b;
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
                request->a, request->b);
  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");

  rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
    node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);

  RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");

  rclcpp::spin(node);
  rclcpp::shutdown();
}
 

3.2.1代码解析

首先两个#include声明是你的包的依赖.

add函数接受自请求的两个整数,并且返回求和计算结果,控制台会以日志展示函数状态.

void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
         std::shared_ptr<example_interfaces::srv::AddTwoInts::Response>      response)
{
    response->sum = request->a + request->b;
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
        request->a, request->b);
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

main主函数实现以下功能,一行接一行:

*初始后ros2客户端库

rclcpp::init(argc, argv);

*创建名为add_two_ints_server的节点

std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");

*为上面节点创建add_two_ints服务器,并且使用&add方式,自动将其放到(系统)网络里面广播.

rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);

当其准备好时会打印日志信息:

RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");

运转节点使得服务可用

rclcpp::spin(node);

3.2.2 添加executable

使用ros2 run 指令,add_executable宏表示会生成一个可执行文件.添加下面代码块到CMakeLists.txt文件里面,生成一个叫server的服务.

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
rclcpp example_interfaces)

把下面几行代码放到CMakeLists.txt文件底部但在ament_package()上面,ros2 run就可以找到这个可执行文件:

install(TARGETS
  server
  DESTINATION lib/${PROJECT_NAME})

现在可以编译你的包了,source一下本地配置文件,然后运行它.让我们首先创建一个客户端,这样子你就可以看见完整运转的系统了.

3.3编写客户端节点

进入dev_ws/src/cpp_srvcli/src目录,新建一个名为add_two_ints_client.cpp文件,把下面代码放到里面:

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  if (argc != 3) {
      RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");
      return 1;
  }

  std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
  rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
    node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

  auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
  request->a = atoll(argv[1]);
  request->b = atoll(argv[2]);

  while (!client->wait_for_service(1s)) {
    if (!rclcpp::ok()) {
      RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
      return 0;
    }
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
  }

  auto result = client->async_send_request(request);
  // Wait for the result.
  if (rclcpp::spin_until_future_complete(node, result) ==
    rclcpp::FutureReturnCode::SUCCESS)
  {
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
  } else {
    RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
  }

  rclcpp::shutdown();
  return 0;
}

3.3.1代码解析

类似服务器节点,下面几行代码创建一个节点,然后创建该节点对应的客户端:

std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
  node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

下一步是请求生成.它的结构样式是在前面提及的.srv文件里面:

auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);

while循环给客户1秒时间,从网络中查找服务器节点.如果找不到,它会继续等待:

RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");

如果客户取消了(例如你输入ctrl+c到终端里面),它会返回(表示)错误日志信息并开始中断:

RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
  return 0;

然后,客户发送请求,节点循环运转直到收到响应或者(运行)失败.

3.3.2添加executable

回到CMakeLists.txt,为新节点添加所需的executabletarget内容. 删除自动生成样板不必要内容之后,你的CMakeLists.txt文件长这样子:

cmake_minimum_required(VERSION 3.5)
project(cpp_srvcli)

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
  rclcpp example_interfaces)

add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client
  rclcpp example_interfaces)

install(TARGETS
  server
  client
  DESTINATION lib/${PROJECT_NAME})

ament_package()
 

3.4编译和运行

编译前,在你的工作空间(dev_ws)运行rosdep检查所缺依赖是很nice的练习.

linux:

rosdep install -i --from-path src --rosdistro foxy -y

定位到dev_ws工作空间根目录,编译你的工作空间:

colcon build --packages-select cpp_srvcli

新开终端,source一下配置文件:

linux:

. install/setup.bash

然后运行服务器节点:

ros2 run cpp_srvcli server

然后等待一下,终端应该返回一下信息:

[INFO] [rclcpp]: Ready to add two ints.

新开一个终端,在dev_ws再次source一下配置信息(环境变量).然后开启客户端节点,两个整数要用空格分开:

ros2 run cpp_srvcli client 2 3

举例,如果你选择2,3,那么客户端会收到如下响应:

[INFO] [rclcpp]: Sum: 5

返回服务器节点运行的终端,当它收到请求以及数据信息,它会打印日志信息,也会返回响应:

[INFO] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [rclcpp]: sending back response: [5]

服务器终端输入ctrl+c,停止节点的持续运行.

4.总结

你创建了两个节点来请求和响应整个服务过程的数据.你添加dependenciesexecutables到包配置文件里面,所以你才可以编译和运行这包,并且看到服务端/用户端系统运转.

5.下一步

下面一些课程,你将会(学习)初始化端口来通过话题和服务传输数据.接下来,你会学习如何创建定制化端口.

6.相关内容

有几种c++编写方式供你选择用来服务器和客户端的.可以在这里(https://github.com/ros2/examples/tree/foxy/rclcpp/services)检查包里面minimal_serviceminimal_client文件.

其他

个人认为重点:

服务器和客户端节点代码句子内容理解;相应配置文件的内容添加.

这课程是在等毕业证那十几天搞的,室友问,现在在线翻译这么强大,为啥还在这里瞎折腾呢?我说,我的目地是好好认真看一下,了解一下,自己折腾,目前是我想到最好的办法来获得最佳效果,即使这翻译有点别扭,哈哈哈.

#####################
不积硅步,无以至千里
好记性不如烂笔头
感觉有点收获的话,麻烦大大们点赞收藏哈