使用
Glib/D-Bus
做异步操作
本部分内容:
- D-Bus客户端(client)的不同步性
- (server)服务器端的缓慢运行
- 使用桩(stub)做异步方法调用
- 不同步性所引发的问题
- 使用Glib的封装做异步方法调用
D-Bus
客户端
(client)
的不同步性
到目前为止,我们所实现的
RPC
调用都是比较快的,原因在于没有访问运行较慢的服务或者外部的资源。不过,在现实生活中,经常会遇到这种情况。这就需要我们在完成方法调用之前,需要等待外部的服务程序完成之后,才算真正的调用结束。
Glib
封装提供了一种方法调用的形式:调用可以被立即启动,函数调用返回时激发注册的回调函数。
当你的程序需要更新一些状态或者对用户做出响应,使用这种异步操作是非常必要的。否则程序可能由于需要等待
RPC
的返回而被阻塞住,或者不能及时刷新屏幕。另外一种不太推荐的方法:使用多线程。多线程有两个弊端:一难调试;二难同步。这里我们不采用多线程的方式。
为了模拟现实中遇到的执行慢的情况,我们在
server
侧的实现代码中加个延时。通过这个试验,我们就会发现异步
RPC
机制是非常重要的。因为信号也是一种异步机制,为了排除它的干扰,我们把
client
侧的信号去掉,保留
server
侧的信号。
使
server
侧程序缓慢执行
Server
侧的代码唯一变动之处:在几个
RPC
函数里面增加延时操作:
/* How many microseconds to delay between each client operation. */
#define
SERVER_DELAY_USEC (5*1000000UL)
/*... Listing cut for brevity ...*/
gboolean value_object_setvalue1(ValueObject* obj, gint valueIn,
GError** error) {
dbg("Called (valueIn=%d)", valueIn);
g_assert(obj != NULL);
dbg("Delaying operation");
g_usleep(SERVER_DELAY_USEC); //延时操作
/* Compare the current value against old one. If they're the same,
we don't need to do anything (except return success). */
if (obj->value1 != valueIn) {
还与前面一样编译
server
侧代码,不过运行时我们就可以注意到
RPC
的延迟现象:
[sbox-CHINOOK_X86: ~/glib-dbus-async] > run-standalone.sh ./server &
server:main Connecting to the Session D-Bus.
server:main Registering the well-known name (org.maemo.Platdev_ex)
server:main RequestName returned 1.
server:main Creating one Value object.
server:value_object_class_init: Called
server:value_object_class_init: Creating signals
server:value_object_class_init: Binding to GLib/D-Bus
server:value_object_class_init: Done
server:value_object_init: Called
server:main Registering it on the D-Bus.
server:main Ready to serve requests (daemonizing).
server: Not daemonizing (built with NO_DAEMON-build define)
[sbox-CHINOOK_X86: ~/glib-dbus-async] > time run-standalone.sh dbus-send /
--type=method_call --print-reply --dest=org.maemo.Platdev_ex /
/GlobalValue org.maemo.Value.getvalue1
server:value_object_getvalue1: Called (internal value1 is 0)
server:value_object_getvalue1: Delaying operation
method return sender=:1.54 -> dest=:1.56
int32 0
real 0m5.066s
user 0m0.004s
sys 0m0.056s
上面,我们使用
”time”
命令来测量走表时间(也称实际时间)、代码运行时间以及系统调用时间。这里,我们对
“real”
时间比较感兴趣,这里可以看出程序的确延迟了
5
秒!当然,这里的延迟不是很精确。
下面我们做另外一个试验:当前已经有函数正在运行,而另外一个客户端程序也要调用这个函数,该怎么处理这种情况呢?一个比较简单的测试:运行两次命令,前一次在后台运行:
[sbox-CHINOOK_X86: ~/glib-dbus-async] > time run-standalone.sh dbus-send /
--type=method_call --print-reply --dest=org.maemo.Platdev_ex /
/GlobalValue org.maemo.Value.getvalue1 & //后台运行,运行后处于延迟状态,下面马上再执行一次该命令
[2] 17010
server:value_object_getvalue1: Called (internal value1 is 0)
server:value_object_getvalue1: Delaying operation
[sbox-CHINOOK_X86: ~/glib-dbus-async] > time run-standalone.sh dbus-send /
--type=method_call --print-reply --dest=org.maemo.Platdev_ex /
/GlobalValue org.maemo.Value.getvalue1 //再运行一次
method return sender=:1.54 -> dest=:1.57
int32 0
real 0m5.176s
user 0m0.008s
sys 0m0.092s
server:value_object_getvalue1: Called (internal value1 is 0)
server:value_object_getvalue1: Delaying operation
method return sender=:1.54 -> dest=:1.58
int32 0
real 0m9.852s
user 0m0.004s
sys 0m0.052s
从上面的输出,我们可以看到第一个客户程序被延迟了
5
秒才得以执行,第二个被延迟了更长的时间,这说明
server
侧的程序一次只能处理一个
client
的调用请求,后面的请求将会排队执行,正是这种排队操作,导致延迟会累加。
后面我们将会碰到
server
侧的并发问题,不过到目前为止,我们希望
client
侧的程序在等待
server
侧响应的同时,能够继续
“
正常
”
的工作。我们这个例子程序,没有用户界面,因此,所谓的
“
正常
”
就是:能够继续等待响应。不过对于图形界面程序,异步操作使得程序能够及时响应用户的输入。
D-Bus
本身并不支持直接取消
server
侧已经运行的方法调用。如果你要增加这种中途取消调用的功能,你需要自行在
server
增加一个单独的函数调用。不幸的是:
server
侧一次只能处理一个操作,所以目前
server
根本不能支持中途取消方法调用的操作。
使用
D-Bus
工具生成的
桩函数
来实现异步方法调用
使用
glib-bindings-tool
,
由
XML
文件生成
stub
头文件时,这个工具已经替你生成了异步方法调用的接口。剩下的工作就是实现回调函数、处理返回错误和启动方法调用了。
/* Pull in the client stubs that were generated with
dbus-binding-tool */
#include
"value-client-stub.h" //直接使用工具生成的桩函数原型
我们实现具体的回调函数:
/**
*
这个回调函数会在三种情况下被调用:1:正常结束;2:超时;3:失败
*
程序运行一段时间后,你可以看到这三种情况。
*
* This function will be called when the async setvalue1 will either
* complete, timeout or fail (our server however does not signal
* errors, but the client D-Bus library might). When this example
* program is left running for a while, you will see all three cases.
*
* The prototype must match the one generated by the dbus-binding-tool
* (org_maemo_Value_setvalue1_reply).
*
* Since there is no return value from the RPC, the only useful
* parameter that we get is the error object, which we'll check.
* If error is NULL, that means no error. Otherwise the RPC call
* failed and we should check what the cause was.
*/
static
void setValue1Completed(DBusGProxy* proxy, GError *error,
gpointer userData) {
g_print(PROGNAME ":%s:setValue1Completed/n", timestamp());
if (error != NULL) {
g_printerr(PROGNAME " ERROR: %s/n", error->message);
/* We need to release the error object since the stub code does
not do it automatically. */
g_error_free(error);
} else {
g_print(PROGNAME " SUCCESS/n");
}
}
由于这里的方法调用并没有返回任何数据,所以传递给回调函数的参数也最少(一般三个)。对于
error
的处理必须放在回调函数里面,因为
server
侧的延时可能会导致在启动方法调用时不能立即得到
error
。
请注意:这里的回调函数并没有在发生错误时结束程序,故意这么做的,主要是为了验证下面出现的异步问题。函数
”timestamp”
用于获取程序开始后的秒数(对于查看异步事件的顺序很有用)。
/**
* This function will be called repeatedly from within the mainloop
* timer launch code.
*
* It will launch asynchronous RPC method to set value1 with ever
* increasing argument.
*/
static
gboolean timerCallback(DBusGProxy* remoteobj) {
/* Local value that we'll start updating to the remote object. */
static gint localValue1 = -80;
/* Start the RPC.
This is done by calling the stub function that will take the new
value and the callback function to call on reply getting back.
The stub returns a DBusGProxyCall object, but we don't need it
so we'll ignore the return value. The return value could be used
to cancel a pending request (from client side) with
dbus_g_proxy_cancel_call. We could also pass a pointer to
user-data (last parameter), but we don't need one in this example.
It would normally be used to "carry around" the application state.
*/
g_print(PROGNAME ":%s:timerCallback launching setvalue1/n",
timestamp());
org_maemo_Value_setvalue1_async(remoteobj, localValue1,
setValue1Completed, NULL);
g_print(PROGNAME ":%s:timerCallback setvalue1 launched/n",
timestamp());
/* Step the local value forward. */
localValue1 += 10;
/* Repeat timer later. */
return TRUE;
}
使用
stub
代码非常简单,对于每一个同步函数调用,都有一个对应的异步操作函数。
主函数还是与前一个例子类似,每隔
1
秒调用一次异步方法调用。
不同步性所引发的问题
编译后运行,开始似乎一切正常,但是一段时间后出现了问题:
[sbox-CHINOOK_X86: ~/glib-dbus-async] > make client-stubs
dbus-binding-tool --prefix=value_object --mode=glib-client /
value-dbus-interface.xml > value-client-stub.h
cc -I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include /
-I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -g -Wall /
-DG_DISABLE_DEPRECATED -DNO_DAEMON -DPROGNAME=/"client-stubs/" /
-c client-stubs.c -o client-stubs.o
cc client-stubs.o -o client-stubs -ldbus-glib-1 -ldbus-1 -lgobject-2.0 -lglib-2.0
[sbox-CHINOOK_X86: ~/glib-dbus-async] > run-standalone.sh ./client-stubs
client-stubs:main Connecting to Session D-Bus.
client-stubs:main Creating a GLib proxy object for Value.
client-stubs: 0.00:main Starting main loop (first timer in 1s).
client-stubs: 1.00:timerCallback launching setvalue1
client-stubs: 1.00:timerCallback setvalue1 launched
server:value_object_setvalue1: Called (valueIn=-80)
server:value_object_setvalue1: Delaying operation
client-stubs: 2.00:timerCallback launching setvalue1
client-stubs: 2.00:timerCallback setvalue1 launched
client-stubs: 3.01:timerCallback launching setvalue1
client-stubs: 3.01:timerCallback setvalue1 launched
client-stubs: 4.01:timerCallback launching setvalue1
client-stubs: 4.01:timerCallback setvalue1 launched
client-stubs: 5.02:timerCallback launching setvalue1
client-stubs: 5.02:timerCallback setvalue1 launched
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
server:value_object_setvalue1: Called (valueIn=-70)
server:value_object_setvalue1: Delaying operation
client-stubs: 6.01:setValue1Completed
client-stubs SUCCESS //正常
client-stubs: 6.02:timerCallback launching setvalue1
client-stubs: 6.02:timerCallback setvalue1 launched
client-stubs: 7.02:timerCallback launching setvalue1
client-stubs: 7.02:timerCallback setvalue1 launched
...
client-stubs:25.04:timerCallback launching setvalue1
client-stubs:25.04:timerCallback setvalue1 launched
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
server:value_object_setvalue1: Called (valueIn=-30)
server:value_object_setvalue1: Delaying operation
client-stubs:26.03:setValue1Completed
client-stubs SUCCESS //正常
client-stubs:26.05:timerCallback launching setvalue1
client-stubs:26.05:timerCallback setvalue1 launched
client-stubs:27.05:timerCallback launching setvalue1
client-stubs:27.05:timerCallback setvalue1 launched
client-stubs:28.05:timerCallback launching setvalue1
client-stubs:28.05:timerCallback setvalue1 launched
client-stubs:29.05:timerCallback launching setvalue1
client-stubs:29.05:timerCallback setvalue1 launched
client-stubs:30.05:timerCallback launching setvalue1
client-stubs:30.05:timerCallback setvalue1 launched
client-stubs:31.02:setValue1Completed //出现问题了
client-stubs ERROR: Did not receive a reply. Possible causes include:
the remote application did not send a reply, the message bus security policy
blocked the reply, the reply timeout expired, or the network connection was
broken.
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
server:value_object_setvalue1: Called (valueIn=-20)
server:value_object_setvalue1: Delaying operation
client-stubs:31.05:timerCallback launching setvalue1
client-stubs:31.05:timerCallback setvalue1 launched
client-stubs:32.03:setValue1Completed
client-stubs ERROR: Did not receive a reply. Possible causes include:
the remote application did not send a reply, the message bus security policy
blocked the reply, the reply timeout expired, or the network connection was
broken.
client-stubs:32.05:timerCallback launching setvalue1
client-stubs:32.05:timerCallback setvalue1 launched
client-stubs:33.03:setValue1Completed
client-stubs ERROR: Did not receive a reply. Possible causes include:
the remote application did not send a reply, the message bus security policy
blocked the reply, the reply timeout expired, or the network connection was
broken.
client-stubs:33.05:timerCallback launching setvalue1
client-stubs:33.05:timerCallback setvalue1 launched
client-stubs:34.03:setValue1Completed
client-stubs ERROR: Did not receive a reply. Possible causes include:
the remote application did not send a reply, the message bus security policy
blocked the reply, the reply timeout expired, or the network connection was
broken.
client-stubs:34.06:timerCallback launching setvalue1
client-stubs:34.06:timerCallback setvalue1 launched
client-stubs:35.03:setValue1Completed
client-stubs ERROR: Did not receive a reply. Possible causes include:
the remote application did not send a reply, the message bus security policy
blocked the reply, the reply timeout expired, or the network connection was
broken.
client-stubs:35.05:timerCallback launching setvalue1
client-stubs:35.05:timerCallback setvalue1 launched
client-stubs:36.04:setValue1Completed
client-stubs ERROR: Did not receive a reply. Possible causes include:
the remote application did not send a reply, the message bus security policy
blocked the reply, the reply timeout expired, or the network connection was
broken.
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
server:value_object_setvalue1: Called (valueIn=-10)
server:value_object_setvalue1: Delaying operation
client-stubs:36.06:timerCallback launching setvalue1
client-stubs:36.06:timerCallback setvalue1 launched
client-stubs:37.04:setValue1Completed
client-stubs ERROR: Did not receive a reply. Possible causes include:
the remote application did not send a reply, the message bus security policy
blocked the reply, the reply timeout expired, or the network connection was
broken.
[Ctrl+c]
[sbox-CHINOOK_X86: ~/glib-dbus-async] >
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
server:value_object_setvalue1: Called (valueIn=30)
server:value_object_setvalue1: Delaying operation
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
server:value_object_setvalue1: Called (valueIn=40)
server:value_object_setvalue1: Delaying operation
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
server:value_object_setvalue1: Called (valueIn=50)
server:value_object_setvalue1: Delaying operation
server:value_object_emitSignal: Emitting signal id 0, with message 'value1'
...
上面发生的异常对时间非常敏感。
Client
侧的定时器回调函数会每隔一秒调用一次异步调用。而
server
侧对每个异步调用都有
5
秒的延时,因此:开始几秒内我们没有看到
server
侧打印什么东西,大概
6
秒后,
client
侧收到了第一个响应。此时,
server
侧实际上收到了
5
个异步调用的请求,还有
4
个没有处理。由于延时和排队的效应,
server
侧不能够及时响应每个请求。
大约
30
秒之后,我看到回调
setValue1Completed
被激发了,但是这次方法调用失败了!并且,从这个时间往后,在
server
累积的方法调用都会在
client
侧反映失败的结果。这是为什么呢?
D-Bus
调用超时!!!
即使我们把
client
侧程序结束掉,
server
侧照样自顾自地继续处理累积的调用申请,而不管
client
侧是否处理调用的结果。
上面的测试结果告诉我们:你需要合理的设计你的服务程序,以免发生函数调用延时的情况,从而导致调用超时
(
失败
)
。同时,你也需要设计一个通知机制:告诉
client
侧,什么事情完成了,而不是采用超时的方式。使用
D-Bus
信号是一种方法(通知机制)。当长时间的操作完成后可以发信号通知
client
侧。
另外,
client
也不要闲着,尽量确保在给定时间段内只有一个方法调用,而不是盲目的去启动
RPC
方法调用:如果前一个调用的响应尚未返回,则推迟启动下一个。
Client
侧和
server
共同努力,尽量避免调用失败的情况。
如果
client
侧确实需要启动多个同样的方法调用,或者有多个
client
程序并行启动同一个
method call,
则
server
侧要有并行处理多个请求的能力。如果具备这种并行处理能力,后面会给些提示。
//D-Bus设置的几个默认超时值
BusConfigParser*
bus_config_parser_new (const DBusString *basedir,
dbus_bool_t is_toplevel,
const BusConfigParser *parent)
{
// ......
{
/* Make up some numbers! woot! */
parser->limits.max_incoming_bytes = _DBUS_ONE_MEGABYTE * 63;
parser->limits.max_outgoing_bytes = _DBUS_ONE_MEGABYTE * 63;
parser->limits.max_message_size = _DBUS_ONE_MEGABYTE * 32;
/* Making this long means the user has to wait longer for an error
* message if something screws up, but making it too short means
* they might see a false failure.
*/
parser->limits.activation_timeout = 25000; /* 25 seconds */
/* Making this long risks making a DOS attack easier, but too short
* and legitimate auth will fail. If interactive auth (ask user for
* password) is allowed, then potentially it has to be quite long.
*/
parser->limits.auth_timeout = 30000; /* 30 seconds */
parser->limits.max_incomplete_connections = 32;
parser->limits.max_connections_per_user = 128;
/* Note that max_completed_connections / max_connections_per_user
* is the number of users that would have to work together to
* DOS all the other users.
*/
parser->limits.max_completed_connections = 1024;
parser->limits.max_pending_activations = 256;
parser->limits.max_services_per_connection = 256;
parser->limits.max_match_rules_per_connection = 512;
parser->limits.reply_timeout = 5 * 60 * 1000; /* 5 minutes */
parser->limits.max_replies_per_connection = 32;
}
parser->refcount = 1;
return parser;
}
从
D-Bus
配置的参数看,前面的
30
秒超时好像和
auth_timeout
一致,但是这又不合逻辑:此时的
connection
已经建立起来了,鉴权已经完成了。后面的
reply_timeout
又是
5
分钟,和
30
秒也不吻合。值得进一步的研究。
原因在这里:
//D-Bus设置的等待消息回应的默认值是
25秒,如果25秒内没有回应,则超时,这就解释了为什么上面会在30秒处失败:25秒等待+5秒延迟打印 = 30秒
/** default timeout value when waiting for a message reply, 25 seconds */
#define _DBUS_DEFAULT_TIMEOUT_VALUE (25 * 1000)
DBusPendingCall*
_dbus_pending_call_new (DBusConnection *connection,
int timeout_milliseconds,
DBusTimeoutHandler timeout_handler)
{
DBusPendingCall *pending;
DBusTimeout *timeout;
_dbus_assert (timeout_milliseconds >= 0 || timeout_milliseconds == -1);
if (timeout_milliseconds == -1)
timeout_milliseconds = _DBUS_DEFAULT_TIMEOUT_VALUE;
//…
}
使用
Glib
的封装实现异步方法调用
有时候,
XML
文件不存在,你没有办法使用
dbus-bindings-tool
来生成
stub
代码。这时候你可以直接使用
Glib
来解决。
直接使用生成的
stub
代码会非常方便,如果我们能在它的基础上做些修改,不是可以减少工作量吗?是的。在一个可以工作的模板基础上修改,好处多多。
好,下面我们看看由工具生成的代码是什么样子:
typedef
void (*org_maemo_Value_setvalue1_reply) (DBusGProxy *proxy,
GError *error,
gpointer userdata);
static
void
org_maemo_Value_setvalue1_async_callback
(
DBusGProxy *proxy,
DBusGProxyCall *call,
void *user_data)
{
DBusGAsyncData *data = user_data;
GError *error = NULL;
dbus_g_proxy_end_call
(
proxy, call, &error, G_TYPE_INVALID);
(*(org_maemo_Value_setvalue1_reply)data->cb) (proxy, error,
data->userdata);
return;
}
static
#ifdef
G_HAVE_INLINE
inline
#endif
DBusGProxyCall*
org_maemo_Value_setvalue1_async
(
DBusGProxy *proxy,
const gint IN_new_value,
org_maemo_Value_setvalue1_reply callback,
gpointer userdata)
{
DBusGAsyncData *stuff;
stuff = g_new (DBusGAsyncData, 1);
stuff->cb = G_CALLBACK (callback);
stuff->userdata = userdata;
return dbus_g_proxy_begin_call (
proxy, "setvalue1", org_maemo_Value_setvalue1_async_callback,
stuff, g_free, G_TYPE_INT, IN_new_value, G_TYPE_INVALID);
}
我们注意到,在
org_maemo_Value_setvalue1_async
中,首先创建一个临时结构变量
DBusGAsyncData *stuff;
这个变量保存一个指向回调函数的指针和
userdata
指针的一份拷贝,今后就用这两个指针来引用回调函数和一些数据。这个临时的结构变量传递给
dbus_g_proxy_begin_call
,同时也把回调函数
(org_maemo_Value_setvalue1_async_callback)
的地址传入。
RPC
调用一完成,回调将会触发。接着就通过那个指向回调函数的指针去引用用户实际传入的回调函数。
下面我们仿照上面的办法搞一个异步调用:在定时器函数中,启动异步操作,同时注册回调函数:
/**
* This function will be called repeatedly from within the mainloop
* timer launch code.
*
* It will launch asynchronous RPC method to set value1 with ever
* increasing argument.
*/
static
gboolean timerCallback(DBusGProxy* remoteobj) {
/* Local value that we'll start updating to the remote object. */
static gint localValue1 = -80;
/* Start the first RPC.
The call using GLib/D-Bus is only slightly more complex than the
stubs. The overall operation is the same. */
g_print(PROGNAME ":timerCallback launching setvalue1/n");
dbus_g_proxy_begin_call(remoteobj,
/* Method name. */
"setvalue1",
/* Callback to call on "completion". */
setValue1Completed,
/* User-data to pass to callback. */
NULL,
/* Function to call to free userData after
callback returns. */
NULL,
/* First argument GType. */
G_TYPE_INT,
/* First argument value (passed by value) */
localValue1,
/* Terminate argument list. */
G_TYPE_INVALID);
g_print(PROGNAME ":timerCallback setvalue1 launched/n");
/* Step the local value forward. */
localValue1 += 10;
/* Repeat timer later. */
return TRUE;
}
当异步调用完成或者超时或者失败时,回调将被激活,在回调里面我们结束异步调用:
/**
* This function will be called when the async setvalue1 will either
* complete, timeout or fail (same as before). The main difference in
* using GLib/D-Bus wrappers is that we need to "collect" the return
* value (or error). This is done with the _end_call function.
*
* Note that all callbacks that are to be registered for RPC async
* notifications using dbus_g_proxy_begin_call must follow the
* following prototype: DBusGProxyCallNotify .
*/
static
void setValue1Completed(DBusGProxy* proxy,
DBusGProxyCall* call,
gpointer userData) {
/* This will hold the GError object (if any). */
GError* error = NULL;
g_print(PROGNAME ":setValue1Completed/n");
/* We next need to collect the results from the RPC call.
The function returns FALSE on errors (which we check), although
we could also check whether error-ptr is still NULL. */
if (!dbus_g_proxy_end_call(proxy,
/* The call that we're collecting. */
call,
/* Where to store the error (if any). */
&error,
/* Next we list the GType codes for all
the arguments we expect back. In our
case there are none, so set to
invalid. */
G_TYPE_INVALID)) {
/* Some error occurred while collecting the result. */
g_printerr(PROGNAME " ERROR: %s/n", error->message);
g_error_free(error);
} else {
g_print(PROGNAME " SUCCESS/n");
}
}
编译:
client-glib:
client-glib.o
$(CC) $^ -o $@ $(LDFLAGS)
# Note that the GLib client doesn't need the stub code.
client-glib.o:
client-glib.c common-defs.h
$(CC)
$(CFLAGS) -DPROGNAME=/"$(basename $@)/" -c $< -o $@
测试的结果和使用生成的异步机制没有什么两样。
注:
在
D-Bus
内部函数中,
pending reply(
等侯回应
)
是用的比较多的概念,这个概念对于前面的超时有莫大的关系。