Skip to content
xiehuc edited this page Apr 20, 2014 · 3 revisions

本教程讲述复杂的异步事件和LwqqAction

创建eventloop

在Primer教程中,最后做到了接收轮循这一步,但是要再做下去就会变得非常困难了.因为我 们没有额外的空间来执行发送操作,整个进程都是不断的在接收循环中运行.没有机会停下 来执行发送的过程.所以我们需要使用更高级的手段.

我们可以用libev或者是其它的来创建主线程的eventloop

loop = ev_loop_new(EVBACKEND_POLL);

因为lwqq的libev使用的是POLL后端,所以最好这里也使用一样的,而不是EPOLL,减少冲突的 可能.然后,我们需要指定本地的dispatch函数:

static struct ev_loop * loop;

typedef struct {
    ev_timer timer;
    LwqqCommand cmd;
}dispatch_t;

static void local_do_dispatch(EV_P_ ev_timer* w,int revents)
{
    dispatch_t *dis = w->data;
    ev_timer_stop(loop, w);
    vp_do(dis->cmd, NULL);
    s_free(dis);
}

static void local_dispatch(LwqqCommand cmd)
{
    dispatch_t* dis = s_malloc0(sizeof(*dis));
    dis->cmd = cmd;
    dis->timer.data = dis;
    ev_timer_init(&dis->timer, local_do_dispatch, 0.1, 0);
    ev_timer_start(loop, &dis->timer);
}

///in main()
lc->dispatch = local_dispatch;

这里需要介绍一下相关的概念.因为eventloop有两个,需要在两个eventloop中安排合适的 处理代码.为了能够在eventloop中任意的切换,一种简单有效的方式是使用一个很小的计时 器timer,因为时间很短,所以可以基本保证执行代码不会有很大的延迟.因为timer是在 eventloop中的.所以一定可以保证timer是在对应的eventloop所在的线程执行的.因此可以 保证切换eventloop.所以一次dispatch就等于设置了一个计时器.

lwqq当需要使用自己的eventloop时候会调用`lwqq_async_dispatch`.当需要使用主线程会 调用`lc->dispatch`.因此你需要做的是覆盖`lc->dispatch`,添加一个主eventloop的 timer并执行它.上面的例子正是完成的这以工作.

lc->dispatch 初始化时是等于 lwqq_async_dispatch 的.也就是说,即使你不覆盖 dispatch,也不会造成程序错误.只是网络请求和处理代码都挤在了lwqq的eventloop中执 行.这对于简单的命令行程序可能没有什么大的影响,但是对于一些gui程序,如gtk,就会造 成资源冲突.

然后,我们就需要在main函数中启动eventloop了.再启动之前,我们可以给它加入一个监听0 号fd的io事件,因为0号fd表示标准输入流,实际上是说,在侦测到输入命令之后执行一些回 调函数:

ev_io io;
io.data = lc;
ev_io_init(&io, input_cb, 0, EV_READ);
ev_io_start(loop, &io);

ev_run(loop, 0);
ev_loop_destroy(loop);

发送QQ消息

既然要发送消息,首先需要额外的区分一下指令:例如我们用`quit`或`q`表示退出程序.用 send`或者`s`表示发送消息.用`who`表示查看`uin.因此下面列出了一个简单的实现.

1 static void input_cb(EV_P_ ev_io* w,int revents) 2 { 3 LwqqClient* lc = w->data; 4 char command[30]; 5 fscanf(stdin, "%s",command); 6 if(!strcmp(command,"quit")||!strcmp(command,"q")){ 7 ev_io_stop(loop, w); 8 ev_break(loop, EVBREAK_ALL); 9 return; 10 } 11 if(!strcmp(command,"send")||!strcmp(command,"s")){ 12 char uin[64]; 13 char message[2048]; 14 fscanf(stdin, "%s %s",uin,message); 15 lwqq_msg_send_simple(lc, LWQQ_MS_BUDDY_MSG, uin, message); 16 return; 17 } 18 if(!strcmp(command,"who")){ 19 LwqqBuddy* b; 20 LIST_FOREACH(b,&lc->friends,entries){ 21 printf("[nick:%s t uin:%s t mark:%s]n",b->nick,b->uin,b->markname?:""); 22 } 23 return; 24 } 25 }

