> 文章列表 > 【从零开始学Skynet】实战篇《球球大作战》(六):gateway代码设计(中)

【从零开始学Skynet】实战篇《球球大作战》(六):gateway代码设计(中)

【从零开始学Skynet】实战篇《球球大作战》(六):gateway代码设计(中)

1、编码和解码

        我们来实现两个辅助方法str_unpackstr_pack,用于消息的解码和编码。

(1)str_unpack代码

local str_unpack = function(msgstr)local msg = {}while true dolocal arg, rest = string.match( msgstr, "(.-),(.*)")if arg thenmsgstr = resttable.insert(msg, arg)elsetable.insert(msg, msgstr)breakendendreturn msg[1], msg
end

str_unpack是一个解码方法:

  • 参数msgstr代表消息字符串。
  • 内部是个循环结构,每次循环都由string.match匹配逗号前的字符。

例如:传入的msgstr为“login, 101, 134”,则匹配后arg的值为“login”、rest的值“101,134”;               传入的msgstr为“101, 134”,则匹配后arg的值为“101”、rest的值为“134”。

  • 每次取值后,它会把参数插入msg表,msg表用作协议对象,方便后续取值。
  • str_unpack会返回两个值,第一个值msg[1]是协议名称(协议对象第一个元素),第二个值即为协议对象

如下图所示:

        图中msgstr的值为“login,101,134”。第一个返回值cmd是字符串“login”,第二个返回值msg是一个Lua表。

 (2)str_pack代码

local str_pack = function(cmd, msg)return table.concat( msg, ",").."\\r\\n"
end

str_pack实现了与str_unpack相反的功能,如下图所示:

它将协议对象转换成字符串,并添加分隔符“\\r\\n”

 2、消息分发

 消息处理方法process_msg如下代码所示:

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 thenlocal node = skynet.getenv("node")local nodecfg = runconfig[node]local loginid = math.random(1, #nodecfg.login)local login = "login"..loginidskynet.send(login, "lua", "client", fd, cmd, msg)--完成登录流程elselocal gplayer = players[playerid]local agent = gplayer.agentskynet.send(agent, "lua", "client", cmd, msg)end
end

虽然代码只有十多行,但还是有点复杂,可通过如下四个部分理解这个方法:

1、消息解码

    通过str_unpack解码消息,相关变量的含义如下。

  • msgstr:切分后的消息,如“login,101,123”。  
  • cmd:消息名,如login。   
  • msg:消息对象,如Lua表{[1]="login", [2]="101", [3]="123"}。         

2、 如果尚未登录

     对于代码“if not playerid”为真的部分,程序将随机选取同节点的一个登录服务器转发消息,相关变量的含义如下。

  • conn:定义的连接对象。
  • playerid:如果完成登录,那么它会保存着玩家id,否则为空。
  • node:中配置文件的节点名,如“node1”。
  • nodecfg:中配置文件的节点配置,如{gateway={...},login={..}}。
  • loginid:随机的login服务编号。
  • login:随机的login服务名称,如“login2”。

3、 如果已登录

   将消息转发给对应的agent,相关变量的含义如下。

  • gpalayer:定义的gateplayer对象。
  • agent:该连接对应的代理服务id。

4、client消息

        消息转发使用了skynet.send(srv,"lua","client", ...)的形式,其中的client是自定义的消息名(skynet中的概念,指服务间传递的消息名字,它与cmd的区别是cmd客户端协议的名字)。在我们之前封装好的service模块中,loginagent可以用s.resp.client接收转发的消息,再根据cmd做不同处理。

         下图是process_msg方法的示意图,gateway收到客户端协议后,如果玩家已登录,它会将消息转发给对应的代理(阶段③);如果未登录,gateway会随机选取一个登录服务器,并将消息转发给它处理。gateway保持着轻量级的功能,它只转发协议,不做具体处理。

 

         读者可以先屏蔽掉process_msg中分发消息的代码,用telnet等客户端测试gateway能否正常工作。由于在telnet换行即为输入分隔符“\\r\\n”,因此直接用换行分割消息即可。

 3、发送消息接口

        gateway将消息传给loginagentloginagent也需要给客户端回应。比如,客户端发送登录协议,login校验失败后,要给客户端回应“账号或密码错误”,这个过程如下图所示,它先将消息发送给gateway(阶段③),再由gateway(阶段④)转发。

 下面编写login给客户端发送消息的代码。

(1)send_by_fd代码:

s.resp.send_by_fd = function(source, fd, msg)if not conns[fd] thenreturnendlocal buff = str_pack(msg[1], msg)skynet.error("send "..fd.." ["..msg[1].."] {"..table.concat( msg, ",").."}")socket.write(fd, buff)
end

        send_by_fd方法用于login服务的消息转发,功能是将消息发送到指定fd的客户端。它先用str_pack编码消息,然后使用socket.write将它发送给客户端。

  • 参数source:消息发送方,比如来自“login1”,
  • 参数fd和:客户端fd
  • 参数msg:消息内容。

 (2)send代码:

s.resp.send = function(source, playerid, msg)local gplayer = players[playerid]if gplayer == nil thenreturnendlocal c = gplayer.connif c == nil thenreturnends.resp.send_by_fd(nil, c.fd, msg)
end

        send方法用于agent的消息转发,功能是将消息发送给指定玩家id的客户端。它先根据玩家id(playerid)查找对应客户端连接,再调用send_by_fd发送。

        这两个接口会在后续实现login和agent时调用。

设计前沿