-
Notifications
You must be signed in to change notification settings - Fork 25
Tutorial Advanced
本教程讲述复杂的异步事件和LwqqAction
在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);
既然要发送消息,首先需要额外的区分一下指令:例如我们用`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,可以构建一个复杂的具有敛散特征的网络请求拓扑图.
异步过程也必须严格按照一定的顺序开启,否则容易出现问题.在以下简单总结以下:
- 开启异步的flag : LWQQ_SYNC_END(lc);
- 创建异步loop : loop = ev_loop_new(EVBACKEND_POLL);
- 指定dispatch : lc->dispatch = local_dispatch;
- 指定其它初始化函数 : lc->dispatch(_C_(p,startup,lc));
- 启动异步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号缓存功能,必须按照一定的顺序来完成一系列步骤.否则则会出现 数据混乱:
- 使用`lwqq_info_get_friends`从服务器获取好友列表
- 使用用flush擦除数据库中旧的数据
- 在本地数据库查找QQ号并写入LwqqClient
- 在本地数据库查找好友信息并更新LwqqBuddy和LwqqGroup
- 需要更新的内容和有二义性的内容则发送网络请求
- 当全部下载完成后把更新部分的内容写回数据库
下面是对该过程的详细解释:
- 如果先用数据库中的内容来初始化`LwqqClient`,当从网络获取好友列表后,则无法区 分那些本来已经被删除的好友.
- 为了能够保证数据的及时更新,同时又避免大量的网络通信,通过擦除最旧的n个数据. 那么从网络上再把这些数据的最新内容下载回来,如此就形成了一个循环.保证了数据 一定能够在某个时候得到更新.`lwdb_userdb_flush_buddies`和 `lwdb_userdb_flush_groups`是擦除最后@last个信息超过@day的条目.擦除只是表示 如昵称,等级之类的信息过于陈旧了.但是QQ号是一定不会受到影响的.
- 使用`lwdb_userdb_query_qqnumbers`可以一次性将所有的好友和群的QQ号填充.如果 没有找到该好友或群,则它的`last_modify`被设置为`LWQQ_LAST_MODIFY_UNKNOW`,如 果它是已经被擦除了.则它的`last_modify`被设置为`LWQQ_LAST_MODIFY_RESET`,表明 需要手工更新数据.
- `lwdb_userdb_query_buddy`和`lwdb_userdb_query_group`是根据QQ号来工作的,为了 能够顺利更新数据,必须保证通过(3)获取到了QQ号码.
- 当`last_modify`为`LWQQ_LAST_MODIFY_UNKNOW`时,需要同时下载QQ号和其它信息.可 以使用`LwqqAsyncEvset`来完成.当`last_modify`为`LWQQ_LAST_MODIFY_RESET`时,只 需要下载其它信息.
- 通过`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.