首先,第5行表示读入指令长度.

  • 在第6-10行处理如果是`quit`的情况.代码涉及到一些libev的知识,这里就省略了.
  • 在第11-17行处理的是`send`的情况.继续扫描出接下来的uin和消息内容.uin是一个关 键参数,webqq通信时候并不直接使用qq号,而是使用每天都会全部刷新一遍的uin,如果 是要发送消息,需要先找到好友的uin才能够发送.因为很多好友的昵称都是用的火星文 ,要是用`send nick message`的格式的话,估计输入昵称会发疯的.所以不如使用`send uin message`的格式了.就算uin比较长,也比火星文来的好.
  • 最后,既然要查询uin,就用`who`指令了.`LIST_FOREACH`是用来遍历好友列表的宏,具 体知识可以参考`BSD的queue.h`,简单说来,它是一个轻量级的c数据结构支持,完全使 用宏定义出了常用数据结构.

异步轮循方式

因为使用了异步了,所以就没有必要用以前低效的`while(1)`来打印poll的内容.另外poll 不像一般的如获取好友等等的请求,它需要执行很多遍,不能返回一个`LwqqAsyncEvent`,类 似于这种不方便返回async event的会使用`LwqqAction`结构体来实现.

static LwqqAction act = {
.poll_msg = poll_msg, .poll_lost = poll_lost

};

//in main() lc->action = &act;

LwqqAction提供了很多有用的调用,如poll有消息来到的`poll_msg`,掉线的`poll_lost`, 处理所有的验证码的`need_verify2`:因为在很多地方需要验证码,登录,查找好友,查找群 等等的.如果在这些地方都对验证码处理,功能分散且冗余,会是一件很烦人的事情.使用 `need_verify2`可以一次性解决所有验证码相关功能.非常的好.

static void poll_msg(LwqqClient* lc) {

LwqqRecvMsg *recvmsg,*bak; pthread_mutex_lock(&l->mutex); if (TAILQ_EMPTY(&l->head)) {

pthread_mutex_unlock(&l->mutex); return;

} recvmsg = TAILQ_FIRST(&l->head); TAILQ_FOREACH_SAFE(recvmsg, &l->head, entries, bak){

TAILQ_REMOVE(&l->head,recvmsg, entries); if(lwqq_mt_bits(recvmsg->msg->type) == LWQQ_MT_MESSAGE){

LwqqMsgMessage* msg = (LwqqMsgMessage*)recvmsg->msg; LwqqMsgContent* c; TAILQ_FOREACH(c, &msg->content, entries){

if(c->type == LWQQ_CONTENT_STRING){
printf("%s",c->data.str);

}

} printf("n");

} lwqq_msg_free(recvmsg->msg); s_free(recvmsg);

} pthread_mutex_unlock(&l->mutex);

}

static void poll_lost(LwqqClient* lc) {

printf("[lost connection]n"); lc->msg_list->poll_close(lc->msg_list); exit(0);

}

`poll_msg`的代码和Primer中是类似的.不过这里需要使用FOREACH及早的处理消息,因为异 步调用会造成一些时间差,这种差距积累多了也会造成明显的差异.

在`poll_close`中则是简单的`exit`,虽然这样做的确不大好,但是为了保持example的简洁 紧凑,不能使用过于复杂的通知机制来正确的释放内存资源.

最后使用`gcc eventloop.c -llwqq -lev`来编译.所有代码见 [eventloop.c](example/eventloop.c)

大规模并发请求

下面介绍`LwqqAsyncEvset`的概念以及用法.`LwqqAsyncEvent/Evset`和 `LwqqCommand(_C_)`为lwqq的两大基石.lwqq内部大量的使用了这些方便的工具来完成复杂 的网络请求.`LwqqCommand`使得给回调函数增加参数特别方便,能够非常快速的修改函数原 型.`LwqqAsyncEvent`则是提供了网络请求回调的通用方法,用来侦听一个网络事件非常方 便.那么,如果是要同时侦听多个网络事件呢?这就是`LwqqAsyncEvset`出场的时候了.

