cocos2d-x 3.1中luasocket试用



花了两三天时间来学习luasocket,调试通过,运行正常,于是发文分享。

说明:文中的代码其实都是伪代码,只表明了思路,我并没有把调试通过的代码直接放上来。


准备工作

如何加载

luasocket官方文档http://w3.impa.br/~diego/software/luasocket/introduction.html,说的是使用require('socket');

不过经过测试,其实在cocos2d-x中,应该使用require('socket.core');才可以。

实际使用时我用的是local socket = require('socket.core');


版本

socket._VERSION取得的值是“LuaSocket 3.0-rc1”,这比luasocket官方网站的版本(2.0.2)还要新。不过一圈实验下来,官方文档所描述的接口也都可用,没有体会到哪里有变化。


正题

1. 首先是创建socket,这使用socket.tcp()即可。

local clientSocket = socket.tcp();
clientSocket:settimeout(0);

注意我刚创建了socket就立刻设置timeout为0。这样一来,当我调用connect连接服务器的时候就不会导致整个游戏卡住了。

由于手机是作为游戏客户端,所以socket主要就是connect、send、receive这三个函数,外加select。至于bind、accept,这些是用在服务端的,我并没有花心思去体会。


2. 连接服务器。

if clientSockend:connect(address, port) == 1 then
    cclog('socket connected');
else
    isConnecting = true;
end

由于前面调用了settimeout(0),因此其实几乎不可能直接连接成功,这个connect函数就返回了。此时用一个变量isConnecting记录状态。
根据文档,可以用select函数查看,再确定是否真的连接成功。

3. select。
select函数会频繁调用,但我觉得我们的游戏对实时性要求不高,不必每帧都调用(流畅情况下每秒60次)。目前我每秒调用大约10次select函数。
根据select的结果,会处理发送数据、接收数据、以及关闭连接。

if isConnecting and socket.gettime() - connectTime > 10 then
    -- 连接超时,关闭socket并清理所有变量。
    -- 我还没有发现如何通过luasocket的接口来判断连接超时,所以只好用了这个笨办法
    return;
end

local arr = {clientSocket};
local r, s, e = socket.select(arr, isConnecting and arr or nil, 0);

if r and #r >= 1 and r[1] == clientSocket then
    local recvData, recvError, recvParticialData = clientSocket:receive(99999999);

    if recvError == 'closed' then
        -- socket已经断开
        return;
    end

    -- 如果是大块数据,可能被拆分成多段,需要多次调用clientSocket:receive才能完成接收。
    -- 因此可以把接收到数据先放到recvBuffer缓存起来。
    if recvData then
        recvBuffer = recvBuffer .. recvData;
    elseif recvParticialData then
        recvBuffer = recvBuffer .. recvParticialData;
    end

    -- 处理缓存起来的数据。
    processRecvBuffer();
end

if s and #s >= 1 and s[1] == clientSocket then
    if isConnecting then
        -- 执行到此,说明连接已经成功了
        isConnecting = false;
        isConnected = true;
    end

    -- 可以调用send
    processSendBuffer();
end

if isConnected then
    procsesSendBuffer();
end

由于socket本身的特点,
a. 本端调用一次send,对端可能需要多次receive才能完整接收。
b. 如果对一个很大的数据块进行send,可能无法一次性发送全部,而是只发送部分字节。剩余字节需要再次调用send才可发送。

对于a,
我把所有接收到的数据都储存到一个叫做recvBuffer的变量中,然后设置一个recvIndex作为索引。
recvIndex初始化为1,如果处理掉x个字节,那么就让recvIndex增加x。
while true do
    -- 看消息头是否完整。目前我设计消息头有四个字段msgSize, msgType, msgSerial, msgCheck,都是USHORT类型。
    -- 这里string.unpack其实是另一个库lpack提供的。cocos2d-x没有自带这个lpack库,不过它只有一个.c文件,很容易就加入到工程中。
    local nextReceiveIndex, msgSize, msgType, msgSerial, msgCheck = string.unpack(receiveBuffer, '=HHHH', receiveIndex);
    if nextReceiveIndex - receiveIndex < 8 then
        break;
    end

    -- 根据消息头,看消息体是否完整
    if msgSize + receiveIndex > #receiveBuffer + 1 then
        break;
    end

    -- 消息体完整。处理这个消息。
    local reader = netmsgReadersMap[msgType];
    local handler = netmsgHandlersMap[msgType];
    if reader and handler then
        local netmsg = reader(receiveBuffer, receiveIndex);
        if netmsg then
            handler(netmsg);
        end
    end

    -- receiveIndex向后移动msgSize个字节。
    -- 如果已经处理完所有消息,则清空整个缓冲。
    receiveIndex = receiveIndex + msgSize;
    if receiveIndex > #receiveBuffer then
        receiveBuffer = '';
        receiveIndex = 1;
        break;
    end
end

对于b,
跟接收类似,receiveBuffer, receiveIndex对应有sendBuffer, sendIndex
local num, err, num2 = clientSocket:send(sendBuffer, sendIndex);

if num then
    -- 所有数据发送完毕
    sendBuffer = '';
    sendIndex = 1;
else
    -- 只有部分数据发送完毕
    sendIndex = num2 + 1;
end

这样就基本完工了。

luasocket用的是string作为发送和接收的数据类型,由于lua的string其实可以保存任意数据(包括'\0'),所以这也不算什么问题。关键是在lua中要怎么把各种数值转化为二进制的string。目前我用了lpack这个库,比价简单,只有一个.c文件,加入到工程里面编译就行。在applicationDidFinishLaunching时,调用luaopen_pack就行了。

服务器我用的是C++,boost.asio和boost.circular_buffer搭配,还算简单。实际测试发送了60k字节的数据,运行正常。由于我的msgSize是一个USHORT,不支持大于64k字节,也就没有测试更大的数据包了。
另外编写了一些python脚本,用于自动生成代码,便于客户端和服务器解析网络收发的数据。这样整个网络模块就搭建完毕。



对于手机游戏来说,网络断开是常有的事情。对于网络时而断开,时而正常,目前我还没有在luasocket上做这方面的测试。

早先我想在TCP之上再封装一层可靠的协议,当网络断开时,把所有需要发送的数据全部缓冲起来,等待重新连接成功时,再把这些数据发送过去。甚至类似TCP那样,发送的每个数据包,都要求对端确认。如果对端不确认,本端就认为超时,并且尝试重新发送。
但后来感觉,这样做并不实际。
例如,商店买东西,如果网络条件差,购买未能成功,此时玩家可能反复点击购买按钮。不过此时的点击就不能再视作购买,即使之后网络重新连接成功,也不应该向服务器发送大量的购买请求。否则玩家可能原本只想买一份物品,结果由于网络延迟导致一口气买了一大堆。这不合适。
比较合适的做法是,如果点击购买之后,长时间得不到服务器响应,那么就提示玩家,网络出现问题。待重新连接成功后,再向服务器查询,看金钱是否有减少,商品是否有购买到。
因此目前的策略是,网络重新连接成功时,向服务器请求各种数据,包括人物的基本数据(金钱、经验、背包物品之类),以及打开当前界面(工会、随机商人之类)所需的数据。某些界面(例如,回合制的战斗界面)因为数据复杂,可能不太好做,但目前也没有更好的办法了。

你可能感兴趣的:(cocos2d-x)