본문 바로가기

IT/ROS

[ROS] 12. 서버-클라이언트 서비스 통신

반응형

☞ 메인보드 : Jetson Nano Developer Kit
☞ 운영 체제 : Ubuntu 18.04 - JetPack 4.4.1
☞ ROS 버전 : Melodic
☞ IDE : Visual Studio Code
☞ 언어 : C++




목차

1. 패키지 생성
2. 서버 코드 작성[server.cpp]
3. 클라이언트 작성[client.cpp]
4. XML, CMAKE 파일 작성 + 서비스 파일 작성
5. 실행 결과

 




ROS는 병렬 작업에 최적화된 통신을 제공한다. 일반적으로 ROS 사용자는 다양한 센서와 부품들을 사용하여 로봇을 만들고자 하는데 이때 각 단위로 세부 제어를 할 수 있도록 하는 것은 ROS가 노드화 되어 있기 때문이다. 노드는 네트워크 통신의 노드를 생각해도 좋고, 구조 해석에 사용되는 유한요소의 노드를 생각해도 좋다. 결국 노드라는 것은 기능을 가진 점으로써 다른 점들과 연결되어 상호작용을 하는 점이라고 할 수 있다.

노드를 한 두 개로 만들어서 사용해도 상관은 없지만, 모든 기능을 한 노드가 수행하도록 때려 박지는 않는다. 센서나 구별해야 하는 기능을 기준으로 생성하면 각각의 노드에 충돌이나 결함이 생기더라도 대처하기가 수월해지기 때문이다. 세분화한 코드의 복잡함을 줄이는 대신 각 노드의 기능에 맞는 통신 방법을 채택하게 된다.

ROS 통신 방법에는 퍼블리셔-서브스크라이버의 메시지 통신 이외에도 서버-클라이언트 서비스 통신과 액션 통신이 있다. 이전에 10번째로 포스팅한 TTL to RS485는 서비스 통신으로 제어했었던 거 같은데, 다시금 새로운 서비스 통신을 코드와 함께 순차적으로 작성해보려 한다.


이전에 작성했던 퍼블리셔-서브스크라이버 메시지 통신 포스트이다. 사실 통신에 관련된 글은 더 일찍 올려야 하는 게 맞았다. 라이다 센서나 뎁스 카메라를 사용할 때 메시지든 서비스든 통신에 대한 것을 숙지했다는 가정하에 글을 작성했기 때문에 순서가 뒤바뀌긴 했으나 늦었을 때가 빠를 때라는 옛 말이 있기에 지금이라도 작성했다.

 

[ROS] 4. 퍼블리셔-서브스크라이버 메세지 통신

☞ 메인보드 : Jetson Nano Developer Kit ☞ 운영 체제 : Ubuntu 18.04 - JetPack 4.3 ☞ ROS 버전 : Melodic ☞ 언어 : C++ <이전 포스트> ROS Melodic <이전 포스트> 2. Jetson Nano 보드에 Ubuntu 18.04 설치..

95mkr.tistory.com



메시지 통신이 같은 주제(topic)로 대화를 나누는 행위로 보면, 서비스 통신은 요청(request)과 응답(response)을 주고받는 행위로 볼 수 있다. 서비스 서버는 서비스 클라이언트의 요청에 대응하는 데이터를 미리 저장해놓은 뒤 응답한다. 메시지 통신의 경우 std_msgs의 타입을 사용하거나 msg라는 디렉터리를 생성하여 사용자 메시지 변수를 선언할 수 있는데, 이와 마찬가지로 서비스 통신 또한 요청과 응답에 대한 타입을 srv라는 디렉터리 안에 만들어 사용할 수 있다. 단, msg나 srv 디렉터리를 만들어 사용하는 사용자 메시지 통신을 하는 경우에는 소스코드에 헤더 파일을 선언해줘야 한다.



 

① 패키지 생성

 

# 패키지 생성
cd ~/catkin_ws/src 
catkin_create_pkg service_node roscpp std_msgs message_generation 

# catkin_make를 위해 ~/catkin_ws 디렉터리로 이동 
cd .. catkin_make


