诗情画意

v2-a3575227ada68693bc7eef27cedccb97_1200x500

Release 实现异步更新网络图片 · bajdcc/GameFramework · GitHub

写在前面

计划着将一些好用的东西整合进框架中,目前用了libevent和libcurl,仅当尝鲜。话说libcurl的使用其实很简单,跟php的curl扩展差不多。libevent是初次使用,很多坑尚未发现。

简单介绍下封面界面的构成:必应背景、一言API、文字、二维码。其中新增的是前二个:必应背景和一言文字。

下面是主要内容:

  1. libevent和libcurl的编译与使用
  2. 如何实现异步刷新,且涉及网络请求

使用libevent

项目中需要用到libevent,实现异步通知功能。libevent支持网络/文件IO、定时器、信号。在这里,我们只需要用到其中的定时器功能。

vs2015中编译静态库libevent

  1. 下载libevent源码libevent-2.0.22-stable.tar.gz
  2. 下载Makefile.nmake到libevent目录,默认是release版本;若需要编译debug版本,需要修改其中的第26行`CFLAGS=$(CFLAGS) /Ox /W3 /wd4996 /nologo`为`CFLAGS=$(CFLAGS) /Od /Zi /W3 /wd4996 /nologo`
  3. 开始菜单=>vs2015开发人员命令提示,cd切换到libevent目录,执行命令nmake /F makefile.nmake;如果要清空上一次编译的结果,执行nmake /F makefile.nmake clean
  4. 复制目录下的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

  1. 下载libcurl源码curl-7.53.1.tar.gz
  2. 打开vs2015工具命令提示,进入到curlwinbuild目录,执行nmake -F Makefile.vc mode=static VC=14 DEBUG=no MACHINE=x86,这里编译的是32位适用于VS2015的静态库release版本;如果需要调试,令DEBUG=yes
  3. 编译好的文件在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其实也相当于一个死循环,在这个死循环中,它可以:

  1. 查看当前是否有win32窗口的消息
  2. 查看定时器事件是否到期了
  3. 查看网络/文件IO是否完成

注意,它一直在“查看”,也就是说,看看没消息,它就继续干别的事,不会卡死在一个地方。

那么结合win32和libevent,我们有:

  1. 每隔10毫秒查看win32消息,若有,立马处理一个消息
  2. 不断监听定时器消息,若有,立马执行

这样保证:win32消息的处理和定时器消息的处理处于同一线程中。这个“处于同一线程中”,好处可大了,因为可以避免线程同步等一系列问题。我们在win32主线程中用lua处理各种消息,而lua可以设置定时器;同样地,我们用lua处理定时器消息。换句话说,自始至终,lua都跑在主线程中,跟其他线程无关。

异步下载网络资源

实现了整个框架最为核心的异步事件模型,那么如何解决网络资源下载问题呢?比如说,我想点击按钮,就下载一个json,通过分析json,下载相应的背景图片,并将这个图片作为程序的背景。

目前,libevent的设置定时器功能是可以跨线程调用的,要注意的是只有这里存在跨线程调用。

那么这一流程如下:

  1. 按下按钮
  2. lua处理单击事件,设置定时器timer1并传参request
  3. timer1中,创建新线程thread2*
  4. thread2*中,用libcurl下载json文件/图片,若下载成功,设置定时器timer2并传参response
  5. 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位于主线程中的渲染事件中,两者相距太远,如何联络?

我采取的解决方法是:

  1. libcurl所在线程thread2*下载完图片数据data,注意,data是new出来的区域,不会自动释放
  2. 在thread*中设置定时器timer2,带参data,为了方便与lua互动,我将用base64字符串表示二进制数据data,那么存放在lua中的UI对象中的text就是将指针地址进行base64编码后的字符串
  3. 一段时间过去了……
  4. 开始渲染事件了
  5. ImageElementRender中,获取UI对象的text,它是个字符串,用base64解码得到图片数据指针
  6. 利用指针中的二进制数据,初始化WICBitmap,进而初始化ID2D1Bitmap用于绘制,释放数据data
  7. 为防止多次渲染闪屏,将WICBitmap保存,仅当图片URL变化时进行重新绘制操作,每次用WICBitmap初始化ID2D1Bitmap

https://zhuanlan.zhihu.com/p/25476629备份。

原文地址:https://www.cnblogs.com/bajdcc/p/8972932.html