메뉴

문서정보

목차

DBus

D-Bus는 message bus 시스템으로 애플리케이션간 통신을 위한 기능들을 지원한다. IPC(Inter process communication) 일종이라 할 수 있겠다. DBus는 IPC의 기능이외에 프로세스의 lifecycle를 관리하는 기능도 가지고 있다. Single instance 애플리케이션이나 데몬 프로세스를 관리하거나 서비스가 필요한 애플리케이션을 실행해 주는 등의 작업을 지원한다는 건데, 직접 테스트를 해봐야 겠다. 오늘은 일단 DBUS 서비스와 클라이언트를 만드는 것에 집중한다.

DBus는 "새로운 하드웨어 추가"나 "프린터 큐 변경 확인"과 같은 이벤트 처리를 위한 시스템 데몬과 애플리케이션간 통신을 위한 전통적인 IPC 기능 모두를 지원한다. 리눅스에서 널리 사용되고 있으며, 특히 KDE와 Gnome같은 데스크탑 환경에서 중요하게 사용한다. 데스크탑 환경의 경우 다양한 컴포넌트들을 서로 통합할 수 있어야 하는데, DBus가 이런 역할을 수행한다.

D-Bus는 C#, Java, Ruby, Python, Go와 같은 다양한 언어들의 구현체가 있다. 나는 Ruby를 이용해서 DBus를 테스트 할 계획이다.

최종 목적

DBus를 이용해서 Avahi의 기능을 이용하는게 목적이다. 시스템 프로그래밍에서 손을 뗀게 벌써 2년은 된 것 같은데, 이제와서 Avahi를 이용하기 위해서 DBus를 배우는 것은 귀찮고 해서 그냥 DNS-SD를 직접 이용 zero conf 환경을 만들어볼 생각이었다. 그래 DNS-SD 모듈을 이용해서 테스트 프로그램을 만들어서 돌렸더니 아주 친절하게도 "Avahi 응용을 만들고 싶으면, DNS-SD보다는 Avahi를 사용하는게 정신건강에 좋을 겁니다. Avahi는 Bonjour도 잘 지원하고 있거든요"라는 메시지를 출력해주는 거다. 그래서 눈물을 머금고 DBus를 공부하게 됐다.

테스트 및 개발 환경

DBus 개요

DBus 아케텍처

http://dbus.freedesktop.org/doc/diagram.png

그림은 복잡해 보이지만 내용은 단순하다. 원격에 있는 프로세스와 통신하기 위해서 필요한게 무언지를 생각하면 된다. 1. 프로세스의 이름을 알아낸 다음 2. 프로세스에서 제공하는 기능을 호출하고 3. 때때로 비동기적인 이벤트를 받는다. 이들 3가지 구성요소만 있으면, 원격 프로세스와 통신할 수 있다. 이 3가지 기본 구성요소에, 이름관리 규칙, marshalling & unmarshalling를 위한 여러 기능들을 추가해서 DBus 시스템이 완성된다.

Object Path

데이터 통신을 하기 위해서는 네트워크 상에서 나와 상대방의 위치를 특정할 수 있어야 한다. 인터넷에서 나와 상대방의 컴퓨터를 찾기 위해서 이더넷 카드에 IP 주소를 할당하는 것을 생각해보라.

DBus도 통신 서비스를 제공하는 시스템이기 때문에, 자신에 연결한 애플리케이션의 경로를 설정하기 위한 경로 식별자(identified)가 필요하다. 이 경로 식별자를 object path라고 한다. Object path는 표준 유닉스 파일 시스템 경로 형식을 사용한다. 유닉스 파일 시스템 경로와 다른 점이라면 숫자, 문자, 밑줄, / 만 사용할 수 있다는 점이다.

Interface

DBus 인터페이스는 DBus객체를 호출하기 위한 "메서드"와 "시그널"이다. 사용자는 "인터페이스"를 이용해서, 메서드와 시그널을 호출할 수 있다. 인터페이스는 하나 이상의 메서드와 시그널을 가질 수 있다. 예컨데, 인터페이스는 메서드와 시그널을 묶어주는(혹은 연결해주는) 그룹 정도로 볼 수 있을 것이다.

인터페이스는 충돌 가능성을 줄이기 위해서 DNS 도메인 형식을 (뒤집어서)사용한다. 예를들어 avahi의 경우 "org.freedestop.Avahi"와 같은 인터페이스 이름을 가진다. JMusic이라는 음악 애플리케이션을 만든다고 가정해보자. JMusic의 관리 기능을 위해셔서 "com.joinc.JMusic.Manager"이름을 가지는 인터페이스를 만들 수 있을 것이다. 이 인터페이스는 "목록보기", "목록추가", "삭제"와 같은 method들을 가지고 있을 것다. 이들은 대략 아래처럼 네이밍 할 수 있을 것이다. 인터페이스는 시그널을 가질 수도 있는데, JMusic의 경우 "volume up/down", stop, start, pause 등의 시그널을 가진다. 제어와 관련된 시그널이니, 인터페이스의 이름은 Control로 하기로 했다.