ROS 4번째 포스트인 메시지 통신에서 의존성으로 message_generation을 빠뜨리고 작성하는 바람에 글을 읽으신 분들이 무수한 삽질을 할 수 있도록 도와주었다... 포스트를 작성할 때는 추가를 하고 진행을 했기 때문에 에러가 나지 않았는데 작업 중간에 캡처한 사진이 올라갔던 것을 다시 읽어본 후에 확인했다. 제성함니다...

패키지를 생성할 때 message_generation을 의존성으로 등록하고 시작하면 CMakeLists.txt 파일의 find_package 안에 포함이 되어 생성된다. 메시지 통신을 할 때는 find_package 아래에 generate_messages를 함께 작성해야 한다는 것만 기억하자.




② 서버 코드 작성[server.cpp]

 

#include "ros/ros.h"
#include "service_node/Service.h" 

class SrvSvr
{ 
private: 
	ros::NodeHandle n_; 
	ros::ServiceServer svr_; 
	
public: 
	SrvSvr() 
	{ 
		svr_ = n_.advertiseService("service", func);
	} 
	
	static bool func(service_node::Service::Request &req, service_node::Service::Response &res) 
	{
		switch(req.num) 
		{ 
			case 1: 
				res.ans = "Hello"; 
				break; 
			
			case 2: 
				res.ans = "ROS"; 
				break; 
				
			case 3: 
				res.ans = "World!";
				break;
			
		}
		ROS_INFO("[MESSAGE] Select Number: %d ", req.num); 
		ROS_INFO_STREAM("[MESSAGE] " << res.ans); 
		return true;
	} 
}; 

int main(int argc, char **argv) 
{ 
	ros::init(argc, argv, "service_server"); 
	SrvSvr s_; 
	ROS_INFO("SERVICE NODE TEST"); 
	ros::spin(); 
	return 0; 
}




 

※ 코드 분석


이전에 작성했던 포스트와는 다른 구조의 코드 작성법이다. 서비스 콜백 함수로써 작용하는 bool 타입의 func 함수를 전역으로 선언하지 않은 이유는 ROS 위키에서 전역 선언을 추천하지 않기 때문이다. 코드 작성법을 들여다보면 변수, 상수, 함수 등 선언에 알맞은 대문자, 소문자, 언더 바(_)의 쓰임새를 제시한다.

사실 이런 간단한 기능의 코드는 이전과 같은 방식으로 코드를 작성해도 상관이 없으나 예제 작성이 아닌 협업을 위한 직관적인 코드를 작성하기 위해서 연습해보면 좋을 것 같다.

 

#include "ros/ros.h"
#include "service_node/Service.h"

 


service_node/Service.h 헤더 선언은 서비스 디렉터리(srv) 안에 Service.srv 파일을 생성하고 catkin_make를 실행한 후에 인식이 된다. 서비스 통신에 필요한 Request, Response 처리를 위해서 꼭 선언해야 한다.

 

class SrvSvr
{ 
private: 
	ros::NodeHandle n_; 
	ros::ServiceServer svr_; 
	
public: 
	SrvSvr() 
	{ 
		svr_ = n_.advertiseService("service", func);
	}



클래스 SrvSvr는 Service Server를 의미한다. 의미를 모르겠거나 간단해 보이면 의미에 맞게 네이밍 하길 바란다. 접근 제어 지시자인 private와 public 에는 각각 멤버 변수와 멤버 함수를 정의하게 되는데, NodeHandle은 노드 초기화를 위해 사용하며 필요에 따라 매개변수를 추가하여 네임스페이스를 지정할 수도 있다.

ServiceServer는. srv 파일에 정의된 응답 타입에 맞게 클라이언트로부터의 응답을 수행하기 위해서 선언한다. public에 선언된 SrvSvr 생성자 안에 advertiseService 메소드는 첫 번째 자리의 매개변수인 "service"라는 이름(문자열)의 서비스를 생성하는데 이는 두 번째 매개변수에 입력한 func에 정의한 절차에 맞게 응답하게 된다.

당연한 말이겠지만 서버는 서비스에 대한 응답을 하고 클라이언트는 서비스 요청을 하기 때문에 서버의 advertiseService와 클라이언트의 serviceClient에서의 서비스 이름은 같아야 한다.

 

static bool func(service_node::Service::Request &req, service_node::Service::Response &res) 
	{
		switch(req.num) 
		{ 
			case 1: 
				res.ans = "Hello"; 
				break; 
			
			case 2: 
				res.ans = "ROS"; 
				break; 
				
			case 3: 
				res.ans = "World!";
				break;
			
		}
		ROS_INFO("[MESSAGE] Select Number: %d ", req.num); 
		ROS_INFO_STREAM("[MESSAGE] " << res.ans); 
		return true;
	} 
};



