Dbus 进程间通讯(二) 总线模式

Posted by feizaipp on May 5, 2019

我的博客

       基于 DBus 的应用程序可以有两种方式实现,一种是使用 DBus Daemon 的总线型结构;另一种是点对点的星型结构。本文我们介绍使用 DBus Daemon 的总线型结构。

1. 概述

       DBus 的总线模式是利用 DBus Daemon 系统守护进程完成消息的转发。 GUN 提供了 dbus-glib 和 glib 两个库,大大简化了开发者的开发工作。其中 dbus-glib 中的接口已经被废弃不建议使用,我这里有一个利用 dbus-glib 接口写的例子ueventmonitor 。下面文章我们主要介绍利用 glib 库实现 DBus 总线模式,源码我已上传到 github

2. 准备工作

       首先引入要使用的库,主要包括两个 glib 和 gio-unix ,这两个库在 CentOS 平台都存在于 glib2-devel 包里。在根目录的 configure.ac 文件中增加如下内容:

PKG_CHECK_MODULES(GLIB, [glib-2.0 >= 2.31.13])
AC_SUBST(GLIB_CFLAGS)
AC_SUBST(GLIB_LIBS)

PKG_CHECK_MODULES(GIO, [gio-unix-2.0 >= 2.31.13])
AC_SUBST(GIO_CFLAGS)
AC_SUBST(GIO_LIBS)

       在 Makefile.am 中使用上面的两个库,这样在编译时就去指定目录寻找相应的库文件和头文件。在 Makefile.am 添加如下内容:

AM_CPPFLAGS = \
	$(GLIB_CFLAGS) \
	$(GIO_CFLAGS) \
    $(NULL)

LIBS = \
	$(GLIB_LIBS) \
	$(GIO_LIBS) \
	$(NULL)

       提供一个用于生成代码的 XML 文件,这个 XML 在 DBus 中称为 introspection data ,用来描述 DBus 服务提供哪些方法、属性、信号等,例如方法的表示如下所示:

<node name="/">
  <interface name="org.freedesktop.Testdbus.Base">
    <method name="TestInt">
      <arg name="ret" type="i" direction="out" />
    </method>
    <method name="TestStr">
      <arg name="arg0" type="i" direction="in" />
      <arg name="ret" type="s" direction="out" />
    </method>
    <method name="TestStructs">
      <arg name="ret" type="a(si)" direction="out" />
    </method>
  </interface>
</node>

       方法可以有输入、输出。方法的名字用驼峰的命名方式命名,参数的名字可以随意,但参数的 type 和 direction 要准确。 type 指参数的类型, direction 指参数的方向, out 代表输出, in 代表输入。 type 类型比较多,如下表所示:

类型 说明
a ARRAY 数组
b BOOLEAN 布尔值
d DOUBLE IEEE 754双精度浮点数
g SIGNATURE 类型签名
i INT32 32位有符号整数
n INT16 16位有符号整数
o OBJECT_PATH 对象路径
q UINT16 16位无符号整数
s STRING 零结尾的UTF-8字符串
t UINT64 64位无符号整数
u UINT32 32位无符号整数
v VARIANT 可以放任意数据类型的容器,数据中包含类型信息。例如glib中的GValue。
x INT64 64位有符号整数
y BYTE 8位无符号整数
() 定义结构时使用。例如”(i(ii))”
{} 定义键-值对时使用。例如”a{us}”
h 文件描述符

       将 XML 文件编译生成代码,代码供给 DBus 服务端和客户端使用。将 XML 文件编译成 C 代码需要使用 gdbus-codegen 工具,具体要在 Makefile.am 文件中添加如下内容:

dbus_built_sources = testdbus-generated.h testdbus-generated.c

$(dbus_built_sources) : $(top_srcdir)/data/org.freedesktop.Testdbus.xml
	gdbus-codegen --interface-prefix org.freedesktop.Testdbus --c-namespace Testdbus --generate-c-code=testdbus-generated $<

