一、 基本概念
1、Bus Name
可以把Bus Name理解为连接的名称,一个Bus Name总是代表一个应用和消息总线的连接。有两种作用不同的Bus Name,一个叫公共名(well-known names),还有一个叫唯一名(Unique Connection Name)。
可能有多个备选连接的公共名:
公共名提供众所周知的服务。其他应用通过这个名称来使用名称对应的服务。可能有多个连接要求提供同个公共名的服务,即多个应用连接到消息总线,要求提供同个公共名的服务。消息总线会把这些连接排在链表中,并选择一个连接提供公共名代表的服务。可以说这个提供服务的连接拥有了这个公共名。如果这个连接退出了,消息总线会从链表中选择下一个连接提供服务。公共名是由一些圆点分隔的多个小写标志符组成的,例如"org.fmddlmyy.Test"、"org.bluez"。
每个连接都有一个唯一名:
当应用连接到消息总线时,消息总线会给每个应用分配一个唯一名。唯一名以":"开头,":"后面通常是圆点分隔的两个数字,例如":1.0"。每个连接都有一个唯一名。在一个消息总线的生命期内,不会有两个连接有相同的唯一名。拥有公众名的连接同样有唯一名,例如在前面的图中,"org.fmddlmyy.Test"的唯一名是":1.17"。
有的连接只有唯一名,没有公众名。可以把这些名称称为私有连接,因为它们没有提供可以通过公共名访问的服务。 d-feet界面上有个"Hide Private"按钮,可以用来隐藏私有连接。
2、Object Paths
对象是处理消息的一个实例。对象有一个或多个接口,在每个接口有一个或多个的方法,每个方法实现了具体的消息处理。在一对一的通讯中,对象通过一个连接直接和另一个客户端应用程序连接起来。在多对多的通讯中,对象通过一个连接和Dbus守护进程连接起来。对象有一个路径用于指明该对象的存放位置,消息传递时通过该路径找到该对象。
Bus Name确定了一个应用到消息总线的连接。在一个应用中可以有多个提供服务的对象。这些对象按照树状结构组织起来。每个对象都有一个唯一的路径(Object Paths)。或者说,在一个应用中,一个对象路径标志着一个唯一的对象。
"org.fmddlmyy.Test"只有一个叫作"/TestObj"的对象。
本质上,D-BUS 是一个对等(peer-to-peer)的协议 -- 每个消息都有一个源和一个目的。这些地址被指定为 对象路径。概念上,所有使用 D-BUS 的应用程序都包括一组 对象,消息发送到或者发送自特定对象 -- 不是应用程序 -- 这些对象由对象路径来标识。 另外,每个对象都可以支持一个或多个 接口(interfaces)。这些接口看起来类似于 Java 中的接口或者 C++ 中的纯粹的虚类(pure virtual classes)。不过,没有选项来检查对象是否实现了它们所声明的接口,而且也没有办法可以调查对象内部以使列出其支持的接口。接口用于名称空间和方法名称,因此一个单独的对象可以有名称相同而接口不同的多个方法。
3、Interfaces
通过对象路径,我们找到应用中的一个对象。每个对象可以实现多个接口。例如:"org.fmddlmyy.Test"的"/TestObj"实现了以下接口:
org.fmddlmyy.Test.Basic
org.freedesktop.DBus.Introspectable
org.freedesktop.DBus.Properties
后面讲代码时会看到,我们在代码中其实只实现了"org.fmddlmyy.Test.Basic"这个接口。接口"org.freedesktop.DBus.Introspectable"和"org.freedesktop.DBus.Properties"是消息总线提供的标准接口。
4、Methods和Signals
接口包括方法和信号。例如"org.fmddlmyy.Test"的"/TestObj"对象的"org.fmddlmyy.Test.Basic"接口有一个Add方法。后面的例子中我们会介绍信号。
标准接口"org.freedesktop.DBus.Introspectable"的Introspect方法是个很有用的方法。类似于Java的反射接口,调用Introspect方法可以返回接口的xml描述。我们双击 "org.fmddlmyy.Test"->"/TestObj"->"org.fmddlmyy.Test.Basic"->"org.freedesktop.DBus.Introspectable"的Introspect方法。
5、D-BUS 特性
D-BUS 有一些有趣的特性,使其像是一个非常有前途的选择。
协议是低延迟而且低开销的,设计得小而高效,以便最小化传送的往返时间。另外,协议是二进制的,而不是文本的,这样就排除了费时的序列化过程。由于只面向本地机器处理的使用情形,所以所有的消息都以其自然字节次序发送。字节次序在每个消息中声明,所以如果一个 D-BUS 消息通过网络传输到远程的主机,它仍可以被正确地识别出来。
从开发者的角度来看,D-BUS 是易于使用的。有线协议容易理解,客户机程序库以直观的方式对其进行包装。
程序库还设计用于为其他系统所包装。预期,GNOME 将使用 GObject 创建包装 D-BUS 的包装器(实际上这些已经部分存在了,将 D-BUS 集成入它们的事件循环),KDE 将使用 Qt 创建类似的包装器。由于 Python 具有面向对象特性和灵活的类型,已经有了具备类似接口的 Python 包装器。
最后,D-BUS 正在 freedesktop.org 的保护下进行开发,在那里,来自 GNOME、KDE 以及其他组织的对此感兴趣的成员参与了设计与实现。
6、D-BUS 的内部工作方式
典型的 D-BUS 设置将由几个总线构成。将有一个持久的 系统总线(system bus),它在引导时就会启动。这个总线由操作系统和后台进程使用,安全性非常好,以使得任意的应用程序不能欺骗系统事件。还将有很多 会话总线(session buses),这些总线当用户登录后启动,属于那个用户私有。它是用户的应用程序用来通信的一个会话总线。当然,如果一个应用程序需要接收来自系统总线的消息,它不如直接连接到系统总线 -- 不过,它可以发送的消息将是受限的。
一旦应用程序连接到了一个总线,它们就必须通过添加 匹配器(matchers) 来声明它们希望收到哪种消息。匹配器为可以基于接口、对象路径和方法进行接收的消息指定一组规则(见后)。这样就使得应用程序可以集中精力去处理它们想处理的内容,以实现消息的高效路由,并保持总线上消息的预期数量,以使得不会因为这些消息导致所有应用程序的性能下降并变得很慢。
7、消息
在 D-BUS 中有四种类型的消息:方法调用(method calls)、方法返回(method returns)、信号(signals)和错误(errors)。要执行 D-BUS 对象的方法,您需要向对象发送一个方法调用消息。它将完成一些处理并返回一个方法返回消息或者错误消息。信号的不同之处在于它们不返回任何内容:既没有"信号返回"消息,也没有任何类型的错误消息。
消息也可以有任意的参数。参数是强类型的,类型的范围是从基本的非派生类型(布尔(booleans)、字节(bytes)、整型(integers))到高层次数据结构(字符串(strings)、数组( arrays)和字典(dictionaries))。
8、服务
服务(Services) 是 D-BUS 的最高层次抽象,它们的实现当前还在不断发展变化。应用程序可以通过一个总线来注册一个服务,如果成功,则应用程序就已经 获得 了那个服务。其他应用程序可以检查在总线上是否已经存在一个特定的服务,如果没有可以要求总线启动它。服务抽象的细节 -- 尤其是服务活化 -- 当前正处于发展之中,应该会有变化。
9、dbus的方法调用和消息机制
在dbus中调用一个方法包含了两条消息,进程A向进程B发送方法调用消息,进程B向进程A发送应答消息。所有的消息都由daemon进行分派,每个调用的消息都有一个不同的序列号,返回消息包含这个序列号,以方便调用者匹配调用消息与应答消息。调用消息包含一些参数,应答消息可能包含错误标识,或者包含方法的返回数据。
方法调用的一般流程:
1).使用不同语言绑定的dbus高层接口,都提供了一些代理对象,调用其他进程里面的远端对象就像是在本地进程中的调用一样。应用调用代理上的方法,代理将构造一个方法调用消息给远端的进程。
2).在DBUS的底层接口中,应用需要自己构造方法调用消息(method call message),而不能使用代理。
3).方法调用消息里面的内容有:目的进程的bus name,方法的名字,方法的参数,目的进程的对象路径,以及可选的接口名称。
4).方法调用消息是发送到bus daemon中的。
5).bus daemon查找目标的bus name,如果找到,就把这个方法发送到该进程中,否则,daemon会产生错误消息,作为应答消息给发送进程。
6).目标进程解开消息,在dbus底层接口中,会立即调用方法,然后发送方法的应答消息给daemon。在dbus高层接口中,会先检测对象路径,接口,方法名称,然后把它转换成对应的对象(如GObject,QT中的QObject等)的方法,然后再将应答结果转换成应答消息发给daemon。
7).bus daemon接受到应答消息,将把应答消息直接发给发出调用消息的进程。
8).应答消息中可以包容很多返回值,也可以标识一个错误发生,当使用绑定时,应答消息将转换为代理对象的返回值,或者进入异常。
bus daemon不对消息重新排序,如果发送了两条消息到同一个进程,他们将按照发送顺序接受到。接受进程并需要按照顺序发出应答消息,例如在多线程中处理这些消息,应答消息的发出是没有顺序的。消息都有一个序列号可以与应答消息进行配对。
在dbus中一个信号包含一条信号消息,一个进程发给多个进程。也就是说,信号是单向的广播。信号可以包含一些参数,但是作为广播,它是没有返回值的。信号触发者是不了解信号接受者的,接受者向daemon注册感兴趣的信号,注册规则是"match rules",记录触发者名字和信号名字。daemon只向注册了这个信号的进程发送信号。
信号的一般流程如下:
1).当使用dbus底层接口时,信号需要应用自己创建和发送到daemon,使用dbus高层接口时,可以使用相关对象进行发送,如Glib里面提供的信号触发机制。
2).信号包含的内容有:信号的接口名称,信号名称,发送进程的bus name,以及其他参数。
3).任何进程都可以依据"match rules"注册相关的信号,daemon有一张注册的列表。
4).daemon检测信号,决定哪些进程对这个信号感兴趣,然后把信号发送给这些进程。
5).每个进程收到信号后,如果是使用了dbus高层接口,可以选择触发代理对象上的信号。如果是dbus底层接口,需要检查发送者名称和信号名称,然后决定怎么做。
二、 DBus-glib程序编写过程
(1) 环境设置
通过dbus-launch命令获取一个全局变量DBUS_SESSION_BUS_ADDRESS的值,再用export命令设置全局变量的值
(2) 服务端编写
1、 首先,编写接口描述xml文件,描述接口、方法、信号等信息。
Node描述对象路径,在主程序中要通过dbus_g_connection_register_g_object函数重新注册连接,因此也可都不声明,即写成<node>;
Interface描述路径下的接口信息;
Method描述接口中方法信息,arg表示方法函数里的参数名,type表示参数类型,direction表示参数传递的方向,其中in表示传进的参数,out表示函数调用返回的参数;
Signal描述接口中的信号信息
其中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}"
a表示数组,数组元素的类型由a后面的标记决定。例如:
"as"是字符串数组。
数组"a(i(ii))"的元素是一个结构。用括号将成员的类型括起来就表示结构了,结构可以嵌套。
数组"a{sv}"的元素是一个键-值对。"{sv}"表示键类型是字符串,值类型是VARIANT。
2、 将接口描述文件生成绑定文件
有一个叫dbus-binding-tool的工具,它读入接口描述xml文件,产生一个绑定文件。这个文件包含了dbus对象的接口信息。在主程序中我们通过dbus_g_object_type_install_info函数向dbus-glib登记对象信息(DBusGObjectInfo结构)。具体如下:
dbus-binding-tool --mode=glib-server --prefix=server server.xml > server-glue.h
或dbus-binding-tool --mode=glib-server --prefix=server --output=server-glue.h server.xml
其中--prefix参数定义了对象的前缀,生成DBusGObjectInfo结构变量是dbus_glib_前缀_object_info,如例子中位dbus_glib_server_object_info。绑定文件会为接口方法定义回调函数。回调函数的名称是这样的:首先将xml中的方法名称转换到全部小写,下划线分隔的格式,然后增加前缀"$(prefix)_"。例如:如果xml中有方法SendMessage,绑定文件就会引用一个名称为$(prefix)_send_message的函数。
3、 主程序编写
3.1 对象定义
Dbus-glib用GObject实现dbus对象,它继承GObject,如下:
#define SERVER_TYPE (server_get_type())定义获取对象类型的宏
GType server_get_type(void);只需声明此函数,不用实现
G_DEFINE_TYPE(Server, server, G_TYPE_OBJECT);定义对象初始化时的前缀
gboolean server_getmacip(Server *obj, char *device, char **mac, char **ip, GError **error);此函数必须在包含#include "server-glue.h"之前先声明,否则编译会出错,并在主程序中要实现该函数
在创建信号枚举时,最后一个定义为LAST_SIGNAL,没具体用途,只是表示数组长度而已;如有信号时,枚举结构为emnu{CLEAR_SIGNAL,MESSAGE_SIGNAL,LAST_SIGNAL};同时定义信号数组static guint signal[LAST_SIGNAL]={0};
两个初始化函数,一些初始化的过程就放在这两个函数里面。如在*_class_init中创建信号,并进行初始化。在创建信号时,应该按照xml中定义信号的顺序来创建和初始化这些信号,比如先clear后message,先后顺序要一致,不然可能会产生错误。
如下图创建信号clear和message,并初始化:
对g_signal_new函数进行分析如下:
"message"表示创建信号的信号名;
G_OBJECT_CLASS_TYPE(klass)表示信号所属类型;
G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED表示信号标志GSignalFlags,至少指定G_SIGNAL_RUN_FIRST或G_SIGNAL_RUN_LAST两个中的一个;
0表示函数指针在该类型结构中的偏移位置,一般用于调用一个类方法,0表示该信号不带方法;
NULL,NULL分别表示信号类聚和信号类聚数据;
g_cclosure_marshal_VOID__STRING表示信号集的函数;
G_TYPE_NONE,1,G_TYPE_STRING分别表示返回值,信号传递参数的个数和信号传递参数的类型,G_TYPE_NONE表示信号没有返回值。
其中上面的g_cclosure_marshal_VOID_STRING是glib已经定义好的,我们不用自己定义。但是如果,我们想在一个信号中携带多个参数,我们就必须自己定义自己的g_cclosure_marshal。
Glib只实现了极少的marshal函数,glib提供了一个小工具glib-genmarshal,它可以产生marshal函数,输入是一个marshal的描述文件,输出marshal函数的实现。下面是一个描述文件marshal.list的示例:
VOID:STRING,BOXED
依次为返回值和参数列表。
使用示例: glib-genmarshal --body marshal.list > marshal.h
注:数据类型表示为
# VOID indicates no return type, or no extra
# parameters. if VOID is used as the parameter
# list, no additional parameters may be present.
# BOOLEAN for boolean types (gboolean)
# CHAR for signed char types (gchar)
# UCHAR for unsigned char types (guchar)
# INT for signed integer types (gint)
# UINT for unsigned integer types (guint)
# LONG for signed long integer types (glong)
# ULONG for unsigned long integer types (gulong)
# ENUM for enumeration types (gint)
# FLAGS for flag enumeration types (guint)
# FLOAT for single-precision float types (gfloat)
# DOUBLE for double-precision float types (gdouble)
# STRING for string types (gchar*)
# PARAM for GParamSpec or derived types (GParamSpec*)
# BOXED for boxed (anonymous but reference counted) types (GBoxed*)
# POINTER for anonymous pointer types (gpointer)
# OBJECT for GObject or derived types (GObject*)
# NONE deprecated alias for VOID
# BOOL deprecated alias for BOOLEAN
3.2 方法的实现
当方法返回参数为字符串时,传进变量定义成char**,如:
gboolean server_getmacip(Server *obj, char *device, char **mac, char **ip, GError **error),并在实现是对其分配空间,如下:
*mac = (char *)g_malloc(sizeof(char)*20);
*ip = (char *)g_malloc(sizeof(char)*MAXIPNUM*16);
因此在客户端调用时传进参数也应是字符串变量的地址的地址。
3.3 程序实现过程
DBusGConnection *bus; DBusGProxy *bus_proxy; GError *error=NULL; GMainLoop *mainloop; Server *obj; 各数据的定义,其中error必须初始化为NULL,否则会出错。
g_type_init();初始化函数,必不可少的,在程序开始前初始化。
dbus_g_object_type_install_info(SERVER_TYPE, &dbus_glib_server_object_info);将xml绑定信息赋值给自定义对象。
mainloop = g_main_loop_new(NULL, FALSE); g_main_loop_run(mainloop);用于服务器的循环调用请求的服务。
bus = dbus_g_bus_get(DBUS_BUS_SESSION, &error);获取一个会话总线。
bus_proxy = dbus_g_proxy_new_for_name(bus, "org.freedesktop.DBus",
"/org/freedesktop/DBus", "org.freedesktop.DBus");获取总线的代理。
dbus_g_proxy_call(bus_proxy, "RequestName", &error,
G_TYPE_STRING, "test.example.Server",
G_TYPE_UINT, 0,
G_TYPE_INVALID,
G_TYPE_UINT, &request_name_result,
G_TYPE_INVALID) 在总线上请求一个连接名为test.example.Server
obj = g_object_new(SERVER_TYPE, NULL);创建新的对象。
dbus_g_connection_register_g_object(bus, "/test/example/Server", G_OBJECT(obj));注册连接新对象到指定的对象路径中,使得对象可以被使用(即接口可被调用了)。
下面说明信号发送处理:
先在对象初始化中通过g_signal_new创建信号,再在主程序中通过g_singal_emit将信号发送出去,只有有请求接收的客户端才能接收到该信号。
(3) 客户端编写
1、 当客户只需求方法调用和接受信号进行信号处理时
不需要定义xml文件,直接由主程序实现对服务端的调用,如下:
还是必须用函数g_type_init()进行初始化;
获取服务器的代理之后,即可进行方法调用了,如getmacip,具体调用如下:
返回值为字符串时,必须传地址,如mac和ip,它们的地址空间在有服务器分配。
如客户端需要捕获信号进行处理,则需要如下实现:
先从信号来源的连接上获取代理,使得可以接收到信号,再将信号添加到代理中,接着进行连接,当接收到信号时,调用回调函数clear,从而进行信号反应处理。
2、 当客户端需发送信号或需要提供接口的函数方法时
此时,需要像服务端编写一样,先定义一个接口描述xml文件,并进行绑定,实现信号发送和函数功能实现。
三、 编译方法
gcc -Wall server.c -g -o server `pkg-config --libs --cflags dbus-glib-1`
四、 原理流程