콜백 함수로써 작용하는 func 함수는 true나 false를 반환하는 bool 타입의 변수로 선언한다. 이는 클라이언트로부터 어떤 서비스를 요청받을지 나열하는 코드가 들어간다. 위 코드는 간단하게 1, 2, 3 숫자를 요청받을 경우를 나눠 Hello, ROS, World! 문자열로 응답하는 내용이다. 여기서 등장하는 service_node::Service의 Request, Response는 작성할 서비스 파일에 정의된 변수들을 의미한다. 클라이언트 코드에서도 똑같이 사용한다.

ROS_INFO를 사용하면 노드를 실행하고 어떤 내용들이 오고 갔는지 확인할 수 있다. 만약 문자열로 응답하는 경우에는 ROS_INFO_STREAM을 사용하면 된다. [cout << 변수 or "문자열" << endl과 같은 입출력 방식으로 사용할 수 있다.]

위 내용들이 제대로 수행됐을 경우에 true를 반환하고 함수가 종료된다.

 

int main(int argc, char **argv) 
{ 
	ros::init(argc, argv, "service_server"); 
	SrvSvr s_; 
	ROS_INFO("SERVICE NODE TEST"); 
	ros::spin(); 
	return 0; 
}

 


메인 함수에선 roscpp 관련 함수를 호출하기 전에 init을 가장 먼저 호출해 노드를 초기화해야 한다. 이 과정으로 "service_server"라는 노드를 마스터에 등록할 수 있다.


③ 클라이언트 코드 작성[client.cpp]

 

#include "ros/ros.h" 
#include "service_node/Service.h" 

class SrvClt 
{ 
private: 
	ros::NodeHandle n_; 
	ros::ServiceClient clt_; 
	
public: 
	SrvClt() 
	{ 
		clt_ = n_.serviceClient<service_node::Service>("service"); 
	} 
	
	void clientCall(service_node::Service &srv) 
	{ 
		clt_.call(srv); 
	} 
}; 

int main(int argc, char **argv) 
{ 
	ros::init(argc, argv, "service_client"); 
	SrvClt c_; 
	service_node::Service srv; 
	
	for(int i=1; i < 4; i++) {	
		srv.request.num = i;
		c_.clientCall(srv);
		ROS_INFO("SEND THE NUMBER: %d", i);
	} 
	return 0; 
}



 

 

※ 코드 분석

 

#include "ros/ros.h" 
#include "service_node/Service.h" 

class SrvClt 
{ 
private: 
	ros::NodeHandle n_; 
	ros::ServiceClient clt_; 
	
public: 
	SrvClt() 
	{ 
		clt_ = n_.serviceClient<service_node::Service>("service"); 
	} 
	
	void clientCall(service_node::Service &srv) 
	{ 
		clt_.call(srv); 
	} 
};



서버 코드와 마찬가지로 SrvClt(Service Client) 클래스를 생성하고 그 안에 NodeHandle과 ServiceClient를 각각 선언한다. clt_ = n_serviceClient 선언에서 <> 괄호 사이에는 <프로젝트 이름::서비스 파일 이름>의 형식으로 작성해야만 해당 서비스 파일에 정의된 내용에 맞게 요청을 할 수 있다. 메인 함수에 요청 방식에 대한 내용을 작성한 후에 서비스 콜을 위해 추가로 clientCall이란 함수를 하나 만들었다.

 

int main(int argc, char **argv) 
{ 
	ros::init(argc, argv, "service_client"); 
	SrvClt c_; 
	service_node::Service srv; 
	
	for(int i=1; i < 4; i++) {	
		srv.request.num = i;
		c_.clientCall(srv);
		ROS_INFO("SEND THE NUMBER: %d", i);
	} 
	return 0; 
}



먼저 "service_client"라는 이름의 노드를 생성한다. 서비스 파일에 선언한 request를 사용하기 위해 srv라는 이름의 객체를 생성한다. for문을 이용하여 순차적으로 수를 보내 서비스 요청을 할 수 있도록 코드를 작성하였다. srv.request.num의 num은 서비스 파일 request 부분에 정의된 요청을 위한 변수이고, 마지막 줄 ROS_INFO에 의해 요청을 보내는 대로 어떤 수가 보내졌는지 사용자가 확인할 수 있다.



④ XML, CMAKE 파일 작성 + 서비스 파일 작성

※ XML

 

<?xml version="1.0"?>
<package format="2">
<name>service_node</name>
<version>0.0.0</version> 
<description>The service_node package</description> 
<maintainer email="yasun95@todo.todo">yasun95</maintainer> 
<license>TODO</license> 
<buildtool_depend>catkin</buildtool_depend> 
<build_depend>message_generation</build_depend> 
<build_depend>roscpp</build_depend> 
<build_depend>std_msgs</build_depend> 
<build_export_depend>roscpp</build_export_depend> 
<build_export_depend>std_msgs</build_export_depend> 
<exec_depend>roscpp</exec_depend> 
<exec_depend>std_msgs</exec_depend> 
<exec_depend>message_runtime</exec_depend> 
<export> </export> 
</package>

 


패키지를 생성하고 바뀐 점은 주석을 지웠다는 것과 meessage_runtime 종속성을 추가 했다는 것이다. 빌드 종속성에 message_generation은 패키지 생성할 때 입력했으므로 이미 작성이되어 있을 것이다.

<exec_depend>message_runtime</exec_depend> 추가


※ CMAKE

 

cmake_minimum_required(VERSION 3.0.2) 
project(service_node) 
find_package(catkin REQUIRED COMPONENTS 
	message_generation 
	roscpp 
	std_msgs 
) 

add_service_files( 
	FILES 
	Service.srv
) 

generate_messages(DEPENDENCIES std_msgs) 

catkin_package( 
	# INCLUDE_DIRS include 
	# LIBRARIES service_node 
	# CATKIN_DEPENDS message_generation roscpp std_msgs 
	# DEPENDS system_lib 
) 

include_directories( 
	# include ${catkin_INCLUDE_DIRS} 
) 

add_executable(server src/server.cpp) 
target_link_libraries(server ${catkin_LIBRARIES}) 
add_dependencies(server ${PROJECT_NAME}_gencpp) 

add_executable(client src/client.cpp) 
target_link_libraries(client ${catkin_LIBRARIES}) 
add_dependencies(client ${PROJECT_NAME}_gencpp)



강조한다....generate_messages를 추가하지 않으면 무적권 catkin_make에서 에러를 맞이하게 될 것이다. 서비스 파일 인식을 위해 add_service_files와 함께 입력해준다.


서비스 파일 작성(Service.srv)

 



서비스 디렉터리는 catkin_create_pkg 명령어로 작성한 프로젝트 디렉터리 안에 있어야 한다. #include "service_node/Service.h"
헤더 파일을 선언해줄 때 프로젝트 이름/서비스 파일 이름.h 으로 선언하기 때문에 이름을 일치시켜줘야 인식할 수 있다.

 

// Request(요청) 
int64 num 
--- 
// Response(응답) 
string ans



 


⑤ 실행 결과

 



서비스 통신은 rqt_graph로 확인할 수 없다. 퍼블리셔-서브스크라이버와 달리 요청과 응답이 동시에 이루어진 경우에만 실행이 되기 때문이다. for문으로 1, 2, 3을 보내지 않고도 터미널에 rosservice call 명령어를 이용하면 위와 같은 결과를 얻을 수 있다. 젯슨보다 저스펙이고 연산을 겸할 수 없는 MCU를 함께 사용한다면, 작성한 코드처럼 switch-case문을 사용해 상황별로 정리를 하고 string 원 투 쓰리를 보내 제어를 할 수 있을 것 같다.




반응형