Skynet服务端框架搭建4-分布式登录流程
Skynet服务端框架搭建4-分布式登录流程
本篇在上一节的基础上,处理玩家登陆逻辑,以便熟悉整套框架
连通网关和玩家
首先修改上节的gateway模块,要把gateway和agent关联起来,定义conns和players两个表,以及conn和gateplayer两个类
service/gateway/init.lua
conns = {
} --[socket_id] = conn
players = {
} --[playerid] = gateplayer
--连接类
function conn()
local m = {
fd = nil,
playerid = nil,
}
return m
end
--玩家类
function gateplayer()
local m = {
playerid = nil,
agent = nil,
conn = nil,
}
return m
end
接受客户端的连接
首先要开启Socket监听,前面配置了runconfig文件,这里也读取该配置,找到gateway的监听端口port,然后使用skynet.socket模块的listen和start方法开启监听。当有客户端连接时,start方法的回调函数connect会被调用
service/gateway/init.lua
local socket = require "skynet.socket"
local runconfig = require "runconfig"
function s.init()
local node = skynet.getenv("node")
local nodecfg = runconfig[node]
local port = nodecfg.gateway[s.id].port
local listenfd = socket.listen("0.0.0.0", port)
skynet.error("Listen socket :", "0.0.0.0", port)
socket.start(listenfd , connect)
end
--有新连接时
local connect = function(fd, addr)
print("connect from " .. addr .. " " .. fd)
local c = conn()
conns[fd] = c
c.fd = fd
skynet.fork(recv_loop, fd)
end
recv_loop负责接收客户端消息,参数fd由skynet.fork传入,表示客户端的标识。
service/gateway/init.lua
--每一条连接接收数据处理
--协议格式 cmd,arg1,arg2,...#
local recv_loop = function(fd)
socket.start(fd)
skynet.error("socket connected " ..fd)
local readbuff = ""
while true do
local recvstr = socket.read(fd)
if recvstr then
readbuff = readbuff..recvstr
readbuff = process_buff(fd, readbuff)
else
skynet.error("socket close " ..fd)
disconnect(fd)
socket.close(fd)
return
end
end
end
处理客户端协议
process_buff中实现消息切分的工作,如果readbuff的内容是”login,101,134\r\nwork\r\nwo”,则会处理成”login,101,123”和”work”两条消息返回给recv_loop
service/gateway/init.lua
local process_buff = function(fd, readbuff)
while true do
local msgstr, rest = string.match( readbuff, "(.-)\r\n(.*)")
if msgstr then
readbuff = rest
process_msg(fd, msgstr)
else
return readbuff
end
end
end
fd为客户端连接标识,readbuff为接收数据的缓冲区,这里的string.match就是正则规则,”(.-)\r\n(.*)”意思是把取出的第一条消息和剩余的部分
编码和解码
其实这个很像之前我写的Erlang游戏服务端mini项目实战4_消息协议层搭建1(封装协议号),其实就是客户端和服务端之间的封包和解包
service/gateway/init.lua
local str_pack = function(cmd, msg)
return table.concat( msg, ",").."\r\n"
end
local str_unpack = function(msgstr)
local msg = {
}
while true do
local arg, rest = string.match( msgstr, "(.-),(.*)")
if arg then
msgstr = rest
table.insert(msg, arg)
else
table.insert(msg, msgstr)
break
end
end
return msg[1], msg
end
消息分发
这里分为两种情况,如果玩家没登录则进入分配agent让客户端进入login协议,如果已经登录了链接到对应的agent服务
service/gateway/init.lua
local process_msg = function(fd, msgstr)
local cmd, msg = str_unpack(msgstr)
skynet.error("recv "..fd.." ["..cmd.."] {"..table.concat( msg, ",").."}")
local conn = conns[fd]
local playerid = conn.playerid
--尚未完成登录流程
if not playerid then
local node = skynet.getenv("node")
local nodecfg = runconfig[node]
local loginid = math.random(1, #nodecfg.login)
local login = "login"..loginid
skynet.send(login, "lua", "client", fd, cmd, msg)
--完成登录流程
else
local gplayer = players[playerid]
local agent = gplayer.agent
skynet.send(agent, "lua", "client", cmd, msg)
end
end
发消息接口
service/gateway/init.lua
s.resp.send_by_fd = function(source, fd, msg)
if not conns[fd] then
return
end
local buff = str_pack(msg[1], msg)
skynet.error("send "..fd.." ["..msg[1].."] {"..table.concat( msg, ",").."}")
socket.write(fd, buff)
end
s.resp.send = function(source, playerid, msg)
local gplayer = players[playerid]
if gplayer == nil then
return
end
local c = gplayer.conn
if c == nil then
return
end
s.resp.send_by_fd(nil, c.fd, msg)
end
确认登录&登出接口
service/gateway/init.lua
s.resp.sure_agent = function(source, fd, playerid, agent)
local conn = conns[fd]
if not conn then --登陆过程中已经下线
skynet.call("agentmgr", "lua", "reqkick", playerid, "未完成登陆即下线")
return false
end
conn.playerid = playerid
local gplayer = gateplayer()
gplayer.playerid = playerid
gplayer.agent = agent
gplayer.conn = conn
players[playerid] = gplayer
return true
end
local disconnect = function(fd)
local c = conns[fd]
if not c then
return
end
local playerid = c.playerid
--还没完成登录
if not playerid then
return
--已在游戏中
else
players[playerid] = nil
local reason = "断线"
skynet.call("agentmgr", "lua", "reqkick", playerid, reason)
end
end
最后还有踢出接口
s.resp.kick = function(source, playerid)
local gplayer = players[playerid]
if not gplayer then
return
end
local c = gplayer.conn
players[playerid] = nil
if not c then
return
end
conns[c.fd] = nil
disconnect(c.fd)
socket.close(c.fd)
end
接下来编写第二个服务——登录
登录协议
刚才客户端发送的登录请求是 login,123,456 三个参数分别是协议名、玩家id、密码,我们服务端只需要返回 login,0/1,登陆成功/登录失败 三个参数分别是协议名,结果参数和msg
login/init.lua
local skynet = require "skynet"
local s = require "service"
s.client = {
}
s.resp.client = function(source, fd, cmd, msg)
if s.client[cmd] then
local ret_msg = s.client[cmd]( fd, msg, source)
skynet.send(source, "lua", "send_by_fd", fd, ret_msg)
else
skynet.error("s.resp.client fail", cmd)
end
end
s.client.login = function(fd, msg, source)
local playerid = tonumber(msg[2])
local pw = tonumber(msg[3])
local gate = source
node = skynet.getenv("node")
--校验用户名密码
if pw ~= 123 then
return {
"login", 1, "密码错误"}
end
--发给agentmgr
local isok, agent = skynet.call("agentmgr", "lua", "reqlogin", playerid, node, gate)
if not isok then
return {
"login", 1, "请求mgr失败"}
end
--回应gate
local isok = skynet.call(gate, "lua", "sure_agent", fd, playerid, agent)
if not isok then
return {
"login", 1, "gate注册失败"}
end
skynet.error("login succ "..playerid)
return {
"login", 0, "登陆成功"}
end
s.start(...)
真正业务上的时候可能还得是从数据库中存取这个啦
agentmgr
再看看agentmgr模块,他是管理agent的服务,是控制玩家登陆流程的主要模块,模块内维护一个列表players,保存着所有玩家的在线状态。
agentmgr/init.lua
local skynet = require "skynet"
local s = require "service"
--状态
STATUS = {
LOGIN = 2,
GAME = 3,
LOGOUT = 4,
}
--玩家列表
local players = {
}
--玩家类
function mgrplayer()
local m = {
playerid = nil,
node = nil,
agent = nil,
status = nil,
gate = nil,
}
return m
end
s.resp.reqlogin = function(source, playerid, node, gate)
local mplayer = players[playerid]
--登陆过程禁止顶替
if mplayer and mplayer.status == STATUS.LOGOUT then
skynet.error("reqlogin fail, at status LOGOUT " ..playerid )
return false
end
if mplayer and mplayer.status == STATUS.LOGIN then
skynet.error("reqlogin fail, at status LOGIN " ..playerid)
return false
end
--在线,顶替
if mplayer then
local pnode = mplayer.node
local pagent = mplayer.agent
local pgate = mplayer.gate
mplayer.status = STATUS.LOGOUT,
s.call(pnode, pagent, "kick")
s.send(pnode, pagent, "exit")
s.send(pnode, pgate, "send", playerid, {
"kick","顶替下线"})
s.call(pnode, pgate, "kick", playerid)
end
--上线
local player = mgrplayer()
player.playerid = playerid
player.node = node
player.gate = gate
player.agent = nil
player.status = STATUS.LOGIN
players[playerid] = player
local agent = s.call(node, "nodemgr", "newservice", "agent", "agent", playerid)
player.agent = agent
player.status = STATUS.GAME
return true, agent
end
s.resp.reqkick = function(source, playerid, reason)
local mplayer = players[playerid]
if not mplayer then
return false
end
if mplayer.status ~= STATUS.GAME then
return false
end
local pnode = mplayer.node
local pagent = mplayer.agent
local pgate = mplayer.gate
mplayer.status = STATUS.LOGOUT
s.call(pnode, pagent, "kick")
s.send(pnode, pagent, "exit")
s.send(pnode, pgate, "kick", playerid)
players[playerid] = nil
return true
end
--情况 永不下线
s.start(...)
代码中包括请求登陆和请求登出的接口
nodemgr
nodemgr/init.lua
local skynet = require "skynet"
local s = require "service"
s.resp.newservice = function(source, name, ...)
local srv = skynet.newservice(name, ...)
return srv
end
s.start(...)
nodemgr管理节点服务,每个节点开一个新的服务,这里只是简单调用了newservice方法
agent
最后一个模块了,
agent/init.lua
local skynet = require "skynet"
local s = require "service"
s.client = {
}
s.gate = nil
require "scene"
s.resp.client = function(source, cmd, msg)
s.gate = source
if s.client[cmd] then
local ret_msg = s.client[cmd]( msg, source)
if ret_msg then
skynet.send(source, "lua", "send", s.id, ret_msg)
end
else
skynet.error("s.resp.client fail", cmd)
end
end
s.client.work = function(msg)
s.data.coin = s.data.coin + 1
return {
"work", s.data.coin}
end
s.resp.kick = function(source)
s.leave_scene()
--在此处保存角色数据
skynet.sleep(200)
end
s.resp.exit = function(source)
skynet.exit()
end
s.resp.send = function(source, msg)
skynet.send(s.gate, "lua", "send", s.id, msg)
end
s.init = function( )
--playerid = s.id
--在此处加载角色数据
skynet.sleep(200)
s.data = {
coin = 100,
hp = 200,
}
end
s.start(...)
这里主要实现的是消息分发、数据加载和保存退出
测试登录流程
修改main.lua里的内容,调用刚才写的各个模块
main.lua
local skynet = require "skynet"
local skynet_manager = require "skynet.manager"
local runconfig = require "runconfig"
local cluster = require "skynet.cluster"
skynet.start(function()
--初始化
local mynode = skynet.getenv("node")
local nodecfg = runconfig[mynode]
--节点管理
local nodemgr = skynet.newservice("nodemgr","nodemgr", 0)
skynet.name("nodemgr", nodemgr)
--集群
cluster.reload(runconfig.cluster)
cluster.open(mynode)
--gate
for i, v in pairs(nodecfg.gateway or {
}) do
local srv = skynet.newservice("gateway","gateway", i)
skynet.name("gateway"..i, srv)
end
--login
for i, v in pairs(nodecfg.login or {
}) do
local srv = skynet.newservice("login","login", i)
skynet.name("login"..i, srv)
end
--agentmgr
local anode = runconfig.agentmgr.node
if mynode == anode then
local srv = skynet.newservice("agentmgr", "agentmgr", 0)
skynet.name("agentmgr", srv)
else
local proxy = cluster.proxy(anode, "agentmgr")
skynet.name("agentmgr", proxy)
end
--scene (sid->sceneid)
for _, sid in pairs(runconfig.scene[mynode] or {
}) do
local srv = skynet.newservice("scene", "scene", sid)
skynet.name("scene"..sid, srv)
end
--退出自身
skynet.exit()
end)
还没有评论,来说两句吧...