Writing a simple service and client (C++)
Goal: Create and run service and client nodes using C++.
Tutorial level: Beginner
Time: 20 minutes
Contents
Background
When nodes communicate using services, the node that sends a request for data is called the client node, and the one that responds to the request is the service node.
The structure of the request and response is determined by a .srv
file.
The example used here is a simple integer addition system; one node requests the sum of two integers, and the other responds with the result.
Prerequisites
In previous tutorials, you learned how to create a workspace and create a package.
Tasks
1 Create a package
Open a new terminal and source your ROS 2 installation so that ros2
commands will work.
Navigate into the dev_ws
directory created in a previous tutorial.
Recall that packages should be created in the src
directory, not the root of the workspace.
Navigate into dev_ws/src
and create a new package:
ros2 pkg create --build-type ament_cmake cpp_srvcli --dependencies rclcpp example_interfaces
Your terminal will return a message verifying the creation of your package cpp_srvcli
and all its necessary files and folders.
The --dependencies
argument will automatically add the necessary dependency lines to package.xml
and CMakeLists.txt
.
example_interfaces
is the package that includes the .srv file you will need to structure your requests and responses:
int64 a
int64 b
---
int64 sum
The first two lines are the parameters of the request, and below the dashes is the response.
1.1 Update package.xml
Because you used the --dependencies
option during package creation, you don’t have to manually add dependencies to package.xml
or CMakeLists.txt
.
As always, though, make sure to add the description, maintainer email and name, and license information to package.xml
.
<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
2 Write the service node
Inside the dev_ws/src/cpp_srvcli/src
directory, create a new file called add_two_ints_server.cpp
and paste the following code within:
#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();
}
2.1 Examine the code
The first two #include
statements are your package dependencies.
The add
function adds two integers from the request and gives the sum to the response, while notifying the console of its status using logs.
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);
}
The main
function accomplishes the following, line by line:
Initializes ROS 2 C++ client library:
rclcpp::init(argc, argv);
Creates a node named
add_two_ints_server
:std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");
Creates a service named
add_two_ints
for that node and automatically advertises it over the networks with the&add
method:rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service = node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);
Prints a log message when it’s ready:
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");
Spins the node, making the service available.
rclcpp::spin(node);
2.2 Add executable
The add_executable
macro generates an executable you can run using ros2 run
.
Add the following code block to CMakeLists.txt
to create an executable named server
:
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
rclcpp example_interfaces)
So ros2 run
can find the executable, add the following lines to the end of the file, right before ament_package()
:
install(TARGETS
server
DESTINATION lib/${PROJECT_NAME})
You could build your package now, source the local setup files, and run it, but let’s create the client node first so you can see the full system at work.
3 Write the client node
Inside the dev_ws/src/cpp_srvcli/src
directory, create a new file called add_two_ints_client.cpp
and paste the following code within:
#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.1 Examine the code
Similar to the service node, the following lines of code create the node and then create the client for that node:
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");
Next, the request is created.
Its structure is defined by the .srv
file mentioned earlier.
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
The while
loop gives the client 1 second to search for service nodes in the network.
If it can’t find any, it will continue waiting.
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
If the client is canceled (e.g. by you entering Ctrl+C
into the terminal), it will return an error log message stating it was interrupted.
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
Then the client sends its request, and the node spins until it receives its response, or fails.
3.2 Add executable
Return to CMakeLists.txt
to add the executable and target for the new node.
After removing some unnecessary boilerplate from the automatically generated file, your CMakeLists.txt
should look like this:
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()
4 Build and run
It’s good practice to run rosdep
in the root of your workspace (dev_ws
) to check for missing dependencies before building:
rosdep install -i --from-path src --rosdistro galactic -y
rosdep only runs on Linux, so you can skip ahead to next step.
rosdep only runs on Linux, so you can skip ahead to next step.
Navigate back to the root of your workspace, dev_ws
, and build your new package:
colcon build --packages-select cpp_srvcli
colcon build --packages-select cpp_srvcli
colcon build –merge-install –packages-select cpp_srvcli
Open a new terminal, navigate to dev_ws
, and source the setup files:
. install/setup.bash
. install/setup.bash
call install/setup.bat
Now run the service node:
ros2 run cpp_srvcli server
The terminal should return the following message, and then wait:
[INFO] [rclcpp]: Ready to add two ints.
Open another terminal, source the setup files from inside dev_ws
again.
Start the client node, followed by any two integers separated by a space:
ros2 run cpp_srvcli client 2 3
If you chose 2
and 3
, for example, the client would receive a response like this:
[INFO] [rclcpp]: Sum: 5
Return to the terminal where your service node is running. You will see that it published log messages when it received the request and the data it received, and the response it sent back:
[INFO] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [rclcpp]: sending back response: [5]
Enter Ctrl+C
in the server terminal to stop the node from spinning.
Summary
You created two nodes to request and respond to data over a service. You added their dependencies and executables to the package configuration files so that you could build and run them, and see a service/client system at work
Next steps
In the last few tutorials you’ve been utilizing interfaces to pass data across topics and services. Next, you’ll learn how to create custom interfaces.