客户端与后端交互

很高兴来到这一步,准备了那么久,我们迎来了正式后端编码阶段,对于做前端的同学这是一个相当煎熬的过程,废话少说,赶紧开始。

回到上篇文章,我说过我们的编写的服务端代码都放在game/service下面,逻辑代码放在script下面。
如果你继续关注本专题,那你就要记住这个约定喔。

在service目录建立agent.lua文件,我们将会在这个agent文件中编写收发包,分发消息的功能。

每次有客户端连接服务器,服务端都会建立一个agent(一个lua虚拟机),然后这个虚拟机会接受客户端(其他服务向这个虚拟机发送消息也算这个agent的客户端)发送的消息,进行处理分发。
(这里大部分参考了examples/agent.lua,但是我们使用pbc,会有所区别)
在编写agent之前,先准备一个工具类uitls.lua以及协议。
我这种协议定义,其实蛮简单,也好扩展,毕竟我不是专业的后台开发,所以我个人采用了自认为比较好用的方式,有什么不妥还请各位多多指教。

message Packet
{
  required uint32 cmd = 1;
  required uint32 session = 2;
  required bytes body = 3;
}

message Head
{
    optional int32 ecode = 1;
}

message RequestLogin
{
    optional Head head = 1;
    optional uint32 type = 2; // 1 为登陆 2 为注册
    optional string name = 3;
    optional string password = 4;
}

message RespondLogin
{
    optional Head head = 1;
}

cmd.lua(放在script下面):

CMD_REQUEST_LOGIN = 10000
CMD_RESPOND_LOGIN = 10001

CMD_MAP = {}
CMD_MAP[CMD_REQUEST_LOGIN] = "login_handler"

uitls.lua(放在script下面):

local protobuf = require "protobuf"

-- 跟客户端约定好的包结构,前两个字节表示长度
function packPacket(cmd, name, data, session)
    local body  = protobuf.encode(name, data or {})
    local packet = protobuf.encode("Packet", { cmd = cmd, body = body, session = session or 1 })
    -- 转为Skynet所需要的网络字节序,两个字节的长度+数据包
    local size = #packet
    assert(size < 65536) -- 2 byte limit
    local a = math.floor(size/256)
    local b = math.floor(size%256)
    packet = string.char(a) .. string.char(b) .. packet
    return packet
end

function unpackPacket(packet)
    local packet = protobuf.decode("Packet", packet)
    local cmd = packet.cmd
    local session = packet.session
    local body = packet.body
    return cmd, session, body
end

function unpackBody(name, body)
    local data = protobuf.decode(name, body)
    return data
end

修改下配置config/game.cfg:

preload = root.."script/global.lua" -- run preload.lua before every lua service run

预加载一个全局global.lua,把utils.lua给引入进来。
global.lua:

require "utils"
require "cmd"

这些都准备好了,我们开始写agent。
agent.lua:

local skynet = require "skynet"
local netpack = require "netpack"
local socket = require "socket"
local protobuf = require "protobuf"

local WATCHDOG

local CMD = {}
local REQUEST = {}
local client_fd

local function send_package(package)
    socket.write(client_fd, package)
end

local function request(cmd, session, body)
    local f = assert(REQUEST[CMD_MAP[cmd]])
    local r = f(cmd, session, body)
    return r
end

function REQUEST.login_handler(cmd, session, body)
    -- skynet.error() 是skynet的log函数,使用print也可以,不过没法打印出当前地址
    skynet.error("登陆处理~,回包给客户端~")
    local data = {
        head = {ecode = 1}
    }
    local packet = packPacket(CMD_RESPOND_LOGIN, "RespondLogin", data, session)
    return packet
end

skynet.register_protocol {
    name = "client",
    id = skynet.PTYPE_CLIENT,
    unpack = function (msg, sz)
        -- 收到消息和长度,tostring一下,转化为buffer
        return skynet.tostring(msg, sz)
    end,
    dispatch = function (_, _, buffer, ...)
        -- 解包
        local cmd, session, body = unpackPacket(buffer)
        -- 根据命令字调用相应的函数
        local ok, result  = pcall(request, cmd, session, body)
        -- 把返回包发送给客户端
        if ok then
            if result then
                send_package(result)
            end
        else
            skynet.error(result)
        end
    end
}

-- 从watchdog可以知道当socket打开便会调用,并且调用先相关的网关服务,然后就可以进行消息的收发
function CMD.start(conf)
    local fd = conf.client
    local gate = conf.gate
    WATCHDOG = conf.watchdog
    client_fd = fd
    skynet.call(gate, "lua", "forward", fd)
end

-- 同看watchdog
function CMD.disconnect()
    -- todo: do something before exit
    skynet.exit()
end

-- 启动一个skynet服务(其实就是启动一个lua虚拟机),接受分发收到的数据
skynet.start(function()
    -- 注册协议
    protobuf.register_file "../game/proto/game.pb"

    skynet.dispatch("lua", function(_,_, command, ...)
        local f = CMD[command]
        skynet.ret(skynet.pack(f(...)))
    end)
end)

估计第一次上手的同学会花很大力气去弄,至此服务端搞好,客户端就简单写个按钮文本和网络管理类,然后发包联调。
客户端:
共用命令字但不需要mapping函数鸟,拷贝到src/app/net下面。
cmd.lua:

CMD_REQUEST_LOGIN = 10000
CMD_RESPOND_LOGIN = 10001

共用解压包函数:
直接把utils.lua拷贝到我们的src目录并且require一下。
公用.pb文件,把proto/game.pb拷贝到res目录。
编写一个网络管理类,使用quick自带的SockeTCP:NetworkManager

cc.utils = require("framework.cc.utils.init")
cc.net = require("framework.cc.net.init")