`LwqqAsyncEvset`就是`LwqqAsyncEvent`的集合.通过`lwqq_async_evset_add_event`将一 个event加入evset.一个event只能加入一个evset,但是它依然可以自由的使用 `lwqq_async_add_event_listener`来特别处理.当所有event都**finish**后,一个evset就 被触发了.通过`lwqq_async_add_evset_listener`来指定回调参数.

当一个evset被成功的触发后,它会被自动的释放内存.一个特殊情况是,如果一个evset没有 添加任何事件,那么在`lwqq_async_add_evset_listener`时候就会被删除了.所以总是先加 入事件后指定回调函数.

下面用一个简单的示例来演示:假设我们要取得所有用户的昵称(long_nick),可以同时申 请大量的网络请求,然后集中处理(比如写数据库),这样能够保证数据库的高效.并且提供了 一个保证:调用evset的回调的时候,一定已经全部获取了QQ号了.

#include<lwqq/lwdb.h>

static void all_qqnumber_got(LwqqClient* lc) {

LwdbUserDB* db = lwdb_userdb_new("2501542492", NULL, 0); LwqqBuddy* b; lwdb_userdb_begin(db); LIST_FOREACH(b,&lc->friends,entries){

lwdb_userdb_insert_buddy_info(db, b);

} lwdb_userdb_commit(db); lwdb_userdb_close(db);

}

static void startup(LwqqClient* lc) {

LwqqBuddy* b; LwqqAsyncEvent* ev; LwqqAsyncEvset* set = lwqq_async_evset_new(); LIST_FOREACH(b,&lc->friends,entries){

ev = lwqq_info_get_friend_qqnumber(lc, b); lwqq_async_evset_add_event(set, ev);

} lwqq_async_add_evset_listener(set, _C_(p,all_qqnumber_got,lc));

}

///in main() lc->dispatch(_C_(p,startup,lc));

///<startup local event loop>///

需要注意的是,异步loop必须尽早启动,为了能够配合lwqq开启的异步,让整个事件循环能够 流动起来.但是一旦loop启动后,就会阻塞主线程,就没有机会再执行其它的初始化代码了. 所以通常的做法是在启动loop之前先设置好一个timer,因为此时loop还没有启动,所以 timer是阻塞在事件队列中的.当启动了loop之后,首先检查timer,会发现早就超时了.于是 赶紧执行回调.就成功的进入到了初始化回调函数中了.

那为什么要使用这种迂回的方式?而不是直接把初始化代码方在启动loop之前?因为每一个 lwqq网络调用都会伴随着产生一次dispatch,又因为此时loop还没有运行,则会堆积大量的 timer.不仅如此,堆积的事件成份很复杂,可能还有io事件等等.所以此时再开启loop会很容 易产生各种错误.至少在libev中是这样的.前面用的方式虽然也是预先设置一个timer,但是 没有堆积成份复杂的事件,所以是正确的.等到回调到初始化函数中的时候,实际上loop已经 启动了.也就是一种延迟初始化的思想.

在gui环境中,因为已经开启了ui的eventloop,所以此时就不再涉及loop的启动顺序的问题.

最后使用`gcc eventloop.c -llwqq -lev`来编译.所有代码见 [eventloop.c](example/eventloop.c)

类似的还有很多大规模并发请求,如获取头像,获取昵称等等的很多地方都需要使用到 LwqqAsyncEvset.同时,一些可并发的请求也可以用evset来加快速度,比如在登录时候好 几个地方可以并发,如获取好友列表,获取群列表,获取讨论组列表这三个请求.最后,通过 event和evset,可以构建一个复杂的具有敛散特征的网络请求拓扑图.

异步开启的顺序

