一、概述
官方网站:http://www.freedesktop.org/wiki/Software/dbus,但是如果要下windows版的代码最好不要从sourceforge下,多次下来的1.2.4版本都无法正常解压。可以从svn上拿,具体见后面的dbus编译部分。
从官方首页中可以看到这样一段描述D-BUS 的话:“D-Bus is a message bus system, a simple way for applications to talk to one another. In addition to interprocess communication, D-Bus helps coordinate process lifecycle; it makes it simple and reliable to code a "single instance" application or daemon, and to launch applications and daemons on demand when their services are needed. ”
因此,D-BUS从本质来说就是进程间通信(inter-process communication)(IPC)的一个实现。他最初产生于Linux平台,是做为freedesktop.org项目的一部分来开发的。正开始深入地渗透到 Linux 桌面之中。已经在Qt4,GNOME,Windows以及Maemo实现。在KDE4中已经取代了著名的DCOP,在GNOME取代笨重的Bonobo。在嵌入式系统中常用来实现C/S结构。
作为一个IPC,他实现了两点:
1.在同一个桌面会话中不同的应用程序进行通讯:系统总线(system bus),这个总线由操作系统和后台进程使用,安全性非常好,以使得任意的应用程序不能欺骗系统事件。应用程序可以直接和系统总线通信,但是发送的消息受限制。
2.桌面程序与内核或守护进程进行通讯:会话总线(Session bus),属于登录用户私有。它是用户的应用程序用来通信的一个会话总线。
二、D-BUS 特性
1.D-BUS的协议是低延迟而且低开销的,设计得小(但是代码量不算很少吧)而且高效,以便最小化传送时间。从设计上避免往返交互并允许异步操作。
2.协议是二进制的,而不是文本,这样就排除了费事的序列化过程(我们的万能参数序列化就比较占时间)。
3.考虑了字节顺序问题。
4.易用性:它按照消息而不是字节流来工作,并且自动地处理了许多困难的IPC问题,并且D-Bus库以可以封装的方式来设计,这样开发者就可以使用框架里存在的对象/类型系统,而不用学习一种新的专用于IPC的对象/类型系统。
5.请求时启动服务以及安全策略。
6.因为是做为freedesktop.org项目来开发的,有许多达人参加,所以质量应该是很有保证的。
7.支持多语言(C/C++/Java/C Sharp/Python/Ruby),多平台(Linux/windows/maemo)。
8.采用C语言,而不是C++。
9.由于基本上不用于internet上的IPC,因此对本地IPC进行了特别优化。
10.提供服务注册,理论上可以进行无限扩展。
三、构架
分成三层:
a.libdbus库,实现了底层的API以及协议,他除了需要XML解析器以外没有必须的依赖。对于不同的语言,协议可能被重新实现。这个库是一个基础,虽然官方说他不是设计给应用程序调用的,但是实际上应用程序是可以直接调用的,特别是windows版,后面的使用分析中的例子就能看到;
b.消息守护进程,建立在libdbus的基础上,可以管理多个应用程序之间的通信。每个应用程序都和消息守护进程建立dbus链接,由消息守护进程统一进行消息的派发;
c.各种包装库,绑定了一些常见的框架(qt,Glib,Java,Python,C sharp etc.)没什么新功能,只是对dbus进行了一层封装。方便使用官方建议应用程序使用这层进行调用;
体系结构图(来自网络)
一、名词解释
- 消息
消息由头部和消息体组成,如果你把消息当作一个package,那头部就是地址,消息体就是包的内容。消息发送系统使用头部的信息来知道把消息送往何处,如何解释消息,接收者则解释消息体。消息体可以没有或有多个参数,这些参数是具有类型的值,如integer或byte数组。消息头和消息体都使用相同类型的type系统及格式来序列化数据。每种值的类型都有wire格式,从某种别的表示把值转换为wire格式叫作列集(marshalling),而把wire格式转回去则叫散集(unmarshalling).消息头部是有固定签名和意义的值块。消息体是另外的值块,带有在头部中指定的签名。头部必须字节对齐,这样当在一个缓冲区中存贮整个消息时可以允许消息体以8对齐开始。如果头部不是自然地终止于8字节边界上,则必须加上最多7字节的nul初始化的对齐填充。消息体不需要字节对齐。消息的最大长度,包括头,头对齐填充以及消息体是2的27次幂即134217728。实现不能发送或接收超过此大小的消息。头部的签名是: "yyyyuua(yv)",以更为可读的方式写出是:
BYTE, BYTE, BYTE, BYTE, UINT32, UINT32, ARRAY of STRUCT of (BYTE,VARIANT)
含义(摘自D-BUS spec:http://dbus.freedesktop.org/doc/dbus-specification.html):
Value |
Description |
1st BYTE
|
Endianness flag; ASCII 'l' for little-endian or ASCII 'B' for big-endian. Both header and body are in this endianness. |
2nd BYTE
|
Message type. Unknown types must be ignored. Currently-defined types are described below. |
3rd BYTE
|
Bitwise OR of flags. Unknown flags must be ignored. Currently-defined flags are described below. |
4th BYTE
|
Major protocol version of the sending application. If the major protocol version of the receiving application does not match, the applications will not be able to communicate and the D-Bus connection must be disconnected. The major protocol version for this version of the specification is 1. |
1st UINT32
|
Length in bytes of the message body, starting from the end of the header. The header ends after its alignment padding to an 8-boundary. |
2nd UINT32
|
The serial of this message, used as a cookie by the sender to identify the reply corresponding to this request. |
ARRAY ofSTRUCT of (BYTE ,VARIANT ) |
An array of zero or more header fields where the byte is the field code, and the variant is the field value. The message type determines which fields are required. |
在消息头尾部的数组包含了头部域。
消息有四种类型:METHOD_CALL
, METHOD_RETURN
, ERROR
, and SIGNAL
a.METHOD_CALL(方法调用消息)
这类消息会调用远程对象的操作,这些消息需要映射到对象的方法上去。
方法调用消息需要有MEMBER头部域用以指明方法的名称,此消息还可能有一个INTERFACE域来给出接口,被调用的方法也是接口的一部分。在没有INTERFACE域时,如果同一对象上的两个接口有同名的方法名字,哪一个方法会被调用是没有定义的。在这种具有二义性的环境里,实现可以选择返回一个错误。但是,如果方法名是独一无二的,实现不能强制要求需要interface域。还包括一个PATH域,该域用以指明在哪个对象上调用该方法。如果此调用正在消息总线中传播,消息也有一个DESTINATION域用以给出接收此消息的连接名称。当应用程序处理方法调用消息时,它需要答复,答复由REPLY_SERIAL头部域确定 ,此头部域也表明了被答复的方法调用的序列号。答复类型有两种,METHOD_RETURN和ERROR。
b.METHOD_RETURN &
ERROR
答复消息的参数就是方法调用的返回值或"out parameters"
,如果答复类型为ERROR,那么会抛出例外,方法调用失败,此时没有返回值提供。对于同一个方法调用发送多个答复是没有意义的。 即使一个方法调用没有返回值,一个METHOD_RETURN的答复也是必须的,这样调用者就能知道方法是否被成功地处理了。
METHOD_RETURN 和
ERROR答复消息必须有
REPLY_SERIAL
头部域。
d.SIGNAL
信号发射不象方法调用需要答复。信号发射只是简单单一信息类型SIGNAL。它必须有三个头域,PATH给出发送信号的对象,加上INTERFACE和MEMBER给出信号的全称名字。INTERFACE头部域在信号中是必须的,尽管它在方法调用中是可选的。
- Bus Name
可以说是连接的名字,一个连接只有一个总线名字并且是惟一的,在此连接的整个生命周期,这个名字不变。总线名字是STRING类型的,意味着它必须是有效的UTF-8字符串。有两种作用不同的Bus Name,一个叫公共名(well-known names),还有一个叫唯一名(Unique Connection Name)。
- 对象
对象是处理消息的一个实例。对象有一个或多个接口,在每个接口有一个或多个的方法,每个方法实现了具体的消息处理。在一对一的通讯中,对象通过一个连接直接和另一个客户端应用程序连接起来。在多对多的通讯中,对象通过一个连接和Dbus守护进程连接起来。对象有一个路径用于指明该对象的存放位置,消息传递时通过该路径找到该对象。
- 接口
每个对象都有一个或者多个接口,一个接口就是多个方法和信号的集合。dbus使用简单的命名空间字符串来表示接口,如org.freedesktop.Introspectable。可以说dbus接口相当于C++中的纯虚类。
- 方发和信号
每个对象都有一些成员,两种成员:方法(methods)和信号(signals),在对象中,方法可以被调用。信号会被广播,感兴趣的对象可以处理这个信号,同时信号中也可以带有相关的数据。每一个方法或者信号都可以用一个名字来命名,如”Frobate” 或者 “OnClicked”。
- 对象路径
一个对象路径是一个用于引用对象实例的名字,从概念上讲,D-Bus信息交换中每个参与者可能有任意数量的对象实例并且每个这样的实例都有一个路径,就象文件系统一样,一个应用中的对象实例形成一个层次树。
- 服务
服务是 D-BUS 的最高层次抽象,它们的实现当前还在不断发展变化。应用程序可以通过一个总线来注册一个服务,如果成功,则应用程序就已经 获得 了那个服务。其他应用程序可以检查在总线上是否已经存在一个特定的服务,如果没有可以要求总线启动它。
- dbus-daemon
dbus的后台程序(守护进程),libdbus运行时会自动创建dbus-daemon进程。daemon就像一个路由器,将从发送者连接得到的消息转发到由消息中的接收者连接名指定的接收者连接中。
- 连接
连接是一个双向的消息传递通道。一个连接将对象和Dbus(dubs-daemon)或客户端应用连接起来,连接支持非阻塞式的异步消息发送和阻塞式的同步消息发送。消息通过连接到达目的端后,连接会将挂起在该连接上的进程唤醒,由该进程将消息取走。每个连接都有一个唯一的名字和可选的其他多个名字,用于在多对多通讯时指明消息的发送者和接收者
- 认证协议(Authentication Protocol)
消息流开始之前,两个应用之间必须进行认证,简单的纯文本协议用来认证,即SASL档案,相当简单地直接从SASL规范映射过来。没有使用消息编码,只是纯文本信息。
二、运行机制
客户端应用是请求消息的发起者。客户端应用通过和自身的相连的一个连接将请求消息发送出去,也通过该连接接收回应的消息、错误消息、系统更新消息等。在一对一的通讯中,请求消息直接到达对象。在多对多的通讯中,请求消息先到达Dbus,Dbus将消息转发到目的对象。每个连接使用bus name来标识的,bus name有两种,一种是公共名(well-known Name),一种是唯一名(Unique Connection Name)。
所有使用D-BUS的应用程序都包含一些对象,它们一般映射为GObject、QObject、C++对象、或者[[Python">Python</a>对象。当经由一个D-BUS连接收到一条消息时,该消息是被发往一个对象而不是整个应用程序。这一点和我们常见的消息机制是不太一样的。这个对象其实很像C++中虚函数
为了允许消息能指定接收对象,还要提供引用对象的方法。但是这个引用一般实现为与应用程序相关的内存地址,因此无法在应用程序之间传递。为了解决这一问题,D-BUS为每个对象引入名字。这些名字看起来像是文件系统路径,例如一个对象可能叫做 “/org/kde/kspread/sheets/3/cells/4/5”。路径名以容易阅读为佳,没有具体的硬性规定。
在dbus中调用一个方法包含了两条消息,进程A向进程B发送方法调用消息,进程B向进程A发送应答消息。所有的消息都由daemon进行分派,每个调用的消息都有一个不同的序列号,返回消息包含这个序列号,以方便调用者匹配调用消息与应答消息。调用消息包含一些参数,应答消息可能包含错误标识,或者包含方法的返回数据。
1.方法调用的一般流程:
a.使用不同语言绑定的dbus高层接口,都提供了一些代理对象,调用其他进程里面的远端对象就像是在本地进程中的调用一样。应用调用代理上的方法,代理将构造一个方法调用消息给远端的进程。
b.在DBUS的底层接口中,应用需要自己构造方法调用消息(method call message),而不能使用代理。
c.方法调用消息里面的内容有:目的进程的bus name,方法的名字,方法的参数,目的进程的对象路径,以及可选的接口名称。
d.方法调用消息是发送到bus daemon中的。
e.bus daemon查找目标的bus name,如果找到,就把这个方法发送到该进程中,否则,daemon会产生错误消息,作为应答消息给发送进程。
f.目标进程解开消息,在dbus底层接口中,会立即调用方法,然后发送方法的应答消息给daemon。在dbus高层接口中,会先检测对象路径,接口,方法名称,然后把它转换成对应的对象(如GObject,QT中的QObject等)的方法,然后再将应答结果转换成应答消息发给daemon。
g.bus daemon接受到应答消息,将把应答消息直接发给发出调用消息的进程。
h.应答消息中可以包容很多返回值,也可以标识一个错误发生,当使用绑定时,应答消息将转换为代理对象的返回值,或者进入异常。
bus daemon不对消息重新排序,如果发送了两条消息到同一个进程,他们将按照发送顺序接受到。接受进程并需要按照顺序发出应答消息,例如在多线程中处理这些消息,应答消息的发出是没有顺序的。消息都有一个序列号可以与应答消息进行配对。
在dbus中一个信号包含一条信号消息,一个进程发给多个进程。也就是说,信号是单向的广播。信号可以包含一些参数,但是作为广播,它是没有返回值的。
信号触发者是不了解信号接受者的,接受者向daemon注册感兴趣的信号,注册规则是”match rules”,记录触发者名字和信号名字。daemon只向注册了这个信号的进程发送信号。
2.信号的一般流程如下:
a.当使用dbus底层接口时,信号需要应用自己创建和发送到daemon,使用dbus高层接口时,可以使用相关对象进行发送,如Glib里面提供的信号触发机制。
b.信号包含的内容有:信号的接口名称,信号名称,发送进程的bus name,以及其他参数。
c.任何进程都可以依据”match rules”注册相关的信号,daemon有一张注册的列表。
d.daemon检测信号,决定哪些进程对这个信号感兴趣,然后把信号发送给这些进程。
e.每个进程收到信号后,如果是使用了dbus高层接口,可以选择触发代理对象上的信号。如果是dbus底层接口,需要检查发送者名称和信号名称,然后决定怎么做。
三、将消息调用映射为本地API
D-Bus的API可以把方法调用映射为特定编程语言(如C++)中的方法调用,或者把IDL中的方法调用映射为D-Bus消息。
在这些方法调用中,方法的参数被标为"in"(指明是METHOD_CALL传入参数)或者"out"(指明是METHOD_CALL的传出参数),象CORBA的一些API具有"inout"参数,它表明该参数既传入又传出,即调用者传入的值将会被修改。映射到D-Bus上,"inout"参数等价于"in"参数后跟着一个"out"参数,你不能在总线上传引用,所以"inout"参数完全是幻想。如果一个方法具有0个或一个返回值,后跟着0个或多个参数,这里的每个参数可能是"in", "out"或"inout",调用者通过按顺序加上"in"或者"inout"参数来构造消息。"out"参数不会出现在调用者的消息里。接收者构造出答复,如果有返回值,则把第一个返回加到答复上,然后按顺序加上每个"out"或"inout"参数,"in"参数不会出现在答复消息里。如果使用的语言中有例外错误答复消息正常情况下被映射为例外。在转换本地API到D-Bus的API时,把D-Bus命名规范("FooBar")自动地映射到本地命名规范(fooBar/foo_bar)或许好一些。只要本地API是专门写给D-Bus就行。这样当写一个对象实现且此实现将被导出到总线上是最好的了。对象代理用于调用远程D-Bus对象,而远程D-Bus对象可能需要可以调用任何D-Bus方法的能力,因此像这样魔术般的名字映射可能是个问题。
我们了解了什么是D-BUS了也了解了他的基本原理。精彩的是D-BUS是一个完全的opensource软件。那么是时候来看看代码了
由于dbus是支持快平台的,自然需要先考虑清楚我们想研究的目标平台。下载了linux版和windows版,其他版本没有下。
工作环境是windows,但是最初从官方网页指引的地方下载了一个windbus,没编译过去,本来也是一般linux的东西在Windows下编译总是一件痛苦的事情,要么借助其他第三方工具库,要么连代码都不一样。如果需要把linux porting到windows有时候也是蛮麻烦的事情,比如汇编,但是还有很多纯粹的体力劳动,比如编译器支持的语法不一样。为了能正常编译linux版偶还特意装了一个redhat 9的虚拟机。配置就搞了半天,原因很可笑,仅仅是因为安装好vmware新增的网卡是默认使用静态IP的。很多公司都是不允许的。以前怎么一点印象都没有。下载了1.2.4版本,但是在linux下编译没成功,正在找原因的时候发现可以通过svn下载windbus(dbus的windows版)代码。找了一篇编译日志,稍加整理hoho编译、运行一切ok。下面是整理出来的编译dbus Windows版的步骤:
1.下载代码
通过TortoiseSVN:
https://windbus.svn.sourceforge.net/svnroot/windbus
2.下载并安装cmake
http://www.cmake.org/files/v2.6/cmake-2.6.1-win32-x86.exe,安装到<ProgramDir>\gnuwin32
3.下载并安装编译库
http://www.winkde.org/pub/kde/ports/win32/releases/stable/4.1.1/libxml2-2.6.32-1-lib.tar.bz2
http://www.winkde.org/pub/kde/ports/win32/releases/stable/4.1.1/libxml2-2.6.32-1-bin.tar.bz2
http://www.winkde.org/pub/kde/ports/win32/releases/stable/4.1.1/expat-2.0.1-bin.zip
http://www.winkde.org/pub/kde/ports/win32/releases/stable/4.1.1/expat-2.0.1-lib.zip
将以上库都解压至<ProgramDir>\gnuwin32目录,把四个库里所有lib,bin目录下的文件都拷贝到<ProgramDir>\gnuwin32目录下的bin和lib目录
3.使用cmake生成sln文件
a.将windbus\tags\1.2.4拷到一个新的目录如:C:\winbus-1.2.4\source
b.创建一个新的目录,用于生成sln文件以及其他需要的相关文件和目录。如:C:\winbus-1.2.4\compile
c.打开CMD,进入到C:\windbus-1.2.4\complile目录,执行cmake -G "Visual Studio 8 2005" ..\windbus-1.2.4\source\cmake
d.如果成功,complie目录下将生成sln文件,VS2005打开此文件即可进行编译。
注意:VS2008 用cmake -G "Visual Studio 9 2008" ..\windbus-1.2.4\source\cmake 命令,其他版本请参见cmakehelp