Cocos2dx-lua中使用LuaSocket
項目背景
客戶端:C++和lua混合,cocos2dx 3.10版本;服務端:C++,某狐公司的棋牌服務端。
需求
手機客戶端使用socket與服務端通信,需要處理數據粘包半包、字符串編碼轉換、心跳機制、接收超時這幾個主要的問題,另外使用luasocket需要考慮數據傳輸格式的問題。檢索網上的資料,基於LuaSocket針對項目需求做了一定的調整,使用了該文中提到的ByteArray和lpack庫實現了lua使用二進制數據和服務器通信,並在C++端利用iconv庫實現了字符串編碼格式的轉換,達到了項目的需求。下面具體談談如何解決上述提到的幾個問題。
1、lua中發送與接收二進制數據
這裏直接使用了ByteArray,不過後來又提到該實現中與Long相關的實現存在並非bug的問題,而是由於跨平臺導致的處理不一致而導致的問題,但是在我們的項目中直接使用且在pc模擬器,多品牌、多處理器平臺、多安卓版本的安卓機型上,多ios版本、5s、6、7、x的蘋果機型上,並沒有出現問題,所以我還是繼續使用了這個庫來進行長整型數據的讀取。在這個庫的基礎上,我額外加上了字符串的讀寫轉換,這個是在C++端利用iconv庫實現的, 之前有試過lua版本的iconv庫,可能是使用方式不對,達不到需求。
轉換成寬字符的部分代碼:
6 iconv_t cd = iconv_open("UTF-16LE", "UTF-8"); 7 if (0 != cd) 8 { 9 char *tmp = (char*)szTmp; 10 #ifdef WIN32 11 if (iconv(cd, &szData, &inlen, &tmp, &outlen) != (size_t) -1 ) 12 #else13 char *szTempData = (char*)szData; 14 if (iconv(cd, &szTempData, &inlen, &tmp, &outlen) != (size_t) -1 ) 15 #endif 16 { 17 iconv_close(cd); 18 lua_pushlstring(tolua_S, (char*)szTmp, returnlen); 19 free(szTmp); 20 return 1; 21 } 22 iconv_close(cd); 23 }
寬字符轉換回的部分代碼:
iconv_t cd = iconv_open("UTF-16LE", "UTF-8"); if (0 != cd) { char *tmp = (char*)szTmp; #ifdef WIN32 iconv(cd, &szData, &inlen, &tmp, &outlen); #else char *szTempData = (char*)szData; iconv(cd, &szTempData, &inlen, &tmp, &outlen); #endif iconv_close(cd); lua_pushlstring(tolua_S, (char*)szTmp, returnlen); free(szTmp); return 1; }
這裏需要註意的是,調用 iconv()進行轉換的時候輸入、輸出的長度一定要計算好,否則會導致內存讀取異常,導致閃退!處理好了與服務端通信數據格式的問題,之後就是在lua中實現socket與服務端通信。
2、接收超時
這裏提到的接收超時是這樣的:socket處於連接狀態,但是長時間無法讀取到數據。前面提到的SocketTCP封裝利用引擎提供的schedule和quick提供的事件框架實現了各種狀態的輪詢如連接超時的檢測、數據接收處理,我主要的調整是根據select函數返回的結果,處理接收超時的狀態
1 local __tick = function()
2 while true do
3 local recvt = socket.select({self.tcp}, nil, 0)
4 -- print("recvt ", #recvt)
5 if #recvt > 0 then
6 -- if use "*l" pattern, some buffer will be discarded, why?
7 local __body, __status, __partial = self.tcp:receive("*a") -- read the package body
8 --print("body:", __body, "__status:", __status, "__partial:", __partial)
9 if __status == STATUS_CLOSED or __status == STATUS_NOT_CONNECTED then
10 self:close()
11 if self.isConnected then
12 self:_onDisconnect()
13 else
14 self:_connectFailure()
15 end
16 -- 跳出循環
17 return
18 end
19
20 -- 數據狀態
21 if (__body and string.len(__body) == 0)
22 or (__partial and string.len(__partial) == 0) then
23 -- 這裏處理接收失敗,如服務器踢
24 -- 跳出循環
25 return
26 end
27 if __body and __partial then
28 __body = __body .. __partial
29 end
30 -- 這裏接收到數據包
31 else
32 -- 這裏拋出超時狀態
33 -- 跳出循環
34 return
35 end
36 end
37 end
38 -- start to read TCP data
39 self.tickScheduler = scheduler.scheduleGlobal(__tick, SOCKET_TICK_TIME)
3、數據粘包
前面有提到使用ByteArray實現與服務端進行二進制數據通信, 在這裏繼續使用ByteArray解決半包和粘包的問題。解決數據粘包半包的問題,首先是跟服務端約定好消息協議:數據包包頭裏面包含當前數據包長度;其次是將每次接收到的數據流填充到一個bytearray對象中,對比接收到的數據長度和數據包實際長度,從填充的bytearray中提取指定長度的數據。
前面也提到,封裝好的SocketTCP利用了schedule和quick事件組件實現了事件輪詢。每次接收到數據包狀態,將數據包填充到bytearray對象,再判斷是否獲取到一個完整的數據包:
1 stream:addData(msg) 2 while self.status ~= STATUS_SOCKET_CLOSED do 3 local msgPack, bHeatResponse, bHandleEnd = stream:getMsg() 4 if bHeatResponse then 5 -- 記錄時間 6 self.lastTime = os.time() 7 -- 心跳回復 8 -- ... 9 break 10 else 11 if msgPack == nil then 12 break 13 end 14 -- 記錄時間 15 self.lastTime = os.time() 17 -- 分發數據 19 -- 是否處理完數據包 20 if bHandleEnd then 21 break 22 end 23 end 24 end
addData是將數據包填充至ByteArray對象,getOneMsg是獲取一個完整的數據包。這裏使用了一個while循環,用於提取所有的數據包。
getMsg方法裏面的實現主要是讀取ByteArray數據,對比包長度,處理消息協議,解包數據。半包和粘包的問題,重點是要控制好ByteArray對象的數據位,半包的時候要將數據位置為末尾位置,以便下一個數據包填充至正確的問題,粘包的話控制好當前包的讀取長度。半包和粘包處理好之後,清空ByteArray對象的緩存,再重置該對象的數據位,等待重新讀取數據。
4、心跳機制
心跳機制結合前面提到的接收超時檢測,每一次接收到心跳包、數據包的時候,記錄一下接收時間,然後再在SocketTCP拋出的超時狀態中進行超時時長檢測,根據接收時長的間隔來判斷客戶端當前是否是接收超時,再做後續的邏輯處理。
總結
大概花了一周的時間在項目中實現luasocket與服務端的通信,難點在於如何實現二進制流通信、半包粘包的問題、接收狀態的超時處理。
Cocos2dx-lua中使用LuaSocket