【遊戲框架系列】詩情畫意
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)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
- 下載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
- 編譯好的文件在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其實也相當於一個死循環,在這個死循環中,它可以:
- 查看當前是否有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;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位於主線程中的渲染事件中,兩者相距太遠,如何聯絡?
我採取的解決方法是:
- 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
階段性小結
花了兩天時間實現上網爬json下圖片設置背景與文字,讓程序具有「詩情畫意」。同樣地,完成這個功能後,感到的喜悅與滿足是遠大於先前編程中遇到的困難的,不僅僅是編程,任何一件事,鍥而不捨去做,總有質變的那天。就像好聲音中的,一開始人被音樂玩,後來才能玩音樂。什麼時候能夠玩編程,不是被編程玩呢?繩鋸木斷,水滴石穿,不停做下去,不斷走出舒適區,不斷迎接新的突破。當知識在腦海中漸漸匯成整體性的知識框架,就是量變到質變的體現,而在這個突破之前,需要的是漫長的積累時間。然而,不能因為積累太長太累而放棄,人總歸要向前看。
下面的任務是做一個計算器,做一個A*尋路可視化界面,總體並不難。
推薦閱讀:
※【遊戲安全】看我如何通過hook攻擊LuaJIT
※Unity3D熱更新LuaFramework入門實戰(5)——UI
※HammerSpoon - 不止是窗口管理
※用好Lua+Unity,讓性能飛起來—LuaJIT性能坑詳解
※Lua程序逆向之Luac文件格式分析