1)当Login2成功登陆后,立刻兴冲冲的想通过webQQ发信,结果却让很多人失望:即使没返回错误信息,发送也是不成功的,因为还缺了一步,那就是webQQ的POLL。
一切都源于webQQ保持连线的机制:Login2成功登陆后,要保持webQQ不断线,就需要不间断的poll,来获取服务器传过来的消息;即使你对返还的信息不感兴趣,你也得不断的poll,因为大约数分钟后,服务器如果没有收到用户的poll请求,会认为QQ用户已经下线或断线,继而会停掉一切相关的webQQ服务。
抓包:
POST http://d.web2.qq.com/channel/poll2 HTTP/1.1
Referer:http://d.web2.qq.com/proxy.html
r={"clientid":"【clientid】","psessionid":"【psessionid】","key":0,"ids":[]}&clientid=【clientid】&psessionid=【psessionid】
C++ (with libcurl)源程序
string WebQQ_poll()
{
// 提取QQ登录信息:
char buf[1024];
size_t found,found2;
string clientid,psessionid;
ifstream QQLoginfile("D:\\SparkHo\\QQLogin.txt");
if(!QQLoginfile)
{
QQLoginfile.close();
clientid = "";
psessionid = "";
}
else
{
QQLoginfile.getline(buf,1024);
string QQLogin = (string) buf;
QQLoginfile.close();
found = QQLogin.find("ClientID");
found2 = QQLogin.find(";",found+9,1);
clientid = QQLogin.substr(found+9,found2-found-9);
found = QQLogin.find("psessionid");
found2 = QQLogin.find(";",found+11,1);
psessionid = QQLogin.substr(found+11,found2-found-11);
}
// 初始化libcurl
CURLcode return_code;
return_code = curl_global_init(CURL_GLOBAL_WIN32);
if (CURLE_OK != return_code) return "";
// 获取easy handle
CURL *easy_handle = curl_easy_init();
if (NULL == easy_handle)
{
curl_global_cleanup();
return "";
}
string buffer;
string post_url = "http://d.web2.qq.com/channel/poll2";
string referer_url = "http://d.web2.qq.com/proxy.html";
string base = "{\"clientid\":\""+clientid+"\",\"psessionid\":\""+psessionid+"\",\"key\":0,\"ids\":[]}";
string urlencode = curl_easy_escape(easy_handle,base.c_str(),0);
string fields = "r=" + urlencode + "&clientid=" + clientid + "&psessionid=" + psessionid;
// 设置easy handle属性
curl_easy_setopt(easy_handle, CURLOPT_URL, post_url.c_str());
curl_easy_setopt(easy_handle, CURLOPT_REFERER, referer_url.c_str());
curl_easy_setopt(easy_handle, CURLOPT_POST, 1);
curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDS, fields.c_str());
curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, writer);
curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, &buffer);
// 提交保存的cookie
curl_easy_setopt(easy_handle, CURLOPT_COOKIEFILE,"D:\\SparkHo\\cookie_login.txt");
// 执行数据请求
return_code = curl_easy_perform(easy_handle);
if (CURLE_OK != return_code)
buffer = "";
// 释放资源
curl_easy_cleanup(easy_handle);
curl_global_cleanup();
return buffer;
}
poll的返回值,也是一个JSON结构,如:{"retcode":103,"errmsg":""} {"retcode":121,"t":"0"} {"retcode":100006,"errmsg":""}
返回103、121,代表连接不成功,需要重新登录;
返回102,代表连接正常,此时服务器暂无信息;
返回0,代表服务器有信息传递过来:包括群信、群成员给你的发信,QQ好友给你的发信。
另外,此程序必须考虑另一种情况:如果网络断线,curl_easy_perform()将返回CURLE_COULDNT_RESOLVE_HOST,返回值buffer为了避免无效赋值:必须赋值为空字符串。
发生错误或有信息到来之时,poll马上返回相应的JSON,占用的时间很短;
但没有信息过来的时候,poll占用的时间一般比较长,不确定,时快时慢。
2)如果等poll获得返回值之后,再去响应用户其他的动作,会感觉有点卡。于是我们另开一个线程,循环不断的poll,而不影响主程序的正常响应,这就是所谓的异步通信;如获得有效信息,即可通过SendMessage消息机制传回主窗口。
线程程序:
DWORD WINAPI ThreadOverPoll(LPVOID n)
{
size_t found;
HWND hfwnd = FindWindow(NULL,"webQQ专用");
while(thread_run)
{
string poll = WebQQ_poll();
if(poll == "")
{
// 网络断线,暂停5秒
Sleep(5000);
}
else
{
found = poll.find(",",11,1);
string ret = poll.substr(11,found-11);
int i = atoi(ret.c_str());
// webQQ断线重连
if(i == 103 || i == 121 || i == 100006)
WebQQ_restart();
// 把有效的信息,传回主窗口
if(atoi( i == 0 )
SendMessage(hfwnd,WM_QPOLL,(WPARAM)poll.c_str(),0);
}
}
return (DWORD)0;
}
注:
1)当网络断线的时候,WebQQ_poll()返回空字符串,线程不作处理,暂停5秒;
2)当网络再度连上的时候,可能webQQ已经断线,WebQQ_poll()返回121或103,这时候启动WebQQ_restart(),进行重连;
3)有一种情况,需要窗口去处理,当QQ用户在其他地方登陆的时候,返回值:
{"retcode":0,"result":[{"poll_type":"kick_message","value":{"way":"poll","show_reason":1,"reason":"您的账号在另一地点登录,您已被迫下线。如有疑问,请登录 safe.qq.com 了解更多。"}}]}
线程的启用:
全局变量int thread_run初始赋值为0
......
thread_run=1;
hThread1 = CreateThread(NULL, 0, ThreadOverPoll, (LPVOID)1, 0, &thread_ID);
......
线程的关闭:
......
thread_run=0;
for (;;)
{
GetExitCodeThread(hThread1, &exitCode1);
GetExitCodeThread(hThread2, &exitCode2);
if ( exitCode1 != STILL_ACTIVE && exitCode2 != STILL_ACTIVE )
break;
}
CloseHandle(hThread1);
CloseHandle(hThread2);
......
附录一:
重新登录:C++ (with libcurl)源程序
string WebQQ_restart()
{
size_t found;
char userno[100],pass[100];
// 从主窗口获取用户登录信息
HWND hfwnd = FindWindow(NULL,"webQQ专用");
GetDlgItemText(hfwnd,Edit_User,userno,100);
GetDlgItemText(hfwnd,Edit_Pass,pass,100);
string VeriCode = WebQQ_check(userno);
// 获取验证码后,变更主窗口信息
SetWindowText(GetDlgItem(hfwnd,Edit_Veri),VeriCode.c_str());
string PassCode = qqMD5(pass,VeriCode);
string ptwebqq = WebQQ_login(userno,PassCode,VeriCode);
string LogJson = WebQQ_login2();
found = LogJson.find(",",11,1);
string ret = LogJson.substr(11,found-11);
if(atoi(ret.c_str())==0)
{
string follow1 = WebQQ_buddy();
string follow2 = WebQQ_group();
//string follow3 = WebQQ_gface_sig();
fstream QQsendfile("D:\\SparkHo\\alert_today.txt",ios::out|ios::trunc);
QQsendfile.close();
alert_no = 0;
}
return LogJson;
}
附录二:
上面提到的webQQ被踢的情况(用户在其他地方登陆的时候),这时候poll的返回值:
{"retcode":0,"result":[{"poll_type":"kick_message","value":{"way":"poll","show_reason":1,"reason":"您的账号在另一地点登录,您已被迫下线。如有疑问,请登录 safe.qq.com 了解更多。"}}]}
该信息,通过SendMessage消息机制传回主窗口,窗口相应的处理程序(启动重新登录):
......
switch (message)
{
case WM_QPOLL: // 收到了自定义的信息,来自POLL
{
MessageBeep(MB_ICONINFORMATION);
string poll = (char *)wParam;
found = poll.find("poll_type");
found2 = poll.find("\":\"",found+1,3);
found3 = poll.find("\"",found2+3,1);
string ret = poll.substr(found2+3,found3-found2-3);
if(ret.compare("kick_message")==0)
{
Sleep(60000); //暂停60秒
string relogin = WebQQ_restart();
}
if(ret.compare("message")==0)
{
//处理QQ好友的来信 ...
}
if(ret.compare("group_message")==0)
{
//处理QQ群的来信 ...
}
......
}
......
}
......