Signature Strings

메서드와 시그널은 "매개변수"를 필요로 하는 경우가 있다. 이 경우 매개변수의 타입을 정의를 해야 한다. DBus는 Signatures라고 부르는 string 기반의 인코딩 매커니즘을 지원하는데, 이걸 이용해서 매개변수의 타입을 설정할 수 있다. 아래는 Signature 인코딩에 사용하는 문자와 문자가 의미하는 데이터 타입을 정리한 표다.
문자 데이터 타입
y 8-bit unsigned integer
b 불리언 타입
n 16-bit signed integer
q 16-bit unsigned integer
i 32-bit sined integer
u 32-bit unsined integer
x 64-bit sined integer
t 64-bit unsined integer
d double-precision floating point
s UTF-8 string
o D-Bus Object Path string
g D-Bus Signature string
a array
( Strucure 시작
) Structure 끝
v Variant Type
{ Dictionary/Map 시작
} Dictionary/Map 끝
h Unix file descriptor

DBus Client

DBus 클라이언트는 DBus에 연결하는 모든 프로세스다. 클라이언트들은 버스에 시그널과 메서드를 등록하거나 원격에 있는 메서드를 호출하고 (시그널)이벤트를 받는 식으로 클라이언트간 메시지를 교환한다.

인터페이스 정의

D-Bus의 인터페이스는 일반적으로 응용 프로그램이 제공하는 클래스의 API에 대응되며, XML 언어로 기술할 수 있다. 앞서 사용했던 JMusic을 DBus XML문서로 기술했다.
<node>
  <interface name="com.joinc.JMusic.Manager">
    <method name="GetAllList">
    </method>
    <method name="AddList">
        <arg name="filename" type="s" direction="in"/>
    </method>
    <method name="DelList">
        <arg name="filename" type="s" direction="in"/>
    </method>
  </interface>
  <interface name="com.joinc.JMusic.Control">
    <signal name="start">
        <arg name="state" type="i"/>
        <arg name="error" type="s"/>
    </signal>
  </interface>
</node>

서비스 개발

DBus 기반으로 통신하는 음악 서비스 서버와 음악 클라이언트 애플리케이션을 만들어 보려고 한다. 음악 서비스의 이름은 "jmusic"이다. jmusic 서비스의 기능은 아래와 같다.

gem 목록들

dbus와 getopt 모듈을 설치한다.
# gem install ruby-dbus
# gem install getopt

jmusic 서비스 서버

설명은 주석으로 대신한다.
require 'dbus' 
require 'json'

class MusicPlayer < DBus::Object
    @music_queue = nil
    def initialize arg
        super(arg)
        @music_queue = Array.new
    end

    # Player 인터페이스를 추가한다. 
    # play와 playAll 두개의 메서드와
    # MusicEnding 시그널을 가지고 있다.
    dbus_interface "co.kr.joinc.jmusic.Player" do
        # string 타입의 매개변수를 받는다.
        # string을 반환한다.
        dbus_method :play, "in name:s, out outstr:s" do |name|
            puts "Play Music : #{name}"
            sleep(1)
            ["#{name} ending..."]
        end

        # 재생 대기열의 모든 곡을 재생한다.
        # 재생 시작과 재생 끝에 대해서 시그널을 전송한다.
        dbus_method :playAll  do
            r = Random.new
            @music_queue.each do | v |
                MusicStart v
                playTime = r.rand(3...6)
                puts "Play Music : #{v}"
                sleep(playTime)
                MusicEnding v, playTime
            end
        end

        # 신경쓰지 말자.
        dbus_method :stop do 
        end

        # 시그널
        dbus_signal :MusicEnding, "toto:s, time:u"
        dbus_signal :MusicStart, "toto:s, time:u"
    end

    # 재생 대기열을 관리한다. 
    dbus_interface "co.kr.joinc.jmusic.Manager" do
        # 재생 대기열에 곡을 추가한다.
        dbus_method :add, "in name:s" do |name|
            @music_queue.push(name)
            puts "#{name} add Queue!!"
        end
        # 재생 대기열의 모든 곡을 출력한다.
        dbus_method :getList,"out data:s" do
            data = @music_queue.to_json
            [data]
        end
    end
end