local protobuf = require("protobuf")

NetworkManager = {}

local instance = nil

local isConnected = false
local isConnecting = false
local isInited = false

-- 单例 
function NetworkManager:getInstance()
    if not instance then
        instance = NetworkManager
    end
    return instance
end

function NetworkManager:registerPb(path)
    local pbFilePath = cc.FileUtils:getInstance():fullPathForFilename(path)
    buffer = cc.FileUtils:getInstance():getDataFromFile(pbFilePath)
    protobuf.register(buffer)
end

function NetworkManager:initialize()
    if self._socket then
        return
    end

    local time = cc.net.SocketTCP.getTime()
    print(string.format("socket_time:%.2f", time))

    local socket = cc.net.SocketTCP.new()
    socket:setName("GameServer")
    -- 使用默认
    -- socket:setTickTime(0.001) 
    -- socket:setReconnTime(6)
    -- socket:setConnFailTime(4)

    print("version is " .. cc.net.SocketTCP._VERSION)
    cc.net.SocketTCP._DEBUG = true

    self._socket = socket
    self._socket:addEventListener(cc.net.SocketTCP.EVENT_CONNECTED,         handler(self, self.onStatus))
    self._socket:addEventListener(cc.net.SocketTCP.EVENT_CLOSE,             handler(self, self.onStatus))
    self._socket:addEventListener(cc.net.SocketTCP.EVENT_CLOSED,            handler(self, self.onStatus))
    self._socket:addEventListener(cc.net.SocketTCP.EVENT_CONNECT_FAILURE,   handler(self, self.onStatus))
    self._socket:addEventListener(cc.net.SocketTCP.EVENT_DATA,              handler(self, self.onData))

    self._session = 0
    self._sessions = {}

    self:registerPb("res/game.pb")
end

function NetworkManager:connect(ip, port)
    print("NetworkManager:connect")
    isConnecting = true
    if isInited == false then
        isInited = true
        self:initialize()
    end
    print("开始建立socket连接")
    self._socket:connect(ip, port, true)
end

function NetworkManager:close()
    if isConnected then
        print("socket closed")
        isConnected = false
        self._socket:close()
    end
end

function NetworkManager:sendPacket(cmd, protoName, data, userData)
    print("NetworkManager:sendPacket")
    if self._socket then
        self._session = self._session + 1
        -- 这里我直接就记录着我的MainScene,回包后,用于修改MainScene的内容
        self._sessions[self._session] = userData
        local packet = packPacket(cmd, protoName, data, self._session)
        self._socket:send(packet)
    end
end

function NetworkManager:onStatus(event)
    print("socket status: %s", event.name)
    if event.name == "SOCKET_TCP_CONNECTED" then
        isConnecting = false
        isConnected = true
        print("连接成功~")
    end
end

function NetworkManager:onData(event)
    if event.data == "" then
        print("socket closed")
        return
    end
    local cmd, session, body = unpackPacket(event.data)
    print(string.format("cmd:%s, session:%s", cmd, session))
    if cmd == CMD_RESPOND_LOGIN then
        self._sessions[session]:onLoginCallback(body)
    end
end

MainScene.lua:

require("NetworkManager")

local MainScene = class("MainScene", function()
    return display.newScene("MainScene")
end)

function MainScene:ctor()
    self:initialize()
end

function MainScene:initialize()
    self._label = cc.ui.UILabel.new({
            UILabelType = 2, text = "Demo Test!", size = 24})
        :align(display.CENTER, display.cx, display.cy)
        :addTo(self)
    local items = {
        "connect",
        "login",
        "close",
    }
    self:addChild(self:createMenu(items,handler(self,self.run)))
end

function MainScene:onLoginCallback(body)
    local data = unpackBody("RespondLogin", body)
    if data.head.ecode == 1 then
        self._label:setString("登陆成功!!!")
    end
end

function MainScene:connect()
    print("MainScene:connect")
    NetworkManager:getInstance():connect("127.0.0.1", 8888)
end

function MainScene:login()
    print("MainScene:login")
    local data = {}
    data.type = 1
    data.name = "quinsmpang"
    data.password = "111111"
    NetworkManager:getInstance():sendPacket(CMD_REQUEST_LOGIN, "RequestLogin", data, self)
end

function MainScene:close()
    print("MainScene:close")
    NetworkManager:getInstance():close()
end

function MainScene:run(name)
    local f = self[name]
    if f then
        f(self)
    end
end

function MainScene:createMenu(items,callback)
    local menu = cc.ui.UIListView.new {
        viewRect = cc.rect(display.cx - 200, display.bottom + 100, 400, display.height - 200),
        direction = cc.ui.UIScrollView.DIRECTION_VERTICAL}

    for i, v in ipairs(items) do
        local item = menu:newItem()
        local content

        content = cc.ui.UIPushButton.new()
            :setButtonSize(200, 40)
            :setButtonLabel(cc.ui.UILabel.new({text = v, size = 24}))
            :onButtonClicked(function(event)
                callback(v)
            end)
        content:setTouchSwallowEnabled(false)
        item:addContent(content)
        item:setItemSize(120, 40)

        menu:addItem(item)
    end
    menu:reload()
    return menu
end

function MainScene:onEnter()
end

function MainScene:onExit()
end

return MainScene

因为工具原因没有看到鼠标点击,其实就顺序点了一遍。

Demo效果

最后提供一下,源码:http://pan.baidu.com/s/1jHR9fwU
相当累呀,写了很久,是边调边写的,从零开始,可能还有很多地方有问题,暂时就这样,后面可以继续完善下,还有以上的NetworkManager有些缺陷,细心的同学会发现使用TCP带来的一些问题,暂时卖个关子,后面告诉大家。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容