Skynet服务端框架搭建4-分布式登录流程

迷南。 2022-08-28 00:42 367阅读 0赞

Skynet服务端框架搭建4-分布式登录流程

本篇在上一节的基础上,处理玩家登陆逻辑,以便熟悉整套框架

连通网关和玩家

首先修改上节的gateway模块,要把gateway和agent关联起来,定义conns和players两个表,以及conn和gateplayer两个类

service/gateway/init.lua

  1. conns = {
  2. } --[socket_id] = conn
  3. players = {
  4. } --[playerid] = gateplayer
  5. --连接类
  6. function conn()
  7. local m = {
  8. fd = nil,
  9. playerid = nil,
  10. }
  11. return m
  12. end
  13. --玩家类
  14. function gateplayer()
  15. local m = {
  16. playerid = nil,
  17. agent = nil,
  18. conn = nil,
  19. }
  20. return m
  21. end

接受客户端的连接

首先要开启Socket监听,前面配置了runconfig文件,这里也读取该配置,找到gateway的监听端口port,然后使用skynet.socket模块的listen和start方法开启监听。当有客户端连接时,start方法的回调函数connect会被调用

service/gateway/init.lua

  1. local socket = require "skynet.socket"
  2. local runconfig = require "runconfig"
  3. function s.init()
  4. local node = skynet.getenv("node")
  5. local nodecfg = runconfig[node]
  6. local port = nodecfg.gateway[s.id].port
  7. local listenfd = socket.listen("0.0.0.0", port)
  8. skynet.error("Listen socket :", "0.0.0.0", port)
  9. socket.start(listenfd , connect)
  10. end
  11. --有新连接时
  12. local connect = function(fd, addr)
  13. print("connect from " .. addr .. " " .. fd)
  14. local c = conn()
  15. conns[fd] = c
  16. c.fd = fd
  17. skynet.fork(recv_loop, fd)
  18. end

recv_loop负责接收客户端消息,参数fd由skynet.fork传入,表示客户端的标识。

service/gateway/init.lua

  1. --每一条连接接收数据处理
  2. --协议格式 cmd,arg1,arg2,...#
  3. local recv_loop = function(fd)
  4. socket.start(fd)
  5. skynet.error("socket connected " ..fd)
  6. local readbuff = ""
  7. while true do
  8. local recvstr = socket.read(fd)
  9. if recvstr then
  10. readbuff = readbuff..recvstr
  11. readbuff = process_buff(fd, readbuff)
  12. else
  13. skynet.error("socket close " ..fd)
  14. disconnect(fd)
  15. socket.close(fd)
  16. return
  17. end
  18. end
  19. end

处理客户端协议

process_buff中实现消息切分的工作,如果readbuff的内容是”login,101,134\r\nwork\r\nwo”,则会处理成”login,101,123”和”work”两条消息返回给recv_loop

service/gateway/init.lua

  1. local process_buff = function(fd, readbuff)
  2. while true do
  3. local msgstr, rest = string.match( readbuff, "(.-)\r\n(.*)")
  4. if msgstr then
  5. readbuff = rest
  6. process_msg(fd, msgstr)
  7. else
  8. return readbuff
  9. end
  10. end
  11. end

fd为客户端连接标识,readbuff为接收数据的缓冲区,这里的string.match就是正则规则,”(.-)\r\n(.*)”意思是把取出的第一条消息和剩余的部分

编码和解码

其实这个很像之前我写的Erlang游戏服务端mini项目实战4_消息协议层搭建1(封装协议号),其实就是客户端和服务端之间的封包和解包

service/gateway/init.lua

  1. local str_pack = function(cmd, msg)
  2. return table.concat( msg, ",").."\r\n"
  3. end
  4. local str_unpack = function(msgstr)
  5. local msg = {
  6. }
  7. while true do
  8. local arg, rest = string.match( msgstr, "(.-),(.*)")
  9. if arg then
  10. msgstr = rest
  11. table.insert(msg, arg)
  12. else
  13. table.insert(msg, msgstr)
  14. break
  15. end
  16. end
  17. return msg[1], msg
  18. end