异步过程也必须严格按照一定的顺序开启,否则容易出现问题.在以下简单总结以下:

  1. 开启异步的flag : LWQQ_SYNC_END(lc);
  2. 创建异步loop : loop = ev_loop_new(EVBACKEND_POLL);
  3. 指定dispatch : lc->dispatch = local_dispatch;
  4. 指定其它初始化函数 : lc->dispatch(_C_(p,startup,lc));
  5. 启动异步loop: : ev_run(loop, 0);

其顺序的确定根据 1. 启动loop后会阻塞主线程,故必须放在最后 2. 其它初始化要求已经 启动loop

数据库支持

在上面的例子中已经用到了数据库支持了.因为QQ号请求不能频繁的发送,所以有必要在本 地建立数据库做缓存.本节介绍数据库支持的原理和易于使用的数据库支持API.需要有一定 的数据库知识.

数据库支持都默认编译进了liblwqq中,所以直接包含头文件`<lwqq/lwdb.h>`来启用数据库 .因为一些历史原因,数据库类被成为LwdbUserDB.每一个账户都对应一个db.另外还有一个 LwdbGlobalDB.是为了兼容旧程序的类,本身没有被维护了,不保证能用.

创建lwdb可以使用`LwdbUserDB* db = lwdb_userdb_new(qqnum, NULL, 0);` 其中第二个参数是文件夹路径.使用NULL则直接使用默认路径.Linux中为 ${HOME}/.config/lwqq,Windows中为`%APPDATA%/lwqq`(大概).

为了能够正确的使用QQ号缓存功能,必须按照一定的顺序来完成一系列步骤.否则则会出现 数据混乱:

  1. 使用`lwqq_info_get_friends`从服务器获取好友列表
  2. 使用用flush擦除数据库中旧的数据
  3. 在本地数据库查找QQ号并写入LwqqClient
  4. 在本地数据库查找好友信息并更新LwqqBuddy和LwqqGroup
  5. 需要更新的内容和有二义性的内容则发送网络请求
  6. 当全部下载完成后把更新部分的内容写回数据库

下面是对该过程的详细解释:

  1. 如果先用数据库中的内容来初始化`LwqqClient`,当从网络获取好友列表后,则无法区 分那些本来已经被删除的好友.
  2. 为了能够保证数据的及时更新,同时又避免大量的网络通信,通过擦除最旧的n个数据. 那么从网络上再把这些数据的最新内容下载回来,如此就形成了一个循环.保证了数据 一定能够在某个时候得到更新.`lwdb_userdb_flush_buddies`和 `lwdb_userdb_flush_groups`是擦除最后@last个信息超过@day的条目.擦除只是表示 如昵称,等级之类的信息过于陈旧了.但是QQ号是一定不会受到影响的.
  3. 使用`lwdb_userdb_query_qqnumbers`可以一次性将所有的好友和群的QQ号填充.如果 没有找到该好友或群,则它的`last_modify`被设置为`LWQQ_LAST_MODIFY_UNKNOW`,如 果它是已经被擦除了.则它的`last_modify`被设置为`LWQQ_LAST_MODIFY_RESET`,表明 需要手工更新数据.
  4. `lwdb_userdb_query_buddy`和`lwdb_userdb_query_group`是根据QQ号来工作的,为了 能够顺利更新数据,必须保证通过(3)获取到了QQ号码.
  5. 当`last_modify`为`LWQQ_LAST_MODIFY_UNKNOW`时,需要同时下载QQ号和其它信息.可 以使用`LwqqAsyncEvset`来完成.当`last_modify`为`LWQQ_LAST_MODIFY_RESET`时,只 需要下载其它信息.
  6. 通过`lwdb_userdb_insert_buddy`和`lwdb_userdb_insert_group`来更新数据库.你可 以总是使用insert而不是update,update在数据库没有条目的时候会出错.

在一些insert和query的函数中可以开启SQLite的事务功能,从而大幅度优化sql执行.如:

lwdb_userdb_begin(db); <<here to insert or query>> lwdb_userdb_commit(db);

所以最好将sql相关代码集中到一起处理.其实现是通过指定`enable_cache`来缓存sql,从 而在开启事务的条件下重复执行同样的操作,能直接从缓存中取出预编译好的sql.

Clone this wiki locally