Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Trying ROS2: client/server within a single container
pikoTutorial
pikoTutorial

Posted on • Originally published atpikotutorial.com

Trying ROS2: client/server within a single container

Welcome to the nextpikoTutorial !

In the first article of Trying ROS2 series I showed how to implement and examine a basic publisher and subscriber within a single Docker container. Now it's time for checking out how do you implement and set up a client/server connection within a single Docker container. In this pikoTutorial you'll read about:

  • setting up a typical ROS server and client written in C++ using ROS 2
  • defining a custom service with .srv file
  • building everything as a Docker image
  • running both nodes within a single container using a launch file which now can be written in Python

Project structure

Let's first define a project structure to understand what am I aiming at:

project├── launch    ├── run_server_client.py├── src    ├── cpp_client        ├── CMakeLists.txt        ├── main.cpp        ├── package.xml    ├── cpp_server        ├── CMakeLists.txt        ├── main.cpp        ├── package.xml    ├── interfaces        ├── srv            ├── GetDataSize.srv        ├── CMakeLists.txt        ├── package.xml├── Dockerfile├── ros_entrypoint.sh
Enter fullscreen modeExit fullscreen mode

Create packages

I will put server, client and the service definitions into separate ROS packages, so let's first create them using the following commands:

ros2 pkg create--build-type ament_cmake cpp_clientros2 pkg create--build-type ament_cmake cpp_serverros2 pkg create--build-type ament_cmake interfaces
Enter fullscreen modeExit fullscreen mode

Defining a custom service type

To define a custom service type calledGetDataSize, I create aGetDataSize.srv file insrc/interfaces/srv folder. Remember that services definitionsmust be placed insrv folder and their namesmust be camel case. Otherwise, you'll get the following build error:

rosidl_adapter.parser.InvalidResourceName: 'get_data_size_Request' is aninvalid message name.  It should have the pattern '^[A-Z][A-Za-z0-9]*$'
Enter fullscreen modeExit fullscreen mode

Thesrc/interfaces/srv/GetDataSize.srv file looks like below:

string dataset_name---int32 dataset_size
Enter fullscreen modeExit fullscreen mode

To generate a C++ code out of such service definition, we need to userosidl_generate_interfaces function insrc/interfaces/CMakeLists.txt file:

cmake_minimum_required(VERSION 3.8)project(interfaces)find_package(ament_cmake REQUIRED)find_package(rosidl_default_generators REQUIRED)rosidl_generate_interfaces(${PROJECT_NAME}"srv/GetDataSize.msg")ament_package()
Enter fullscreen modeExit fullscreen mode

And add proper entries to thesrc/interfaces/package.xml file:

<buildtool_depend>rosidl_default_generators</buildtool_depend><exec_depend>rosidl_default_runtime</exec_depend><member_of_group>rosidl_interface_packages</member_of_group>
Enter fullscreen modeExit fullscreen mode

Writing a C++ ROS2 client

Below you can find the implementation of the C++ client using ROS2. Below is the content of thesrc/cpp_client/main.cpp file.

// ROS client library for C++ header#include"rclcpp/rclcpp.hpp"// GetDataSize service header#include"interfaces/srv/get_data_size.hpp"// aliasesusingGetDataSizeService=interfaces::srv::GetDataSize;usingGetDataSizeRequest=GetDataSizeService::Request;usingGetDataSizeRequestPtr=std::shared_ptr<GetDataSizeService::Request>;usingGetDataSizeResponsePtr=std::shared_ptr<GetDataSizeService::Response>;usingnamespacestd::chrono_literals;// constantsstaticconstexprconstchar*kNodeName{"cpp_client"};staticconstexprconstchar*kServiceName{"GetDataSize"};// client classclassCppClient:publicrclcpp::Node{public:CppClient():Node(kNodeName),client_{create_client<GetDataSizeService>(kServiceName)}{// wait for the service in 1 second intervalswhile(!client_->wait_for_service(1s)){std::cout<<"[CLIENT] Waiting for service..."<<std::endl;}std::cout<<"[CLIENT] GetDataSizeService service found!"<<std::endl;}// function responsible for sending requestvoidSendRequest(conststd::stringdataset_name){std::cout<<"[CLIENT] Requesting size of dataset "<<dataset_name<<std::endl;// create a requestGetDataSizeRequestPtrrequest=std::make_shared<GetDataSizeRequest>();request->dataset_name=dataset_name;// obtain the responseautoresponse_future=client_->async_send_request(request);constautoresult=rclcpp::spin_until_future_complete(this->get_node_base_interface(),response_future);// check validity of the resultif(result!=rclcpp::FutureReturnCode::SUCCESS){std::cout<<"[CLIENT] Request sending failed!"<<std::endl;client_->remove_pending_request(response_future);return;}// read the response from the future objectconstGetDataSizeResponsePtrresponse=response_future.get();// print the response payloadstd::cout<<"[CLIENT] Response received: "<<response->dataset_size<<std::endl;}private:rclcpp::Client<GetDataSizeService>::SharedPtrclient_;};// main functionintmain(intargc,char*argv[]){rclcpp::init(argc,argv);std::shared_ptr<CppClient>node=std::make_shared<CppClient>();// send 3 different requestsnode->SendRequest("Lakes");node->SendRequest("Oceans");node->SendRequest("Rivers");rclcpp::shutdown();}
Enter fullscreen modeExit fullscreen mode

