Services
ros-z implements ROS 2's service pattern with type-safe request-response communication over Zenoh. This enables synchronous, point-to-point interactions between nodes using a pull-based model for full control over request processing.
Services provide request-response communication for operations that need immediate feedback. Unlike topics, services are bidirectional and ensure a response for each request. ros-z uses a pull model that gives you explicit control over when to process requests.
Visual Flow
graph TD
A[ZContextBuilder] -->|configure| B[ZContext]
B -->|create| C[Client Node]
B -->|create| D[Server Node]
C -->|create_client| E[Service Client]
D -->|create_service| F[Service Server]
E -->|send_request| G[Service Call]
G -->|route| F
F -->|take_request| H[Request Handler]
H -->|send_response| G
G -->|deliver| E
E -->|take_response| I[Response Handler]
Key Features
| Feature | Description | Benefit |
|---|---|---|
| Type Safety | Strongly-typed service definitions with Rust structs | Compile-time error detection |
| Pull Model | Explicit control over request processing timing | Predictable concurrency and backpressure |
| Async/Blocking | Dual API for both paradigms | Flexible integration patterns |
| Request Tracking | Key-based request/response matching | Reliable message correlation |
Service Server Example
This example demonstrates a service server that adds two integers. The server waits for requests, processes them, and sends responses back to clients.
use ros_z::{Builder, Result, context::ZContext};
use ros_z_msgs::example_interfaces::{AddTwoIntsResponse, srv::AddTwoInts};
/// AddTwoInts server node that provides a service to add two integers
///
/// # Arguments
/// * `ctx` - The ROS-Z context
/// * `max_requests` - Optional maximum number of requests to handle. If None, handles requests indefinitely.
pub fn run_add_two_ints_server(ctx: ZContext, max_requests: Option<usize>) -> Result<()> {
// Create a node named "add_two_ints_server"
let node = ctx.create_node("add_two_ints_server").build()?;
// Create a service that will handle requests
let mut service = node.create_service::<AddTwoInts>("add_two_ints").build()?;
println!("AddTwoInts service server started, waiting for requests...");
let mut request_count = 0;
loop {
// Wait for a request
let (key, req) = service.take_request()?;
println!("Incoming request\na: {} b: {}", req.a, req.b);
// Compute the sum
let sum = req.a + req.b;
// Create the response
let resp = AddTwoIntsResponse { sum };
println!("Sending response: {}", resp.sum);
// Send the response
service.send_response(&resp, &key)?;
request_count += 1;
// Check if we've reached the max requests
if let Some(max) = max_requests
&& request_count >= max
{
break;
}
}
Ok(())
}
Key points:
- Pull Model: Uses
take_request()for explicit control over when to accept requests - Request Key: Each request has a unique key for matching responses
- Bounded Operation: Optional
max_requestsparameter for testing - Simple Processing: Demonstrates synchronous request handling
Running the server:
# Basic usage - runs indefinitely
cargo run --example demo_nodes_add_two_ints_server
# Handle 5 requests then exit
cargo run --example demo_nodes_add_two_ints_server -- --count 5
# Connect to specific Zenoh router
cargo run --example demo_nodes_add_two_ints_server -- --endpoint tcp/localhost:7447
Service Client Example
This example demonstrates a service client that sends addition requests to the server and displays the results.
use std::time::Duration;
use ros_z::{Builder, Result, context::ZContext};
use ros_z_msgs::example_interfaces::{AddTwoIntsRequest, srv::AddTwoInts};
/// AddTwoInts client node that calls the service to add two integers
///
/// # Arguments
/// * `ctx` - The ROS-Z context
/// * `a` - First number to add
/// * `b` - Second number to add
/// * `async_mode` - Whether to use async response waiting
pub fn run_add_two_ints_client(ctx: ZContext, a: i64, b: i64, async_mode: bool) -> Result<i64> {
// Create a node named "add_two_ints_client"
let node = ctx.create_node("add_two_ints_client").build()?;
// Create a client for the service
let client = node.create_client::<AddTwoInts>("add_two_ints").build()?;
println!(
"AddTwoInts service client started (mode: {})",
if async_mode { "async" } else { "sync" }
);
// Create the request
let req = AddTwoIntsRequest { a, b };
println!("Sending request: {} + {}", req.a, req.b);
// Wait for the response
let resp = if async_mode {
tokio::runtime::Runtime::new().unwrap().block_on(async {
client.send_request(&req).await?;
client.take_response_async().await
})?
} else {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async { client.send_request(&req).await })?;
client.take_response_timeout(Duration::from_secs(5))?
};
println!("Received response: {}", resp.sum);
Ok(resp.sum)
}
Key points:
- Async Support: Supports both blocking and async response patterns
- Timeout Handling: Uses
take_response_timeout()for reliable operation - Simple API: Send request, receive response, process result
- Type Safety: Request and response types are enforced at compile time
Running the client:
# Basic usage
cargo run --example demo_nodes_add_two_ints_client -- --a 10 --b 20
# Using async mode
cargo run --example demo_nodes_add_two_ints_client -- --a 5 --b 3 --async-mode
# Connect to specific Zenoh router
cargo run --example demo_nodes_add_two_ints_client -- --a 100 --b 200 --endpoint tcp/localhost:7447
Complete Service Workflow
To see services in action, you'll need to start a Zenoh router first:
Terminal 1 - Start Zenoh Router:
cargo run --example zenoh_router
Terminal 2 - Start Server:
cargo run --example demo_nodes_add_two_ints_server
Terminal 3 - Send Client Requests:
# Request 1
cargo run --example demo_nodes_add_two_ints_client -- --a 10 --b 20
# Request 2
cargo run --example demo_nodes_add_two_ints_client -- --a 100 --b 200
Each client request is processed immediately by the server, demonstrating synchronous request-response communication over Zenoh.
Service Server Patterns
Service servers in ros-z follow a pull model pattern, similar to subscribers. You explicitly receive requests when ready to process them, giving you full control over request handling timing and concurrency.
This pull-based approach is consistent with subscriber's recv() pattern, allowing you to control when work happens rather than having callbacks interrupt your flow.
Pattern 1: Blocking Request Handling
Best for: Simple synchronous service implementations
let mut service = node
.create_service::<ServiceType>("service_name")
.build()?;
loop {
let (key, request) = service.take_request()?;
let response = process_request(&request);
service.send_response(&response, &key)?;
}
Pattern 2: Async Request Handling
Best for: Services that need to await other operations
let mut service = node
.create_service::<ServiceType>("service_name")
.build()?;
loop {
let (key, request) = service.take_request_async().await?;
let response = async_process_request(&request).await;
service.send_response(&response, &key)?;
}
Why Pull Model?
| Aspect | Pull Model (take_request) | Push Model (callback) |
|---|---|---|
| Control | Explicit control over when to accept requests | Interrupts current work |
| Concurrency | Easy to reason about | Requires careful synchronization |
| Backpressure | Natural - slow processing slows acceptance | Can overwhelm if processing is slow |
| Consistency | Same pattern as subscriber recv() | Different pattern |
Service Client Patterns
Service clients send requests to servers and receive responses. Both blocking and async patterns are supported.
Pattern 1: Blocking Client
Best for: Simple synchronous request-response operations
let client = node
.create_client::<ServiceType>("service_name")
.build()?;
let request = create_request();
client.send_request(&request)?;
let response = client.take_response()?;
Pattern 2: Async Client
Best for: Integration with async codebases
let client = node
.create_client::<ServiceType>("service_name")
.build()?;
let request = create_request();
client.send_request(&request).await?;
let response = client.take_response_async().await?;
Match your client and server patterns for consistency. Use blocking patterns for simple scripts and async patterns when integrating with async runtimes like tokio.
ROS 2 Interoperability
ros-z services work seamlessly with ROS 2 C++ and Python nodes:
# List available services
ros2 service list
# Call ros-z service from ROS 2 CLI
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 42, b: 58}"
# Show service type
ros2 service type /add_two_ints
# Get service info
ros2 service info /add_two_ints
ros-z service servers and clients are fully compatible with ROS 2 via Zenoh bridge or rmw_zenoh, enabling cross-language service calls.
Resources
- Custom Messages - Defining and using custom service types
- Message Generation - Generating service definitions
- Actions - For long-running operations with feedback
Start with the examples above to understand the basic service workflow, then explore custom service types for domain-specific operations.