Release 实现异步更新网络图片 · bajdcc/GameFramework · GitHub
写在前面
计划着将一些好用的东西整合进框架中,目前用了libevent和libcurl,仅当尝鲜。话说libcurl的使用其实很简单,跟php的curl扩展差不多。libevent是初次使用,很多坑尚未发现。
简单介绍下封面界面的构成:必应背景、一言API、文字、二维码。其中新增的是前二个:必应背景和一言文字。
下面是主要内容:
- libevent和libcurl的编译与使用
- 如何实现异步刷新,且涉及网络请求
使用libevent
项目中需要用到libevent,实现异步通知功能。libevent支持网络/文件IO、定时器、信号。在这里,我们只需要用到其中的定时器功能。
vs2015中编译静态库libevent
- 下载libevent源码libevent-2.0.22-stable.tar.gz
- 下载Makefile.nmake到libevent目录,默认是release版本;若需要编译debug版本,需要修改其中的第26行`CFLAGS=$(CFLAGS) /Ox /W3 /wd4996 /nologo`为`CFLAGS=$(CFLAGS) /Od /Zi /W3 /wd4996 /nologo`
- 开始菜单=>vs2015开发人员命令提示,cd切换到libevent目录,执行命令nmake /F makefile.nmake;如果要清空上一次编译的结果,执行nmake /F makefile.nmake clean
- 复制目录下的libevent.lib、libevent_core.lib、libevent_extras.lib到项目中
libevent的简单使用
void msg_timer(evutil_socket_t fd, short event, void *arg) { /*do something...*/ } struct event_base *evbase = event_base_new();//初始化event_base,一线程一个 struct event msgtimer; struct timeval tv; evtimer_assign(&msgtimer, evbase, &msg_timer, NULL);//初始化事件 evutil_timerclear(&tv); tv.tv_sec = 0; tv.tv_usec = 10;//10毫秒后触发事件 evtimer_add(&msgtimer, &tv);//将事件加入到队列中 event_base_dispatch(evbase);//开始处理队列 event_base_free(evbase);//释放
上述例子简单介绍了如何用libevent设置定时事件。
使用libcurl
curl和wget是做爬虫的常用工具,它们有很多功能。这里,项目中使用libcurl来下载web上的json。
vs2015中编译静态库libcurl
- 下载libcurl源码curl-7.53.1.tar.gz
- 打开vs2015工具命令提示,进入到curlwinbuild目录,执行nmake -F Makefile.vc mode=static VC=14 DEBUG=no MACHINE=x86,这里编译的是32位适用于VS2015的静态库release版本;如果需要调试,令DEBUG=yes
- 编译好的文件在curluildslibcurl-vc14-x86-debug-static-ipv6-sspi-winssl下;我们需要lib下的静态库,以及include中的头文件
libcurl的简单使用
static size_t http_get_process(void *data, size_t size, size_t nmemb, std::string &content) { auto sizes = size * nmemb; content += std::string((char*)data, sizes); return sizes; } curl_global_init(CURL_GLOBAL_ALL); CURL *curl = curl_easy_init();//初始化 std::string text;//保存的内容 curl_easy_setopt(curl, CURLOPT_URL, "http://www.baidu.com");//url curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36"); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L);//超时,单位秒 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);//自动301、302跳转 curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");//留空表示自动解压 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, TRUE); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, TRUE); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &text); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &http_get_process); CURLcode res = curl_easy_perform(curl); if (res == CURLE_OK) { /*text中的内容就是url返回的内容,当然这里面有编码问题,暂且不谈*/ } curl_easy_cleanup(curl); curl_global_cleanup();
那么后面json的下载就要用到curl了。
异步模型
Win32事件驱动模型
经典的win32程序是基于消息的,程序不断处理操作系统给的消息,总体是单线程的。
//消息处理函数 LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) //主循环 while (GetMessage (&msg, NULL, NULL, NULL)) { TranslateMessage (&msg) ; //翻译消息 DispatchMessage (&msg) ; //分派消息 }
大道至简,一个循环解决问题。一般而言,这么写没问题。但是,如果涉及耗时的操作如网络IO……程序就假死了!
比如想做一个下载器,做一个带界面的爬虫,如果只是单线程处理,那么在下载过程中,win32窗口是无响应的,因为它卡在网络IO上了。为了避免这种情况,只能使用多线程。
有了多线程,也就有了竞争与冲突风险,以及各种线程同步问题,解决这些问题的关键是设计一个好用的、简单的模型。最终的思路必然是简单的,否则出了问题谁也找不出。
异步模型的思考
一般的思路:耗时的操作交给工作线程做,主线程处理窗口的消息。这里用libevent解决。
libevent其实也相当于一个死循环,在这个死循环中,它可以:
- 查看当前是否有win32窗口的消息
- 查看定时器事件是否到期了
- 查看网络/文件IO是否完成
注意,它一直在“查看”,也就是说,看看没消息,它就继续干别的事,不会卡死在一个地方。
那么结合win32和libevent,我们有:
- 每隔10毫秒查看win32消息,若有,立马处理一个消息
- 不断监听定时器消息,若有,立马执行
这样保证:win32消息的处理和定时器消息的处理处于同一线程中。这个“处于同一线程中”,好处可大了,因为可以避免线程同步等一系列问题。我们在win32主线程中用lua处理各种消息,而lua可以设置定时器;同样地,我们用lua处理定时器消息。换句话说,自始至终,lua都跑在主线程中,跟其他线程无关。
异步下载网络资源
实现了整个框架最为核心的异步事件模型,那么如何解决网络资源下载问题呢?比如说,我想点击按钮,就下载一个json,通过分析json,下载相应的背景图片,并将这个图片作为程序的背景。
目前,libevent的设置定时器功能是可以跨线程调用的,要注意的是只有这里存在跨线程调用。
那么这一流程如下:
- 按下按钮
- lua处理单击事件,设置定时器timer1并传参request
- timer1中,创建新线程thread2*
- thread2*中,用libcurl下载json文件/图片,若下载成功,设置定时器timer2并传参response
- timer2中,处理response,得到json/图片,用lua更新UI
以上,打*星号的是其他线程,只有curl所在的thread2是其他线程,其他操作都在主线程中。这个模型也是实现题图效果的关键。
其他的问题
不是说实现了模型就能运行程序了,上得了厅堂、下得了厨房,还有些细节需要考虑。
编码问题
默认的std::string是GBK编码的,而一般的json文件是UTF-8,需要转码。
在curl中,我们用
char *content_type;
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
如果content_type中有UTF-8出现,那么文件编码就是UTF8。
第一步:先用curl下载byte[]二进制数据
size_t http_get_process_bin(void *data, size_t size, size_t nmemb, std::vector<byte> &content) { auto sizes = size * nmemb; auto bin = (byte*)data; for (size_t i = 0; i < sizes; ++i) { content.push_back(bin[i]); } return sizes; } auto bindata = new std::vector<byte>(); curl_easy_setopt(curl, CURLOPT_WRITEDATA, bindata); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &http_get_process_bin); /*其他的设置以及curl_easy_perform都省略了*/
将数据存到std::vector<byte>中。
第二步:转码,UTF8 to GBK
CString Utf8ToStringT(LPCSTR str) { _ASSERT(str); USES_CONVERSION; WCHAR *buf; int length = MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0); buf = new WCHAR[length + 1]; ZeroMemory(buf, (length + 1) * sizeof(WCHAR)); MultiByteToWideChar(CP_UTF8, 0, str, -1, buf, length); return (CString(W2T(buf))); } auto gbk = CStringA(content_type);//gbk相当于std::string
其中CString是ATL中的unicode字符串。将CString自行转换至CStringA,而CStringA是ANSI编码的。
如何呈现网上下载的图片
先用libcurl下载二进制图片数据std::vector<byte> data,我们需要用一个byte[]类型去呈现它。
data首先存放在libcurl所在线程中,最终调用者却是渲染图元ImageElement位于主线程中的渲染事件中,两者相距太远,如何联络?
我采取的解决方法是:
- libcurl所在线程thread2*下载完图片数据data,注意,data是new出来的区域,不会自动释放
- 在thread*中设置定时器timer2,带参data,为了方便与lua互动,我将用base64字符串表示二进制数据data,那么存放在lua中的UI对象中的text就是将指针地址进行base64编码后的字符串
- 一段时间过去了……
- 开始渲染事件了
- ImageElementRender中,获取UI对象的text,它是个字符串,用base64解码得到图片数据指针
- 利用指针中的二进制数据,初始化WICBitmap,进而初始化ID2D1Bitmap用于绘制,释放数据data
- 为防止多次渲染闪屏,将WICBitmap保存,仅当图片URL变化时进行重新绘制操作,每次用WICBitmap初始化ID2D1Bitmap