目錄
一、概念
二、何時使用服務
三、話題通信與服務通信的區別
四、案例
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++的coutROS_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");// 創建一個服務對象 srvplumbing_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.anum2 = request.bsum = num1 + num2response = AddTwoIntsResponse() # 將結果封裝進responseresponse.sum = sumrospy.loginfo("服務器收到:num1=%d, num2=%d, 響應:sum=%d", num1, num2, sum)return responseif __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.pysource ./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")