BUILT_SOURCES =										\
	$(dbus_built_sources)								\
	$(NULL)

       gdbus-codegen 工具将根据 XML 文件生成 testdbus-generated.h 和 testdbus-generated.c 两个文件。这两个文件中创建了 TestdbusBase 对象,该对象的名字是由 –c-namespace 和 –interface-prefix 两个选项决定的。

       在编译服务端和客户端程序时将 $(BUILT_SOURCES) 编译进去。在 Makefile.am 文件中添加如下内容:

# 服务端程序
testdbusprivdir = $(libexecdir)/testdbus
testdbuspriv_PROGRAMS = testdbusd
testdbusd_SOURCES = main.c $(BUILT_SOURCES)
# 客户端程序
bin_PROGRAMS = testgdbus
testgdbus_SOURCES = testgdbus.c $(BUILT_SOURCES)

3. 服务端实现

       服务端首先创建一个循环 g_main_loop_new ,然后使用 g_bus_own_name 接口想 DBus Daemon 获取 “org.freedesktop.Testdbus” 名字的 DBus ,如果该名字没有被占用,则获取成功,并回调 on_bus_acquired 接口,该接口是调用 g_bus_own_name 函数时传入的。

       在 on_bus_acquired 回调函数中创建 TestdbusBase 对象,并注册 XML 内容中的方法,然后将这个 Interface 导出,这样可以在 D-feet 工具中查看并使用。代码如下所示:

skeleton = testdbus_base_skeleton_new ();
g_signal_connect(skeleton, "handle-test-int", G_CALLBACK(on_handle_test_int), NULL);
g_signal_connect(skeleton, "handle-test-str", G_CALLBACK(on_handle_test_str), NULL);
g_signal_connect(skeleton, "handle-test-structs", G_CALLBACK(on_handle_test_structs), NULL);
g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(skeleton), connection,
                                                    "/org/freedesktop/Testdbus/Base", &error);

4. 客户端实现

       使用 testdbus-generated.h 中提供的接口实现调用服务端进程的方法。首先获取 TestdbusBase 对象的代理,然后通过代理调用服务端的方法。代码如下所示:

proxy = testdbus_base_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM,
                                                                G_DBUS_PROXY_FLAGS_NONE,
                                                                "org.freedesktop.Testdbus",
                                                                "/org/freedesktop/Testdbus/Base",
                                                                NULL,
                                                                &error);
testdbus_base_call_test_int_sync(proxy, &ret, NULL, &error);

5. 返回值处理

       客户端调用服务端的方法返回值的类型各异,这里介绍下对于返回指类型是结构体数组的情况。

5.1. 服务端处理

       首先创建一个 GVariantBuilder 对象,并初始化该对象存储的参数类型;

       然后将参数值一次添加到 GVariantBuilder 对象中;

       最终将 GVariantBuilder 对象转换成 GVariant 对象返回;

       代码如下所示:

GVariantBuilder builder;
GVariant *ret = NULL;
g_printerr ("Method call:on_handle_test_structs\n");
g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(si)"));
g_variant_builder_add (&builder, "(si)", "test1", 1);
g_variant_builder_add (&builder, "(si)", "test2", 2);
g_variant_builder_add (&builder, "(si)", "test3", 3);
ret = g_variant_builder_end (&builder);
testdbus_base_complete_test_structs(skeleton, invocation, ret);

5.2. 客户端处理

       首先客户端得到一个 GVariant 类型的对象,并将其初始化为 GVariantIter 对象;

       然后以此遍历 GVariantIter 对象,取出每一个结构体的值;

       代码如下所示:

testdbus_base_call_test_structs_sync(proxy, &ret, NULL, &error);
g_variant_iter_init(&iter, ret);
while (g_variant_iter_next(&iter, "(si)", &ret_str, &ret_int)) {
    g_printerr("sync reply:%s\n", ret_str);
    g_printerr("sync reply:%d\n", ret_int);
    g_free(ret_str);
}