libuv經過Node.js的實踐和應用,已經證明非常之成熟,本來之前項目用的是這個:clsocket https://github.com/DFHack/clsocket 當初選它的主要原因是它支持Windows、Linux、Mac OSX(我猜測的),但致命的缺點就是僅支持阻塞的TCP,這樣就會導致一個問題,在連接遊戲服務器、聊天服務器的時候遊戲主界面會直接被卡死,等連接成功後才能恢復正常。而LuaSocket之前遊戲也替換過,發現的問題主要是依賴lua的循環檢測是否有新的數據(定時器),從而導致明顯的界面延時。Cocos2d-x 3.x版本因爲性能大幅提升,似乎此問題感受並不明顯,而我們因爲項目歷史明顯,lua 與 C++結合的很死,本身跑起來就一卡一卡的。
當然還有很多優秀的C++ TCP網絡庫,不過大部分似乎寫的時候就只准備支持Linux/Unix,壓根就沒想支持Windows。而我們開發人員首先肯定是先在Windows下進行開發,神馬?用Mac,公司連給iMac換個512G的SSD審批就很難,就別做夢了。自己出錢或者自帶mac筆記本行麼?不行!不允許使用外置USB,想用嗎?走個OA單申請一下,一般開發人員的申請是直接被拒絕的,主程除外(好吧,我算比較幸運的 - “主程”)
吐槽歸吐槽,活還是要幹活的滴。libuv在實際使用中我發現的幾個問題,如果連接socket時後臺主動斷開連接,那麼後臺最後發送出來的消息有可能會接收不到(概率性的,解決方法就是讓後臺發送消息完之後延時幾秒再關閉socket連接)。iOS設備在關閉電源後,socket立馬就斷掉了,遊戲從後臺切換到前臺時需要能自動重連一次。而libuv因爲本身是用純C實現的,它的回調方法基本上都是static函數,用C++封裝的話有點小麻煩,網上也有人用C++11封裝的比較好,可惜我使用的NDK版本比較低,支持不了C++11的特性只好放棄,但人家的思路還是可以借鑑的,覺得很贊。
以客戶端爲例,先了解下libuv的基本使用,示例來自gist (所有的libuv函數都以uv_開頭)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <uv.h> #define log(x) printf("%s\n", x); uv_loop_t *loop; void on_connect(uv_connect_t *req, int status); void on_write_end(uv_write_t *req, int status); uv_buf_t alloc_buffer(uv_handle_t *handle, size_t suggested_size); void echo_read(uv_stream_t *server, ssize_t nread, uv_buf_t buf); // サーバからのレスポンスを表示 void echo_read(uv_stream_t *server, ssize_t nread, uv_buf_t buf) { if (nread == -1) { fprintf(stderr, "error echo_read"); return; } // 結果を buf から取得して表示 printf("result: %s\n", buf.base); } // suggeseted_size で渡された領域を確保 uv_buf_t alloc_buffer(uv_handle_t *handle, size_t suggested_size) { // 読み込みのためのバッファを、サジェストされたサイズで確保 return uv_buf_init((char*) malloc(suggested_size), suggested_size); } // サーバへデータ送信後, サーバからのレスポンスを読み込む void on_write_end(uv_write_t *req, int status) { if (status == -1) { fprintf(stderr, "error on_write_end"); return; } // 書き込みが終わったら、すぐに読み込みを開始 uv_read_start(req->handle, alloc_buffer, echo_read); } // サーバとの接続を確立後, サーバに文字列を送信 void on_connect(uv_connect_t *req, int status) { if (status == -1) { fprintf(stderr, "error on_write_end"); return; } // 送信メッセージを登録 char *message = "hello.txt"; int len = strlen(message); /** これだとセグフォ * uv_buf_t buf[1]; * buf[0].len = len; * buf[0].base = message; */ // 送信データ用のバッファ char buffer[100]; uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer)); // 送信データを載せる buf.len = len; buf.base = message; // ハンドルを取得 uv_stream_t* tcp = req->handle; // 書き込み用構造體 uv_write_t write_req; int buf_count = 1; // 書き込み uv_write(&write_req, tcp, &buf, buf_count, on_write_end); } int main(void) { // loop 生成 loop = uv_default_loop(); // Network I/O の構造體 uv_tcp_t client; // loop への登録 uv_tcp_init(loop, &client); // アドレスの取得 struct sockaddr_in req_addr = uv_ip4_addr("127.0.0.1", 7000); // TCP コネクション用の構造體 uv_connect_t connect_req; // 接続 uv_tcp_connect(&connect_req, &client, req_addr, on_connect); // ループを開始 return uv_run(loop); }
libuv使用的基本步驟:
1、生成一個loop (uv_default_loop() 或者 uv_loop_t _loop)
2、初始化一個client,uv_tcp_init
3、連接指定的服務器,uv_tcp_connect
4、開啓消息循環,uv_run
通常使用時,我們都需要新啓動一個線程,在該線程中來執行uv_run來保證不阻塞當前調用的線程(uv_run是阻塞的,不會立即返回)。
使用線程的關鍵函數:uv_thread_create(創建線程)、uv_async_init、uv_async_send(線程通信),消息的發送是異步的,在另外一個線程中多次(二次或更多)調用了uv_async_send函數後它只會保證uv_async_init回調函數至少被調用一次
uv_async_send是非阻塞的,同樣也不是線程安全的,在變量訪問時應該儘量和互斥量或讀寫鎖來保證訪問順序。
我們遊戲服務器是雙線的,所以返回給客戶端的數據是域名 + 端口,這裏需要先將域名轉爲ip然後進行uv_tcp_connect連接。
示例代碼:
uv_getaddrinfo_t* getaddrinfo_handle = (uv_getaddrinfo_t*)malloc(sizeof(uv_getaddrinfo_t)); getaddrinfo_handle->data = this; int r = uv_getaddrinfo(&loop_, getaddrinfo_handle, &AfterDNSResolved, m_strDomain.c_str(), NULL, NULL); //r 返回0時表示正常,非0則說明出錯了可通過 uv_err_name(r)、uv_strerror(r)獲得出錯信息
uvbook的QueryDNS示例:
int main() { loop = uv_default_loop(); struct addrinfo hints; hints.ai_family = PF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = 0; uv_getaddrinfo_t resolver; fprintf(stderr, "irc.freenode.net is... "); int r = uv_getaddrinfo(loop, &resolver, on_resolved, "irc.freenode.net", "6667", &hints); if (r) { fprintf(stderr, "getaddrinfo call error %s\n", uv_err_name(r)); return 1; } return uv_run(loop, UV_RUN_DEFAULT); } void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) { if (status < 0) { fprintf(stderr, "getaddrinfo callback error %s\n", uv_err_name(status)); return; } char addr[17] = {'\0'}; uv_ip4_name((struct sockaddr_in*) res->ai_addr, addr, 16); fprintf(stderr, "%s\n", addr); uv_connect_t *connect_req = (uv_connect_t*) malloc(sizeof(uv_connect_t)); uv_tcp_t *socket = (uv_tcp_t*) malloc(sizeof(uv_tcp_t)); uv_tcp_init(loop, socket); uv_tcp_connect(connect_req, socket, (const struct sockaddr*) res->ai_addr, on_connect); uv_freeaddrinfo(res); }
Windows下的libuv工程構建,使用官方推薦的gyp生成vs的解決方案即可
1、安裝並設置python(2.6或2.7版本)
2、源碼目錄下新建build目錄,然後將gyp下載至該目錄
3、雙擊執行vcbuild.bat即可
在其它項目中引用libuv.lib時,需要在VS附加依賴項中添加幾個系統的lib,不然會報錯
libuv.lib advapi32.lib iphlpapi.lib psapi.lib shell32.lib userenv.lib ws2_32.lib
Android下編譯libuv.a,我安裝了虛擬機然後折騰了好一會,最後放棄了,參考Linux生成的mk自己整了一個完整的mk文件
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := uv_static LOCAL_MODULE_FILENAME := libuv #LOCAL_SRC_FILES := proj.android/libuv.a #LOCAL_EXPORT_CFLAGS := -I$(LOCAL_PATH)/include LOCAL_SRC_FILES := \ src/fs-poll.c \ src/inet.c \ src/threadpool.c \ src/uv-common.c \ src/version.c \ src/unix/async.c \ src/unix/core.c \ src/unix/dl.c \ src/unix/fs.c \ src/unix/getaddrinfo.c \ src/unix/getnameinfo.c \ src/unix/loop.c \ src/unix/loop-watcher.c \ src/unix/pipe.c \ src/unix/poll.c \ src/unix/process.c \ src/unix/signal.c \ src/unix/stream.c \ src/unix/tcp.c \ src/unix/thread.c \ src/unix/timer.c \ src/unix/tty.c \ src/unix/udp.c \ src/unix/proctitle.c \ src/unix/linux-core.c \ src/unix/linux-inotify.c \ src/unix/linux-syscalls.c \ src/unix/pthread-fixes.c \ src/unix/android-ifaddrs.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/include \ $(LOCAL_PATH)/src \ $(LOCAL_PATH)/src/unix LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include \ $(LOCAL_PATH)/src \ $(LOCAL_PATH)/src/unix LOCAL_CFLAGS := \ -Wall \ -fvisibility=hidden \ -g \ --std=gnu89 \ -pedantic \ -Wall \ -Wextra \ -Wno-unused-parameter \ -Wstrict-aliasing \ -O3 \ -fstrict-aliasing \ -fomit-frame-pointer \ -fdata-sections \ -ffunction-sections include $(BUILD_STATIC_LIBRARY)
在項目的jni/Android.mk中只需要添加
LOCAL_WHOLE_STATIC_LIBRARIES += uv_static
$(call import-module,libuv)
//注意添加NDK的搜索路徑
而自己項目中需要用到uv.h時,修改相應的Android.mk文件,在
LOCAL_C_INCLUDES += $(LOCAL_PATH)/../../libuv/include \
//目錄自己依照自己的環境進行修改
一切正常的話,在eclipse項目的obj目錄下會看到編譯成功後的文件
iOS下的編譯,默認libuv只提供了Mac下的編譯,修改一下就可以讓它支持iOS了
1、下載libuv https://github.com/joyent/libuv/releases 2、shell進入libuv $mkdir -p build $git clone https://git.chromium.org/external/gyp.git build/gyp 3、$./gyp_uv.py -f xcode 生成xcode項目文件uv.xcodeproj 4、打開xcode,修改architectures
XCode7的話,直接先最新的SDK版本,上面的用的比較舊。引用項目的話,將libuv.a加到項目中,然後Framework Search Path以及Header Search Path添加路徑即可。
到這裏,三個平臺全部都搞定了。我引入到項目中,主要是參考了
libuv_tcp https://github.com/wqvbjhc/libuv_tcp
也借鑑了
libsourcey https://github.com/sourcey/libsourcey
uvpp https://github.com/larroy/uvpp
我自己收藏的一個libuv for lua的項目,覺得也挺不錯的。luv https://github.com/luvit/luv 有興趣可以去搗鼓一下,跟Node.js差不多
附帶貼一下uvbook的二個鏈接:
英文版(最新V1.3.0) https://nikhilm.github.io/uvbook/introduction.html
中文版(V0.9.8) http://www.nowx.org/uvbook/index.html