Skip to content
xiehuc edited this page Jun 10, 2014 · 11 revisions

基本教程

本教程用一个简单的命令行示例,演示快速搭建一个自己的QQ客户端, 并且讲解WebQQ的登录流程

首先需要编译好lwqq库,并且安装到系统目录下.

登录过程

一般lwqq的调用函数是没有顺序的.可以随意使用.不过登录过程必须 严格按照顺序来.登录成功就可以了.

引入头文件 <lwqq/lwqq.h> 然后创建一个LwqqClient:

#include <lwqq/lwqq.h>
int main(int argc,char * argv)
{
    LwqqClient* lc = lwqq_client_new("<username>", "<password>");
}

当然,用户名密码还请填自己的.

同步方式

在继续写之前需要分清楚同步和异步.lwqq完全支持2种方式. 同步方式的请求和处理响应 都在同一段函数中.逻辑比较清晰,适合Demo程序.请求过程是阻塞的,一次只能发出一个请 求. 异步方式的处理响应是在回调函数中进行的,可以做到在等待网络传输时执行其它代 码.效率更高.

两种方式可以动态切换.一般推荐使用异步的书写方式,因为lwqq可以做到在不更改代码的 情况下,将异步方式转换为同步方式.但反之是不行的,因为同步方式的请求和处理是在同一 段函数中,无法分离.

虽然是在设计上提供两种方式, 但是同步工作模式维护比较少, 问题比较多. 推荐简单程 序可以使用同步模式. 复杂程序还是使用异步模式.

lwqq在初始化的时候使用异步,使用下面的方式来设置为同步:

LWQQ_SYNC_BEGIN(lc);
<sync code area>
LWQQ_SYNC_END(lc);

如果不结束同步方式的话,那么整个程序都是同步的了.

设置输出等级

为了能够方便的调试,所以通过不同的输出等级来控制输出的信息的量.有0-5这 几个等级.每个等级的具体含义如下:

  1. 不输出额外的信息
  2. 输出poll轮循的响应
  3. 输出所有请求的响应
  4. 输出所有http请求的url和post
  5. 输出图像传输时候的详细信息
  6. 预留

通常推荐使用3或者是4的等级.使用下面的方式来设置输出等级:

lwqq_log_set_level(4);

处理错误

然后就可以登录了,使用:

LwqqErrorCode err = LWQQ_EC_OK;
lwqq_login(lc, LWQQ_STATUS_ONLINE, &err);

其中第二个参数是设置登录后为在线状态.第三个参数为错误代码.

这里err登录错误会有很多种类.其中需要特殊处理的是需要验证码的错误. 所以在这里用下面的方式来处理:

char vcode[5] = {0};
switch(err){
    case LWQQ_EC_LOGIN_NEED_VC:
        lwqq_util_save_img(lc->vc->data, lc->vc->size, "verify.jpg", NULL);
        printf("Input Verify:");
        scanf("%s",vcode);
        lc->vc->str = s_strdup(vcode);
        break;
    case LWQQ_EC_OK:
        printf("login successful\n");
        break;
    default:
        printf("login failed\n");
        break;
}

这里需要注意的是,使用了 lwqq_util_save_img 来保存验证码,这个函数很好用的 然后提示你输入它,最后需要重新使用 lwqq_login 函数再次登录

于是我们可以用 goto label 或者是外面加一个while循环:

while(1){
    <login>
    <process error code>
}

关闭链接

时刻注意我们是在写C程序,而不是其它,所以要注意内存的释放.:

if(lwqq_client_logined(lc))
    lwqq_logout(lc, NULL);
lwqq_client_free(lc);

最后所有代码如同 :download:`login.c <example/login.c>` 所示.

简单的登录过程先说到这里,下面用以下的命令来编译:

$ gcc -o login_demo login.c -llwqq

执行 ./login_demo 就可以看到登录成功的提示啦:

[login stage 1:get webqq version]

https://ssl.ptlogin2.qq.com/check?uin=<qqnum>&appid=1003903

[Jul 26 12:23:40] ERROR[11263]: type.c:507 lwqq_set_cookie:
    No this cookie:confirmuin
Get response verify code: ptui_checkVC('1','1c4dfb254597d9f944068adf2a8ba4dbca9159327cacf18c','\x00\x00\x00\x00\x95\x1a\x82\x5c');
We need verify code image: 1c4dfb254597d9f944068adf2a8ba4dbca9159327cacf18c
[Jul 26 12:23:40] WARNING[11263]: login.c:666 login_stage_3:
    Need to enter verify code
Input Verify:sksa
[login stage 1:get webqq version]


https://ssl.ptlogin2.qq.com/login?u=<qqnum>&p=348BD6881195F3DE3DC9CCB022837DF6&verifycode=sksa&webqq_type=10&remember_uin=1&aid=1003903&login2qq=1&u1=http%3A%2F%2Fweb.qq.com%2Floginproxy.html%3Flogin2qq%3D1%26webqq_type%3D10&h=1&ptredirect=0&ptlang=2052&daid=164&from_ui=1&pttype=1&dumy=&fp=loginerroralert&action=4-15-15865&mibao_css=m_webqq&t=1&g=1&js_ver=10034