I keptsrc/cpp_client/CMakeLists.txt file in its most minimalistic form, so it contains the executable depending on our previously definedinterfaces package:

cmake_minimum_required(VERSION 3.5)project(cpp_client)find_package(ament_cmake REQUIRED)find_package(rclcpp REQUIRED)find_package(interfaces REQUIRED)add_executable(${PROJECT_NAME} main.cpp)ament_target_dependencies(${PROJECT_NAME}    rclcpp    interfaces)install(TARGETS${PROJECT_NAME}        DESTINATION lib/${PROJECT_NAME})ament_package()
Enter fullscreen modeExit fullscreen mode

We must also remember about addinginterfaces dependency to thesrc/cpp_client/package.xml file. Otherwise, the dependency build order will not be preserved andcolcon may try to build e.g.cpp_client package beforeinterfaces package (on which cpp_client depends) has been built.

<depend>interfaces</depend>
Enter fullscreen modeExit fullscreen mode

Writing a C++ ROS2 server

Now it's time to createsrc/cpp_server/main.cpp file:

// ROS client library for C++ header#include"rclcpp/rclcpp.hpp"#include<iomanip>// GetDataSize service header#include"interfaces/srv/get_data_size.hpp"// aliasesusingGetDataSizeService=interfaces::srv::GetDataSize;usingRequestHeaderPtr=std::shared_ptr<rmw_request_id_t>;usingGetDataSizeRequestPtr=std::shared_ptr<GetDataSizeService::Request>;usingGetDataSizeResponsePtr=std::shared_ptr<GetDataSizeService::Response>;// constantsstaticconstexprconstchar*kNodeName{"cpp_server"};staticconstexprconstchar*kServiceName{"GetDataSize"};// overloaded stream operator for easy request header printingstd::ostream&operator<<(std::ostream&os,constRequestHeaderPtr&request_header){os<<"Request number "<<request_header->sequence_number<<" from ";std::copy(std::begin(request_header->writer_guid),std::end(request_header->writer_guid),std::ostream_iterator<int>(os," "));returnos;}// mock datastaticconststd::unordered_map<std::string,std::vector<int>>kData={{"Lakes",{1,2,3}},{"Oceans",{1,2,3,4,5}},{"Rivers",{1,2,3,4,5,6,7}},};// server classclassCppServer:publicrclcpp::Node{public:CppServer():Node(kNodeName){// create a service and provide a callback which will be called upon// receiving a requestservice_=create_service<GetDataSizeService>(kServiceName,[this](RequestHeaderPtrrequest_header,GetDataSizeRequestPtrrequest,GetDataSizeResponsePtrresponse){HandleGetDataSizeRequest(request_header,request,response);});std::cout<<"[SERVER] Server up and running"<<std::endl;}private:voidHandleGetDataSizeRequest(RequestHeaderPtrrequest_header,GetDataSizeRequestPtrrequest,GetDataSizeResponsePtrresponse){std::cout<<"[SERVER] Received: "<<request_header<<std::endl;// populate the response with the proper data sizeresponse->dataset_size=kData.at(request->dataset_name).size();}rclcpp::Service<GetDataSizeService>::SharedPtrservice_;};// main functionintmain(intargc,char*argv[]){rclcpp::init(argc,argv);std::shared_ptr<CppServer>node=std::make_shared<CppServer>();rclcpp::spin(node);rclcpp::shutdown();}
Enter fullscreen modeExit fullscreen mode

The implementations ofsrc/cpp_server/CMakeLists.txt andsrc/cpp_server/package.xml files are analogous to cpp_client, so I'll skip writing about them here.

Writing a ROS2 launch file

After we have client and server ready to run, it's time to define a Python launch filelaunch/run_server_client.py :

