1. 程式人生 > >Cocos2dx-lua中使用LuaSocket

Cocos2dx-lua中使用LuaSocket

pri 客戶端使用 receive 一定的 根據 約定 組件 重點 cvt

項目背景

客戶端: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             #else
13 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