[Jul 26 12:23:53] ERROR[11263]: type.c:507 lwqq_set_cookie:
    No this cookie:superuin
[Jul 26 12:23:53] ERROR[11263]: type.c:507 lwqq_set_cookie:
    No this cookie:superkey
[Jul 26 12:23:53] ERROR[11263]: type.c:507 lwqq_set_cookie:
    No this cookie:ETK
[Jul 26 12:23:53] ERROR[11263]: type.c:507 lwqq_set_cookie:
    No this cookie:airkey
ptuiCB('0','0','https://ssl.ptlogin2.qq.com/pt4_302?u1=http%3A//ptlogin4.web2.qq.com/check_sig%3Fpttype%3D1%26uin%3D<qqnum>%26service%3Dlogin%26nodirect%3D0%26ptsig%3D4WnYCeKCWEjjcs6UWXNoyu6bFbg5pTJyc4lEkLrMTQM_%26s_url%3Dhttp%253a%252f%252fweb.qq.com%252floginproxy.html%253flogin2qq%253d1%2526webqq%255ftype%253d10%26f_url%3D%26ptlang%3D2052%26ptredirect%3D100%26aid%3D1003903%26daid%3D164%26j_later%3D0%26low_login_hour%3D0%26regmaster%3D0','0','登录成功!', 'd3dd');


r=%7B%22status%22%3A%22online%22%2C%22ptwebqq%22%3A%22a54ff83bd9a03825b36837735b2cad136184f28253caa3ad32475a2d847ad6bc%22%2C%22passwd_sig%22%3A%22%22%2C%22clientid%22%3A%2247795973%22%2C%20%22psessionid%22%3Anull%7D

{"retcode":0,"result":{"uin":<qqnum>,"cip":1858033426,"index":1075,"port":48293,"status":"online","vfwebqq":"205b9fb2165f80abd7936316b4da66d2ac4339a39c21ea19d9bed03070a9bc40d1c6113a3fe9a0bc","psessionid":"8368046764001f636f6e6e7365727665725f7765627171403137322e32332e3133342e32313600007d5e00000686026e04005c821a956d0000000a40707949306d696c44706d00000028205b9fb2165f80abd7936316b4da66d2ac4339a39c21ea19d9bed03070a9bc40d1c6113a3fe9a0bc","user_state":0,"f":0}}


login successful

获取初始信息流程

初始信息最重要的是三个list:friends list, group list, discu list以及QQ号.QQ号非 常的麻烦,因为webqq在获取的初始信息中没有提供QQ号,需要对每一个好友发送一个请求获 取QQ号.所以如果好友特别多的话,就产生了大量的网络通讯.并且腾讯服务器对于QQ号的获 取频率有限制,很快就会超过限制,从而获取失败.通常使用本地的数据库来缓存QQ号,好在 lwqq已经内置了数据库的支持,可以方便的完成这个功能

异步书写的同步调用

大部分网络请求都需要获取错误号,以便判断是否成功的执行,如果错误那么错误又是什么. 在旧的API接口中,通常使用一个 LwqqErrorCode *err 的参数来获取错误号.但是新的API 接口中没有相应的参数.这里需要使用异步的书写方式,才能成功获取到,因为此处没有使用 LWQQ_SYNC_END(lc) 来关闭同步方式.所以虽然这里书写方式是异步的,但是其实依然是同 步调用.

那么这里为什么需要首先介绍这种混合模式而不直接开启异步呢?因为这里和异步方式设计 的历史原因有关系.

一开始lwqq是同步的,但是对于pidgin这种的明显是不行的.所以需要异步支持.细分为请求 过程和处理过程.通常UI程序的主线程都有一个事件循环,所有的UI事件都在循环中运行.于 是这里就可以把请求过程和处理过程都放在这个事件循环中.当请求时,这个事件就被移到 了队列的末尾,当收到服务器的响应后,再去执行对应的处理过程.所以到此为止,已经是异 步化了.

但是问题不仅如此,在一些系统的libcurl中,如ubuntu,在查询dns的时候依然是同步的.也 就是说,在查询结束之前,都会一直阻塞.因此这里就阻塞了事件循环,使得UI事件不能及时 的处理,从而出现了卡UI的现象了.那么这里怎么办?通过创建一个新的线程,在上面建立另 外一个事件循环,如libev,然后把网络请求的过程全部放在新线程中.这样,就算dns查询阻 塞了,也不会阻塞主线程,因此就解决了卡UI的问题了.

接下来,新问题是处理过程应该是放在新线程中还是主线程中?答案是前者,因为一些事件循 环,如gtk的,要求一些操作UI的代码必须在主线程中执行,在别的线程中执行会出现竞争问 题,也就是说,它不是线程安全的.处理过程经常涉及到更新UI,所以必须放在主线程中执行.

