【遊戲框架系列】詩情畫意

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)n{n/*do something...*/n}nnstruct event_base *evbase = event_base_new();//初始化event_base,一線程一個nstruct event msgtimer;nstruct timeval tv;nevtimer_assign(&msgtimer, evbase, &msg_timer, NULL);//初始化事件nevutil_timerclear(&tv);ntv.tv_sec = 0;ntv.tv_usec = 10;//10毫秒後觸發事件nevtimer_add(&msgtimer, &tv);//將事件加入到隊列中nevent_base_dispatch(evbase);//開始處理隊列nevent_base_free(evbase);//釋放n

上述例子簡單介紹了如何用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. 編譯好的文件在curlbuildslibcurl-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)n{n auto sizes = size * nmemb;n content += std::string((char*)data, sizes);n return sizes;n}nncurl_global_init(CURL_GLOBAL_ALL);nCURL *curl = curl_easy_init();//初始化nstd::string text;//保存的內容ncurl_easy_setopt(curl, CURLOPT_URL, "http://www.baidu.com");//urlncurl_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");ncurl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 2L);ncurl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L);//超時,單位秒ncurl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);//自動301、302跳轉ncurl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");//留空表示自動解壓ncurl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, TRUE);ncurl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, TRUE);ncurl_easy_setopt(curl, CURLOPT_WRITEDATA, &text);ncurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &http_get_process);nCURLcode res = curl_easy_perform(curl);nif (res == CURLE_OK)n{n /*text中的內容就是url返回的內容,當然這裡面有編碼問題,暫且不談*/n}ncurl_easy_cleanup(curl);ncurl_global_cleanup();n

那麼後面json的下載就要用到curl了。

非同步模型

Win32事件驅動模型

經典的win32程序是基於消息的,程序不斷處理操作系統給的消息,總體是單線程的。

//消息處理函數nLRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)nn//主循環nwhile (GetMessage (&msg, NULL, NULL, NULL))n{n TranslateMessage (&msg) ; //翻譯消息n DispatchMessage (&msg) ; //分派消息n}n

大道至簡,一個循環解決問題。一般而言,這麼寫沒問題。但是,如果涉及耗時的操作如網路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;ncurl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);n

如果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)n{n auto sizes = size * nmemb;n auto bin = (byte*)data;n for (size_t i = 0; i < sizes; ++i)n {n content.push_back(bin[i]);n }n return sizes;n}nnauto bindata = new std::vector<byte>();ncurl_easy_setopt(curl, CURLOPT_WRITEDATA, bindata);ncurl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &http_get_process_bin);n/*其他的設置以及curl_easy_perform都省略了*/n

將數據存到std::vector<byte>中。

第二步:轉碼,UTF8 to GBK

CString Utf8ToStringT(LPCSTR str)n{n _ASSERT(str);n USES_CONVERSION;n WCHAR *buf;n int length = MultiByteToWideChar(CP_UTF8, 0, str, -1, nullptr, 0);n buf = new WCHAR[length + 1];n ZeroMemory(buf, (length + 1) * sizeof(WCHAR));n MultiByteToWideChar(CP_UTF8, 0, str, -1, buf, length);n return (CString(W2T(buf)));n}nnauto gbk = CStringA(content_type);//gbk相當於std::stringn

其中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

階段性小結

花了兩天時間實現上網爬json下圖片設置背景與文字,讓程序具有「詩情畫意」。同樣地,完成這個功能後,感到的喜悅與滿足是遠大於先前編程中遇到的困難的,不僅僅是編程,任何一件事,鍥而不捨去做,總有質變的那天。就像好聲音中的,一開始人被音樂玩,後來才能玩音樂。什麼時候能夠玩編程,不是被編程玩呢?繩鋸木斷,水滴石穿,不停做下去,不斷走出舒適區,不斷迎接新的突破。當知識在腦海中漸漸匯成整體性的知識框架,就是量變到質變的體現,而在這個突破之前,需要的是漫長的積累時間。然而,不能因為積累太長太累而放棄,人總歸要向前看。

下面的任務是做一個計算器,做一個A*尋路可視化界面,總體並不難。

推薦閱讀:

【遊戲安全】看我如何通過hook攻擊LuaJIT
Unity3D熱更新LuaFramework入門實戰(5)——UI
HammerSpoon - 不止是窗口管理
用好Lua+Unity,讓性能飛起來—LuaJIT性能坑詳解
Lua程序逆向之Luac文件格式分析

TAG:Lua | CC | 编程学习 |