很高兴来到这一步,准备了那么久,我们迎来了正式后端编码阶段,对于做前端的同学这是一个相当煎熬的过程,废话少说,赶紧开始。
回到上篇文章,我说过我们的编写的服务端代码都放在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
因为工具原因没有看到鼠标点击,其实就顺序点了一遍。
最后提供一下,源码:http://pan.baidu.com/s/1jHR9fwU
相当累呀,写了很久,是边调边写的,从零开始,可能还有很多地方有问题,暂时就这样,后面可以继续完善下,还有以上的NetworkManager有些缺陷,细心的同学会发现使用TCP带来的一些问题,暂时卖个关子,后面告诉大家。