到此,整个异步设计就完成了.需要注意的是,设计中要求必须有两个事件循环分别运行在主 线程,和子线程中.

因为这里主线程还没有建立事件循环,当请求过程在子线程中执行时,主线程没有什么代码 执行的了,于是就结束了.整个程序就退出了.因此这里还不能简单的打开异步.

获取好友列表

使用如下的代码可以获取好友列表:

LwqqAsyncEvent* ev = lwqq_info_get_friends(lc,NULL,NULL);

其中第二个参数是hash函数,设置为NULL来使用内置的版本.因为hash函数经常变动嘛,尝试 使用多种版本的hash函数就交给调用者来实现了. 很多lwqq的函数都会返回一个 LwqqAsyncEvent 结构.这个结构非常的好用.当收到服务器 的响应之后,其中result的结果就可用了.

  • 如果 ev->result==0 那么表示没有错误发生.
  • 如果 ev->result>0 表示webqq服务器发回的错误号.例如50就表示hash错误.还有102,118 表示掉线等等.
  • 如果 ev->result<0 表示lwqq的错误,例如网络不可用啦,或者是超时等等.

可以通过错误号来判断是否执行成功:

if(ev->result != LWQQ_EC_OK){
    if(ev->result == LWQQ_EC_HASH_WRONG){
        printf("hash wrong\n");
        exit(0);
    }else{
        printf("could not get friends info\n");
        exit(-1);
    }
}

要是所有错误号都处理的话是很辛苦的.所以一般只处理常见的错误就可以了.

最后,需要使用特殊的 lwqq_async_event_finish(ev) 来正确的释放内存.

获取群组列表

当然,上面的方式完全没有体现出 LwqqAsyncEvent 的威力,所以在下面使用另外的一种方 式来调用:

ev = lwqq_info_get_group_name_list(lc, NULL);
lwqq_async_add_event_listener(ev, _C_(p,common_error_process,ev));
ev = lwqq_info_get_discu_name_list(lc);
lwqq_async_add_event_listener(ev, _C_(p,common_error_process,ev));

static void common_error_process(LwqqAsyncEvent* ev)
{
    if(ev->result!=LWQQ_EC_OK){
        printf("get group list failed\n");
    }
}

lwqq_async_add_event_listener 为一个事件添加了回调函数.其中回调函数使用 _C_ 宏 封装了一下. _C_ 宏被称为不定参回调,它的好用不亲自尝试一下是体会不到的:).它的第 一个参数是回调函数模板,例如p表示只有一个指针,即 (void*) ,2p表示有两个指针作为参 数,即 (void*,void*) ,2pi表示两个指针后一个int参数.最多到4p.其实展开之后是对应的 vp_func_## 所以要是你觉得参数还不够的话,可以自己写一个 vp_func_2pl3i 的参数模 板.表示 (void*,void*,long,int,int,int) 的参数列表.因为指针的长度都是一样的,所以 无论是 LwqqAsyncEvent* 还是 LwqqClient* 都可以用等数量的p

_C_ 宏的第二个参数是回调函数的入口,剩下的就是依次排列的实参列表了.要是你用了4p 但是后面只写了2个指针,或者用了2p但是写了3个指针会怎么样呢?那就只能呵呵了.因为没 有编译器检查(不知道该怎么写,就像printf一样的检查参数).所以不会报错,但是运行的时 候就会出错了.而且都是非常诡异的错误,基本看不出来是怎么回事.所以写的时候一定要匹 配.否则很难发现错误了.

另外 LwqqAsyncEvent 还可以绑定多个回调函数.另外使用了`add_event_listener`之后就 不用管内存了,会正确的释放内存的.

启动轮循(poll)

最后,就可以开启poll线程,接受webqq服务器发过来的消息了.此时会创建一个新的线程.所 以同样的,需要让主线程也定期的执行检查过程,否则就直接退出程序了:

LwqqRecvMsgList *l = lc->msg_list;
l->poll_msg(l,0);

此时就会新开一个线程用于poll轮循.然后在主函数中继续执行检查循环,如下:

while(1){
    LwqqRecvMsg *recvmsg;
    pthread_mutex_lock(&l->mutex);
    if (TAILQ_EMPTY(&l->head)) {
        /* No message now, wait 100ms */
        pthread_mutex_unlock(&l->mutex);
        usleep(100000);
        continue;
    }
    recvmsg = TAILQ_FIRST(&l->head);
    TAILQ_REMOVE(&l->head,recvmsg, entries);
    pthread_mutex_unlock(&l->mutex);
    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);
}

从第2-12行是使用互斥锁来获取一个RecvMsg对象,第13行检查recvmsg的类型是否是Message类型, 然后强制转换结构体并取得Content列表.最后把所有是字符串的打印出来.

最后所有代码如同 :download:`info.c <example/info.c>` 所示.