1、编码和解码
我们来实现两个辅助方法str_unpack和str_pack,用于消息的解码和编码。
(1)str_unpack代码
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
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 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
虽然代码只有十多行,但还是有点复杂,可通过如下四个部分理解这个方法:
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模块中,login和agent可以用s.resp.client接收转发的消息,再根据cmd做不同处理。
下图是process_msg方法的示意图,gateway收到客户端协议后,如果玩家已登录,它会将消息转发给对应的代理(阶段③);如果未登录,gateway会随机选取一个登录服务器,并将消息转发给它处理。gateway保持着轻量级的功能,它只转发协议,不做具体处理。
读者可以先屏蔽掉process_msg中分发消息的代码,用telnet等客户端测试gateway能否正常工作。由于在telnet换行即为输入分隔符“\r\n”,因此直接用换行分割消息即可。
3、发送消息接口
gateway将消息传给login或agent,login或agent也需要给客户端回应。比如,客户端发送登录协议,login校验失败后,要给客户端回应“账号或密码错误”,这个过程如下图所示,它先将消息发送给gateway(阶段③),再由gateway(阶段④)转发。
下面编写login给客户端发送消息的代码。
(1)send_by_fd代码:
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
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 then
return
end
local c = gplayer.conn
if c == nil then
return
end
s.resp.send_by_fd(nil, c.fd, msg)
end
send方法用于agent的消息转发,功能是将消息发送给指定玩家id的客户端。它先根据玩家id(playerid)查找对应客户端连接,再调用send_by_fd发送。
这两个接口会在后续实现login和agent时调用。