bus = DBus::SessionBus.instance
service = bus.request_service("co.kr.joinc.jmusic")
myobj = MusicPlayer.new("/co/kr/joinc/jmusic") 
service.export(myobj)

puts "listening"
main = DBus::Main.new
main << bus
main.run

jmusic 클라이언트

그냥 서비스 메서드를 호출하는 프로그램이라서 딱히 설명할게 없다.
require 'dbus'
require 'getopt/long'
require 'json'

class Player
    @session_bus = nil
    @player = nil
    @intro = nil
    @manager_iface = nil
    @player_iface = nil

    def initialize
        @session_bus = DBus::SessionBus.instance
        service = @session_bus.service("co.kr.joinc.jmusic")
        @player = service.object("/co/kr/joinc/jmusic")
        introspect
        # 두개의 인트페이스가 필요하다.
        @manager_iface = @player["co.kr.joinc.jmusic.Manager"]
        @player_iface = @player["co.kr.joinc.jmusic.Player"]

        playerSignal
    end

    def introspect
        @intro = @player.introspect
    end

    def showspec
        return @intro
    end

    def play name
        response = @player_iface.play name
        puts response
    end

    def playAll
        @player_iface.playAll
    end

    def add name
        puts "Add music #{name}"
        @manager_iface.add name
    end

    def getList 
        a = @manager_iface.getList
        return JSON.parse(a[0])
    end

    def playerSignal
        @player_iface.on_signal("MusicStart") do |name|
            puts "Music Play #{name}++"
        end
        @player_iface.on_signal("MusicEnding") do |name, time|
            puts "Music Ending #{name} (#{time})..."
        end
    end
end

opt = Getopt::Long.getopts(
    ["--add", "-a", Getopt::REQUIRED],
    ["--play", "-n", Getopt::REQUIRED],
    ["--playall", "-p", Getopt::BOOLEAN],
    ["--intro", "-i", Getopt::BOOLEAN],
    ["--list", "-l", Getopt::BOOLEAN]
)

myPlayer = Player.new

if opt['a']
    myPlayer.add(opt['a'])
elsif opt['n']
    myPlayer.play opt['n']
elsif opt['p']
    myPlayer.playAll
elsif opt['i']
    puts myPlayer.showspec
elsif opt['l']
    myPlayer.getList.each_with_index do | v, index |
        puts "#{index} : #{v}"
    end
end

-i 옵션으로 jmusic의 introspect정보를 출력해 봤다.
# ruby music_player.rb -i | xmllint --format -
<?xml version="1.0"?>                                                                                                                         
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>                                                                                                                                        
  <interface name="co.kr.joinc.jmusic.Player">                                                                                                
    <method name="play">                                                                                                                      
      <arg name="name" direction="in" type="s"/>                                                                                              
      <arg name="outstr" direction="out" type="s"/>
    </method>
    <method name="playAll">
</method>
    <method name="stop">
</method>
    <signal name="MusicEnding">
      <arg name="toto" type="s"/>
      <arg name="time" type="u"/>
    </signal>
    <signal name="MusicStart">
      <arg name="toto" type="s"/>
      <arg name="time" type="u"/>
    </signal>
  </interface>
  <interface name="co.kr.joinc.jmusic.Manager">
    <method name="add">
      <arg name="name" direction="in" type="s"/>
    </method>
    <method name="getList">
      <arg name="data" direction="out" type="s"/>
    </method>
  </interface>
</node>

정리

응용

서비스를 사용하기 위해서 필요한 것들을 정리해 보자.
  1. 서비스 도메인 : 서비스의 위치를 알 수 있어야 한다.
  2. 서비스 인터페이스 : 서비스와 연결하기 위한 "인터페이스"가 있어야 한다.
  3. 서비스 메서드
    • 메서드를 위한 매개변수
    • 반환 값
  4. 서비스로 부터의 비동기 적인 메시지를 처리 하기 위한, 시그널 핸들
이들 구성요소는 원격에서 서비스를 호출하기 위한 최소한의 요구사항들이다. 다른 IPC 혹은 RPC 모두 위의 구성에서 크게 벗어나지 않는다.

OpenAPI와 MQTT를 이용한 원격/로컬 메시지 전송 프로토콜을 설계 할 때, DBus의 구성을 응용해 봐야 겠다.

DBus와 MQTT

메시징 기반의 IPC로 사용 할 수 있는 도구로 MQTT를 생각해 볼 수 있다. 두 개의 툴을 비교해 보는 것도 재미있겠다. MQTT를 IPC 용도로 사용하는 것은 따로 실험해 봐야 겠다.

Avahi

DBus를 공부한 목적은 Avahi와 통신하기 위해서다. Avahi와의 통신을 테스트해봐야 겠다.