消息分发

这里分为两种情况,如果玩家没登录则进入分配agent让客户端进入login协议,如果已经登录了链接到对应的agent服务

service/gateway/init.lua

  1. local process_msg = function(fd, msgstr)
  2. local cmd, msg = str_unpack(msgstr)
  3. skynet.error("recv "..fd.." ["..cmd.."] {"..table.concat( msg, ",").."}")
  4. local conn = conns[fd]
  5. local playerid = conn.playerid
  6. --尚未完成登录流程
  7. if not playerid then
  8. local node = skynet.getenv("node")
  9. local nodecfg = runconfig[node]
  10. local loginid = math.random(1, #nodecfg.login)
  11. local login = "login"..loginid
  12. skynet.send(login, "lua", "client", fd, cmd, msg)
  13. --完成登录流程
  14. else
  15. local gplayer = players[playerid]
  16. local agent = gplayer.agent
  17. skynet.send(agent, "lua", "client", cmd, msg)
  18. end
  19. end

发消息接口

service/gateway/init.lua

  1. s.resp.send_by_fd = function(source, fd, msg)
  2. if not conns[fd] then
  3. return
  4. end
  5. local buff = str_pack(msg[1], msg)
  6. skynet.error("send "..fd.." ["..msg[1].."] {"..table.concat( msg, ",").."}")
  7. socket.write(fd, buff)
  8. end
  9. s.resp.send = function(source, playerid, msg)
  10. local gplayer = players[playerid]
  11. if gplayer == nil then
  12. return
  13. end
  14. local c = gplayer.conn
  15. if c == nil then
  16. return
  17. end
  18. s.resp.send_by_fd(nil, c.fd, msg)
  19. end

确认登录&登出接口

service/gateway/init.lua

  1. s.resp.sure_agent = function(source, fd, playerid, agent)
  2. local conn = conns[fd]
  3. if not conn then --登陆过程中已经下线
  4. skynet.call("agentmgr", "lua", "reqkick", playerid, "未完成登陆即下线")
  5. return false
  6. end
  7. conn.playerid = playerid
  8. local gplayer = gateplayer()
  9. gplayer.playerid = playerid
  10. gplayer.agent = agent
  11. gplayer.conn = conn
  12. players[playerid] = gplayer
  13. return true
  14. end
  15. local disconnect = function(fd)
  16. local c = conns[fd]
  17. if not c then
  18. return
  19. end
  20. local playerid = c.playerid
  21. --还没完成登录
  22. if not playerid then
  23. return
  24. --已在游戏中
  25. else
  26. players[playerid] = nil
  27. local reason = "断线"
  28. skynet.call("agentmgr", "lua", "reqkick", playerid, reason)
  29. end
  30. end

最后还有踢出接口

  1. s.resp.kick = function(source, playerid)
  2. local gplayer = players[playerid]
  3. if not gplayer then
  4. return
  5. end
  6. local c = gplayer.conn
  7. players[playerid] = nil
  8. if not c then
  9. return
  10. end
  11. conns[c.fd] = nil
  12. disconnect(c.fd)
  13. socket.close(c.fd)
  14. end

接下来编写第二个服务——登录

登录协议

刚才客户端发送的登录请求是 login,123,456 三个参数分别是协议名、玩家id、密码,我们服务端只需要返回 login,0/1,登陆成功/登录失败 三个参数分别是协议名,结果参数和msg

login/init.lua

  1. local skynet = require "skynet"
  2. local s = require "service"
  3. s.client = {
  4. }
  5. s.resp.client = function(source, fd, cmd, msg)
  6. if s.client[cmd] then
  7. local ret_msg = s.client[cmd]( fd, msg, source)
  8. skynet.send(source, "lua", "send_by_fd", fd, ret_msg)
  9. else
  10. skynet.error("s.resp.client fail", cmd)
  11. end
  12. end
  13. s.client.login = function(fd, msg, source)
  14. local playerid = tonumber(msg[2])
  15. local pw = tonumber(msg[3])
  16. local gate = source
  17. node = skynet.getenv("node")
  18. --校验用户名密码
  19. if pw ~= 123 then
  20. return {
  21. "login", 1, "密码错误"}
  22. end
  23. --发给agentmgr
  24. local isok, agent = skynet.call("agentmgr", "lua", "reqlogin", playerid, node, gate)
  25. if not isok then
  26. return {
  27. "login", 1, "请求mgr失败"}
  28. end
  29. --回应gate
  30. local isok = skynet.call(gate, "lua", "sure_agent", fd, playerid, agent)
  31. if not isok then
  32. return {
  33. "login", 1, "gate注册失败"}
  34. end
  35. skynet.error("login succ "..playerid)
  36. return {
  37. "login", 0, "登陆成功"}
  38. end
  39. s.start(...)

真正业务上的时候可能还得是从数据库中存取这个啦

agentmgr

再看看agentmgr模块,他是管理agent的服务,是控制玩家登陆流程的主要模块,模块内维护一个列表players,保存着所有玩家的在线状态。

agentmgr/init.lua

  1. local skynet = require "skynet"
  2. local s = require "service"
  3. --状态
  4. STATUS = {
  5. LOGIN = 2,
  6. GAME = 3,
  7. LOGOUT = 4,
  8. }
  9. --玩家列表
  10. local players = {
  11. }
  12. --玩家类
  13. function mgrplayer()
  14. local m = {
  15. playerid = nil,
  16. node = nil,
  17. agent = nil,
  18. status = nil,
  19. gate = nil,
  20. }
  21. return m
  22. end
  23. s.resp.reqlogin = function(source, playerid, node, gate)
  24. local mplayer = players[playerid]
  25. --登陆过程禁止顶替
  26. if mplayer and mplayer.status == STATUS.LOGOUT then
  27. skynet.error("reqlogin fail, at status LOGOUT " ..playerid )
  28. return false
  29. end
  30. if mplayer and mplayer.status == STATUS.LOGIN then
  31. skynet.error("reqlogin fail, at status LOGIN " ..playerid)
  32. return false
  33. end
  34. --在线,顶替
  35. if mplayer then
  36. local pnode = mplayer.node
  37. local pagent = mplayer.agent
  38. local pgate = mplayer.gate
  39. mplayer.status = STATUS.LOGOUT,
  40. s.call(pnode, pagent, "kick")
  41. s.send(pnode, pagent, "exit")
  42. s.send(pnode, pgate, "send", playerid, {
  43. "kick","顶替下线"})
  44. s.call(pnode, pgate, "kick", playerid)
  45. end
  46. --上线
  47. local player = mgrplayer()
  48. player.playerid = playerid
  49. player.node = node
  50. player.gate = gate
  51. player.agent = nil
  52. player.status = STATUS.LOGIN
  53. players[playerid] = player
  54. local agent = s.call(node, "nodemgr", "newservice", "agent", "agent", playerid)
  55. player.agent = agent
  56. player.status = STATUS.GAME
  57. return true, agent
  58. end
  59. s.resp.reqkick = function(source, playerid, reason)
  60. local mplayer = players[playerid]
  61. if not mplayer then
  62. return false
  63. end
  64. if mplayer.status ~= STATUS.GAME then
  65. return false
  66. end
  67. local pnode = mplayer.node
  68. local pagent = mplayer.agent
  69. local pgate = mplayer.gate
  70. mplayer.status = STATUS.LOGOUT
  71. s.call(pnode, pagent, "kick")
  72. s.send(pnode, pagent, "exit")
  73. s.send(pnode, pgate, "kick", playerid)
  74. players[playerid] = nil
  75. return true
  76. end
  77. --情况 永不下线
  78. s.start(...)

代码中包括请求登陆和请求登出的接口

nodemgr

nodemgr/init.lua

  1. local skynet = require "skynet"
  2. local s = require "service"
  3. s.resp.newservice = function(source, name, ...)
  4. local srv = skynet.newservice(name, ...)
  5. return srv
  6. end
  7. s.start(...)

nodemgr管理节点服务,每个节点开一个新的服务,这里只是简单调用了newservice方法

agent

最后一个模块了,

agent/init.lua

  1. local skynet = require "skynet"
  2. local s = require "service"
  3. s.client = {
  4. }
  5. s.gate = nil
  6. require "scene"
  7. s.resp.client = function(source, cmd, msg)
  8. s.gate = source
  9. if s.client[cmd] then
  10. local ret_msg = s.client[cmd]( msg, source)
  11. if ret_msg then
  12. skynet.send(source, "lua", "send", s.id, ret_msg)
  13. end
  14. else
  15. skynet.error("s.resp.client fail", cmd)
  16. end
  17. end
  18. s.client.work = function(msg)
  19. s.data.coin = s.data.coin + 1
  20. return {
  21. "work", s.data.coin}
  22. end
  23. s.resp.kick = function(source)
  24. s.leave_scene()
  25. --在此处保存角色数据
  26. skynet.sleep(200)
  27. end
  28. s.resp.exit = function(source)
  29. skynet.exit()
  30. end
  31. s.resp.send = function(source, msg)
  32. skynet.send(s.gate, "lua", "send", s.id, msg)
  33. end
  34. s.init = function( )
  35. --playerid = s.id
  36. --在此处加载角色数据
  37. skynet.sleep(200)
  38. s.data = {
  39. coin = 100,
  40. hp = 200,
  41. }
  42. end
  43. s.start(...)

这里主要实现的是消息分发、数据加载和保存退出

测试登录流程

修改main.lua里的内容,调用刚才写的各个模块

main.lua

  1. local skynet = require "skynet"
  2. local skynet_manager = require "skynet.manager"
  3. local runconfig = require "runconfig"
  4. local cluster = require "skynet.cluster"
  5. skynet.start(function()
  6. --初始化
  7. local mynode = skynet.getenv("node")
  8. local nodecfg = runconfig[mynode]
  9. --节点管理
  10. local nodemgr = skynet.newservice("nodemgr","nodemgr", 0)
  11. skynet.name("nodemgr", nodemgr)
  12. --集群
  13. cluster.reload(runconfig.cluster)
  14. cluster.open(mynode)
  15. --gate
  16. for i, v in pairs(nodecfg.gateway or {
  17. }) do
  18. local srv = skynet.newservice("gateway","gateway", i)
  19. skynet.name("gateway"..i, srv)
  20. end
  21. --login
  22. for i, v in pairs(nodecfg.login or {
  23. }) do
  24. local srv = skynet.newservice("login","login", i)
  25. skynet.name("login"..i, srv)
  26. end
  27. --agentmgr
  28. local anode = runconfig.agentmgr.node
  29. if mynode == anode then
  30. local srv = skynet.newservice("agentmgr", "agentmgr", 0)
  31. skynet.name("agentmgr", srv)
  32. else
  33. local proxy = cluster.proxy(anode, "agentmgr")
  34. skynet.name("agentmgr", proxy)
  35. end
  36. --scene (sid->sceneid)
  37. for _, sid in pairs(runconfig.scene[mynode] or {
  38. }) do
  39. local srv = skynet.newservice("scene", "scene", sid)
  40. skynet.name("scene"..sid, srv)
  41. end
  42. --退出自身
  43. skynet.exit()
  44. end)

测试

GIF 2021-10-29 20-27-57

发表评论

表情:
评论列表 (有 0 条评论,367人围观)

还没有评论,来说两句吧...

相关阅读