fromlaunchimportLaunchDescriptionfromlaunch_ros.actionsimportNodedefgenerate_launch_description():returnLaunchDescription([Node(package='cpp_server',executable='cpp_server',output='screen',),Node(package='cpp_client',executable='cpp_client',output='screen',),])
Enter fullscreen modeExit fullscreen mode

This file can be provided toros2 launch command, but because we will run it inside the docker container environment which will require sourcing the ROS underlay and the workspace overlay anyway, let's put all of these commands to aros_entrypoint.sh file:

#!/bin/bashset-esource /opt/ros/rolling/setup.bashsource /app/src/install/setup.bashros2 launch launch/run_server_client.py
Enter fullscreen modeExit fullscreen mode

If you want to learn more about the ROS underlays and overlays, you can read more about them inthis pikoTutorial.

Alternatively, if you don't want to useros2 launch, this script could look like this:

#!/bin/bashset-esource /opt/ros/rolling/setup.bashsource /app/src/install/setup.bashros2 run cpp_server cpp_server &ros2 run cpp_client cpp_client
Enter fullscreen modeExit fullscreen mode

Writing a Dockerfile

To create a Docker image, I'll useosrf/ros:rolling-desktop as a base image. It's because on my Ubuntu 24.04 I use Rolling Ridley ROS 2 distro, so I want my container environment to be the same as my local one.

# use ROS Rolling Ridley image as the baseFROM osrf/ros:rolling-desktop# specify bash as shell to use source commandSHELL ["/bin/bash", "-c"]# change working directory to /appWORKDIR /app# copy all the ROS workspace filesCOPY src src/COPY launch launch/COPY ros_entrypoint.sh /app# build workspaceRUNcdsrc&&\source /opt/ros/rolling/setup.bash&&\    colcon build# assign permission to the entrypoint scriptRUNchmod +x /app/ros_entrypoint.sh# run entrypoint scriptENTRYPOINT ["/app/ros_entrypoint.sh"]
Enter fullscreen modeExit fullscreen mode

Putting all together

At this point we have all the parts, but before we start working with the container, let's check locally if the workspace build at all with the following command:

source /opt/ros/rolling/setup.bashcolcon build
Enter fullscreen modeExit fullscreen mode

If everything succeeded, you should see the output similar to:

Starting >>> interfacesFinished <<< interfaces [6.45s]                     Starting >>> cpp_clientStarting >>> cpp_serverFinished <<< cpp_server [15.2s]                                       Finished <<< cpp_client [15.4s]                    Summary: 3 packages finished [22.2s]
Enter fullscreen modeExit fullscreen mode

Now as we know that ROS workspace can be successfully built, it's time to build a docker image:

docker build-t ros_server_client.
Enter fullscreen modeExit fullscreen mode

Note for beginners: if you're struggling with missing Docker permissions, checkthis pikoTutorial out.

To run the container basing on this image, run:

docker run--rm--name server_client ros_server_client
Enter fullscreen modeExit fullscreen mode

Examining nodes' behavior inside the container

After running the container, you will see the following logs:

[SERVER] Server up and running[CLIENT] GetDataSizeService service found![CLIENT] Requesting size of dataset Lakes[SERVER] Received: Request number 1 from 1 15 235 125 64 0 173 57 0 0 0 0 0 0 20 4 [CLIENT] Response received: 3[CLIENT] Requesting size of dataset Oceans[SERVER] Received: Request number 2 from 1 15 235 125 64 0 173 57 0 0 0 0 0 0 20 4 [CLIENT] Response received: 5[CLIENT] Requesting size of dataset Rivers[SERVER] Received: Request number 3 from 1 15 235 125 64 0 173 57 0 0 0 0 0 0 20 4 [CLIENT] Response received: 7
Enter fullscreen modeExit fullscreen mode

You may want to examine further what's going on in the container, so let's go inside our container using:

docker containerexec-it server_client /bin/bash
Enter fullscreen modeExit fullscreen mode

The first thing to check is whether our serviceGetDataSize is on the list of active services:

ros2 service list
Enter fullscreen modeExit fullscreen mode

This should give the output in which one of the entries represents our service:

/GetDataSize
Enter fullscreen modeExit fullscreen mode

Let's then check the properties of that service:

ros2 service info /GetDataSize
Enter fullscreen modeExit fullscreen mode

As expected, we see that there is 1 server and 0 clients because after sending 3 requests, the client implemented above exits:

Type: interfaces/srv/GetDataSizeClients count: 0Services count: 1
Enter fullscreen modeExit fullscreen mode

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Inspired by the SI unit prefix “pico” (10^-12), this place aims to deliver short and targeted tutorials which can speed up work of every software engineer.
  • Joined

More frompikoTutorial

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp