서비스란? -> 동기식 양방향 메세지 송수신 방식
- 토픽과 마찬가지로 서비스도 ROS2의 노드 간 통신 방식
- 토픽 vs 서비스
- 토픽은 Publisher-Subscriber 모델을 사용
- 노드는 토픽을 Subscribe하여 지속적인 업데이트와 함께 특정 정보를 받을 수 있다.
- 반면에 서비스는 Request-Response 모델을 사용
- 토픽과 다르게 서비스는 클라이언트가 호출할 때만 데이터를 제공
- 토픽은 Publisher-Subscriber 모델을 사용
- 서비스는 클라이언트(Client)와 서버(Server) 구성된다.
- 동일한 서비스 서버에 대해 여러 클라이언트를 가질 수 있지만 하나의 서비스에 대해 하나의 서버만 가질 수 있다.
- ROS2에서 서비스의 서버는 여러 클라이언트의 요청을 받을 수는 있지만 동시에 여러 요청을 처리하지는 못한다.
-> 서비스 서버(Service Node)는 일반적으로 하나의 요청을 처리하고 있는 동안에는 다른 요청을 기다리게 함
-> 즉, 순차적으로 요청을 처리한다.
-> 병렬로 여러 응답을 동시에 처리하지 않는다.
- ROS2에서 서비스의 서버는 여러 클라이언트의 요청을 받을 수는 있지만 동시에 여러 요청을 처리하지는 못한다.
- 서비스는 한번의 행동을 정확하게 수행해야하는 경우에 사용한다
-> ex) 배터리 도킹, 이미지 인식 명령 인식 등
서비스 명령어들
ros2 service -h # service 관련 명령어가 어떤 것이 있는지 볼 수 있는 명령어
ros2 service list
# ros2 service list 명령어는 현재 ros2 시스템에서 사용 가능한 모든 서비스를 나열한다.
# 아무것도 실행하고 있지 않다면 어떠한 리스트도 뜨지 않는다.
ros2 service call <service_name> <service_type> <value>
# ros2 service call 명령어는 서비스를 호출(요청 보내기)하는데 사용
# 위 명령어를 보듯이 서비스를 호출하려면 서비스 유형이 필요
- service_type은 아래 명령어로 알 수 있다.
ros2 service type <service_name>
# ros2 service type 명령어를 통해 서비스가 어떤 유형인지 알 수 있다.
# 아래의 예시를 통해 service 노드를 실행 했을때 service list가 뜨고, 해당 service의 타입을 확인했을때
# "hangman_interfaces/srv/CheckLetter" 인터페이스 타입이라는 것을 볼 수 있다.
# "source install/setup.bash" 안하면 해당 패키지에서의 인터페이스의 내용을 볼 수 없다.
# 위의 서비스 유형을 보니 요청 데이터가 없는 것을 볼 수 있다.
# 말 그대로 서비스를 요청할 때 데이터가 포함되지 않는 서비스라는 것을 알 수 있다.
# ros2 service call은 서비스 요청을 보내는 명령어라서 위와 같이 request의 데이터가 없는 경우
ros2 service call /check_letter hangman_interfaces/srv/CheckLetter
# 위와 같이 입력하면 된다.
#아래의 예시와 같이 request 요청 데이터가 있을 경우
ros2 service call /arithmetic_operator ros_study_msgs/srv/ "{arithmetic_operator: 1}"
#위와 같이 <value>에는 "{변수명: 값}" 이렇게 써줘야한다
#":" 다음에 띄워쓰기 한 칸 해줘야하는거 주의하자
상수로 1~4 까지를 PLUS,MINUS, MUILTIPLY, DIVISION으로 mapping한 것을 바탕으로 service client가 1~4 중 하나의 값을 보냈(요청)을 때, service server는 a=3, b=2 일때 client가 보낸(요청한) 부호에 맞게 연산을 수행 후 연산 결과를 응답으로 넘기는 간단한 service를 만들어 보자
- ex) client =1 을 보냈다면 3+2 =5의 연산을 수행한 후 결과 값인 5를 client에게 응답으로 값을 넘겨주는 흐름
1. Service Server 만들기 전에 Service를 통해서 Server와 Client가 서비스 메세지를 주고받을 인터페이스를 만들어야 한다.
- 위에서 봤던 인터페이스 형식을 가지도록 srv 인터페이스 파일을 만들고나서 꼭 CMakeList.txt에 경로를 적어줘야한다.
- 아래 그림을 통해 서비스에서 사용하기 위한 인터페이스가 잘 생성된 것을 볼 수 있다.
2. 이제 Service Server를 만들 수 있다.
- 저번에 만들었던 "ros2_test" workspace에 service_pkg라는 패키지를 만들어보자
cd ros2_test/src # 워크스페이스로 이동
ros2 pkg create --build-type ament_python service_pkg --dependencies rclpy # python 패키지 생성
- 방금 만든 사용자 인터페이스를 찾을 수 있도록 package.xml 파일에서 종속성 추가
- service_server 코드
- 코드 설명
1. 필수 패키지들 호출
# 생성한 사용자 인터페이스 패키지 호출
from ros_study_msgs.srv import ArithmeticOperator
# Node를 생성하기 위한 라이브러리 호출
from rclpy.node import Node
# 서비스를 생성하는 등 Python 클라이언트 라이브러리 호출
import rclpy
** Node의 기본 틀은 Class와 Main 함수로 구성되어 있다.
2. Node를 만들기 위해서는 해당 클래스가 Node를 상속해야한다.
-> ROS2에서 제공하는 Node 클래스에는 기본적인 함수들이 정의되어있다.
-> 부모 클래스를 상속받으면 자식클래스는 부모 클래스에 선언된 함수와 변수 등을 사용할 수 있다.
class service_server(Node): # Node 클래스(부모)를 상속받는 service_server 클래스(자식)
3. 클래스 내부에는 __init__(생성자 : 클래스 초기화 함수), get_arithmetic_operator(클라이언트로부터 서비스 요청이 들어왔을때 응답을 주기 위해 처리하는 함수) 로 이루어져 있다.
def __init__(self):
'''''
'''''
def get_arithmetic_operator(self):
'''''
'''''''
3-1 __init__ 함수 내부 코드
def __init__(self):
# 부모 클래스 생성자를 호출하는 것으로 인자로 넘겨준 'service_server' 가 Node의 이름이 된다.
super().__init__('service_server')
self.get_logger().info("service server start!!") # 서비스 서버가 작동했다는 것을 알려주는 로그 메세지
self.argument_a = 3.0 # 초기 a 값
self.argument_b = 2.0 # 초기 b 값
self.argument_operator = 0 # request 값을 담기 위한 변수 선언
self.argument_formula ='' # 나중에 server에서 연산 과정을 담기 위한 변수 선언
self.argument_result = 0.0 # response로 보내기 위한 연산 결과를 담는 변수 선언
self.operator =['+','-','*','/'] # request로 들어온 값에 따른 부호를 출력하기 위한 미리 선언한 리스트 변수
# service server라면 무조건 가져야하는 self.create_service 함수이다.
# self.create_service는 서비스 서버라는 것을 명시해주는 함수로서 이 함수를 통해 반환된 객체인
# self.service_server를 통해서 서버로서의 역할을 수행한다.
self.service_server = self.create_service(ArithmeticOperator, # 서비스 인터페이스 타입
# 서비스명 -> 이 서비스 명이 일치해야 클라이언트와 서버끼리 통신이 가능하다.
'arithmetic_operator',
# 요청이 들어왔을때 처리하기 위한 함수 -> 토픽의 콜백함수와 비슷한 개념이다.
self.get_arithmetic_operator )
의문) 왜 self.service_server를 통해서 서비스 서버 객체를 저장했지만 직접적으로 이 객체를 사용하지를 않을까?
- self.create_service() 함수가 반환하는 객체는 ROS2가 관리하는 서비스 서버에 대한 핸들러 역할을 한다.
- 반환하는 객체는 서비스가 생성된 후. 해당 서비스와 관련된 작업을 처리하는 객체이지만 객체를 명시적으로 사용하거나 참조하는 것은 필요하지 않다.
- 왜냐하면 self.create_service()를 통해 반환된 서비스 서버 객체는 자체적으로 서비스를 관리하고 self.service_server라는 변수에 할당하지 않아도 서비스 요청이 처리된다.
- 자체적으로 서비스를 관리하기 위한 초기 설정을 세팅하는 것이 self.create_service()함수를 통해서 init 함수에서 실행되고, 그 후에 ROS2가 spin()과 같은 이벤트 루프를 통해서 자동 관리와 함께 실행이 되기 때문이다.
의문 ) 그렇다면 self.create_service() 함수를 통해 반환되는 객체를 왜 self.service_server에 담아줬을까?
- self.create_service를 통해서 초기 세팅을 하고 spin과 같은 이벤트 루프를 통해서 실행되면서 자체적으로 관리를 하지만 사용자가 원할때 서비스 서버를 종료하거나 관리를 수동으로 하려면 객체를 저장해두어서 아래와 같이 정상적으로 서비스 서버를 종료할 때 필요하기 때문이다.
-> 사용자가 원할때 명시적으로 제어하거나 종료할 수 있게 하기 위함
executor.service_server.destroy() # ㅅㅓㅂㅓ closed
의문) 서비스 서버 핸들러란 대체 무엇이길래 가능한건가?
- 서비스 서버 핸들러(Service Server Handler)는 서비스 요청을 처리하고 관리하는 객체
- 이 객체는 ROS2에서 서비스 서버가 어떻게 동작할지에 대한 설정을 포함하고 있으며, 서비스 요청이 들어오면 그에 대한 처리를 담당한다.
- 서비스 서버 등록: 서비스 서버 핸들러는 서비스 이름과 타입에 맞게 서비스를 등록한다.(초기 값 세팅)
- 서비스가 생성된 후 내부적으로 ROS2 이벤트 루프(spin, spin_once 등)와 연동되고 나서 클라이언트로부터 요청이 들어올때까지 서비스 요청 대기 상태로 들어간다
- 콜백 처리: 요청이 들어오면 클라이언트 요청을 받아 처리하기 위해 미리 정의된 콜백 함수를 호출하여 그 요청을 처리
- 서비스 상태 관리: 서비스가 활성화되어 있는지, 대기 중인지, 요청을 처리할 준비가 되어 있는지 등을 관리합니다.
- 상태의 변경은 ROS2의 이벤트 루프(spin 등)을 통해 이루어진다.
- 자원 해제: 서비스 서버를 종료할 때 자원을 해제합니다. -> 할당된 객체를 통해 수동 제어 ex) self.service_server
- 서비스 서버 등록: 서비스 서버 핸들러는 서비스 이름과 타입에 맞게 서비스를 등록한다.(초기 값 세팅)
- 이 객체는 ROS2에서 서비스 서버가 어떻게 동작할지에 대한 설정을 포함하고 있으며, 서비스 요청이 들어오면 그에 대한 처리를 담당한다.
3-2 get_arithmetic_operator(클라이언트로부터 서비스 요청이 들어왔을때 응답을 주기 위해 처리하는 함수)
def get_arithmetic_operator(self,request,response):
# 클라이언트의 요청을 담는 과정(-> 선언한 인터페이스 형식에 맞게 "." 연산자로 접근
self.argument_operator = request.arithmetic_operator
#self.calculate_given_formula : 클라이언트의 요청 값에 따른 연산을 수행하는 함수
self.argument_result = self.calculate_given_formula( self.argument_a,
self.argument_b,
self.argument_operator )
response.arithmetic_result = self.argument_result # 응 답 결과를 담기 위한 객체
self.get_logger().info("response complete!!") # 서비스 서버에게 요청이 왔다는 것을 알리기 위한 로그 메세지
return response # 응답 반환 -> 서비스 서버 객체가 이것을 클라이언트에게 전송해준다.
def calculate_given_formula(self, a, b, operator): # 응답을 위한 연산 결과를 리턴하는 함수
if operator == ArithmeticOperator.Request.PLUS: # PLUS 연산 수행
self.argument_result = a + b
elif operator == ArithmeticOperator.Request.MINUS: # request 값이 2 이면 Minus 연산 수행
self.argument_result = a - b
elif operator == ArithmeticOperator.Request.MULTIPLY: # request 값이 3이면 * 연산 수행
self.argument_result = a * b.
elif operator == ArithmeticOperator.Request.DIVISION: # request 값이 4이면 / 연산 수행
try: # 0으로 나눌 경우의 예외 처리
self.argument_result = a / b
except ZeroDivisionError: # 오류 안나게 미리 에러 처리
self.get_logger().error('ZeroDivisionError!')
self.argument_result = 0.0
return self.argument_result
else: # request값이 1,2,3,4 말고 다른 값이 들어왔을 경우 처리하기 위한 부분
self.get_logger().error( 'Please make sure arithmetic operator(plus, minus, multiply, division).') self.argument_result = 0.0
return self.argument_result # 연산 결과 반환
의문) 왜 서비스 서버 핸들러 객체가 주는 것은 클라이언트의 요청 뿐인데 왜 get_arithmetic_operator 함수에서 response도 매개변수로 만들어줬을까?
- 서비스 서버 콜백 함수에서는 단순히 요청 뿐만 아니라 응답도 직접 구성해야하는 이유는 서비스 서버 핸들러 객체가 "빈 응답 객체를 미리 만들어서 전달"해주기 떄문이다.
- 빈 response 객체를 전달해주어서 사용자가 정의했거나 내부에서 정의된 인터페이스의 request 부분의 데이터 타입에 맞게 값을 할당하라고 강제성을 부여해준다.
- 타입 안정성과 코드 일관성을 유지할 수 있게 해줌
- 이러한 패턴은 성능상 이점이 있다.
- 파이썬에서 response 객체를 새로 생성하는 비용을 줄일 수 있고, 내부적으로 rclpy가 메모리를 재활용할 수 있게 해준다.
- 빈 response 객체를 전달해주어서 사용자가 정의했거나 내부에서 정의된 인터페이스의 request 부분의 데이터 타입에 맞게 값을 할당하라고 강제성을 부여해준다.
4. main 함수 코드 내용
def main(args=None):
# ROS2 노드를 실행할 준비를 함(RCL 초기화)
rclpy.init(args=args)
# 위에서 만든 클래스(-> Node로 만듬)의 객체 즉 인스턴스(노드의 역할을 수행) 생성
executor = service_server()
rclpy.spin(executor) # # 종료되지 않도록 해당 노드를 계속 실행 상태로 유지함
executor.service_server.destroy() # ㅅㅓㅂㅓ 종료
executor.destroy_node() rclpy.shutdown() # 프로그램이 종료될 때 ROS2 통신을 안전하게 종료하는 함수
if __name__ == '__main__': # 파이썬 스크립트가 직접 실행될때만 main함수가 실행되도록 하는 구문
main()
#if __name__ == '__main__':은 절대 들여쓰기 하면 안 되고, **맨 바깥 레벨(전역 스코프)**에 있어야 한다.
서비스의 서버는 spin()함수를 통해서 계속 실행시켜줘야한다. 서비스 클라이언트가 요청 보낼때마다 계속 응답해야 하고, 언제 요청을 보낼지 모르기 때문이다.
-> 즉, " spin()은 서비스 요청이 오면 → 콜백 함수 실행 → 다시 대기 "이런 흐름을 무한 반복하면서 응답할 수 있도록 만들어준다.
서비스 클라이언트는 spin_until_future_complete() 또는 spin_once() 함수를 사용한다.
-> 요청 보내고 응답만 정확히 한번 기다리면 되니까 짧게 처리
- 해당 스크립트를 실행 시키려면 토픽을 만들때와 마찬가지로 setup.py의 entry_points에 추가해줘야한다
- -> 방식이 이해 안되면 topic 생성하는 부분 다시 보고오기를 바란다.
- 이제 colcon build 한 후에 해당 스크립트를 실행하고 "ros2 service call" 명령어로 클라이언트의 요청을 보냈을때 잘 동작하는 것을 볼 수 있다.
- 왼쪽 터미널 입력한 명령어
ros2 run service_pkg service_server # service server 동작 코드
- 오른쪽 터미널 입력한 명령어들
ros2 service list # service 노드가 잘 실행되었는지 보기 위한 명령어
ros2 service type /arithmetic_operator # 해당 서비스의 인터페이스 타입 확인
ros2 interface show ros_study_msgs/srv/ArithmeticOperator # 해당 서비스의 인터페이스 내용 확인
#해당 서비스의 인터페이스에 맞게 서비스 요청 => PLUS 요청 보냄
ros2 service call /arithmetic_operator ros_study_msgs/srv/ArithmeticOperator '{arithmetic_operator: 1}'
#해당 서비스의 인터페이스에 맞게 서비스 요청 => MINUS 요청 보냄
ros2 service call /arithmetic_operator ros_study_msgs/srv/ArithmeticOperator '{arithmetic_operator: 2}'
#해당 서비스의 인터페이스에 맞게 서비스 요청 => MULTIPLY 요청 보냄
ros2 service call /arithmetic_operator ros_study_msgs/srv/ArithmeticOperator '{arithmetic_operator: 3}'
#해당 서비스의 인터페이스에 맞게 서비스 요청 => DIVISION 요청 보냄
ros2 service call /arithmetic_operator ros_study_msgs/srv/ArithmeticOperator '{arithmetic_operator: 4}'
아래 그림과 같은 틀을 기반으로 필요한 코드를 추가해서 만드는 것이 서비스 서버이다!
'ROS2 이론 정리' 카테고리의 다른 글
Action Server 구현(1) (0) | 2025.04.24 |
---|---|
서비스 클라이언트 만들기(2) (0) | 2025.04.23 |
사용자 정의 Interface 생성 및 활용 (0) | 2025.04.18 |
노드 생성과 Topic 만들기 (0) | 2025.04.17 |
Ros2 파일 분석 및 실행 과정 (0) | 2025.04.16 |