基于skynet框架业务中的gateway实现分析
gate.lua
基于gateserver构建的通用网关服务,负责连接管理和消息转发
lua
local skynet = require "skynet"
local gateserver = require "snax.gateserver"
local watchdog
local connection = {} -- fd -> connection : { fd , client, agent , ip, mode }
skynet.register_protocol {
name = "client",
id = skynet.PTYPE_CLIENT,
}
local handler = {}
function handler.open(source, conf)
watchdog = conf.watchdog or source
return conf.address, conf.port
end
function handler.message(fd, msg, sz)
-- recv a package, forward it
local c = connection[fd]
local agent = c.agent
if agent then
-- It's safe to redirect msg directly , gateserver framework will not free msg.
skynet.redirect(agent, c.client, "client", fd, msg, sz)
else
skynet.send(watchdog, "lua", "socket", "data", fd, skynet.tostring(msg, sz))
-- skynet.tostring will copy msg to a string, so we must free msg here.
skynet.trash(msg,sz)
end
end
function handler.connect(fd, addr)
local c = {
fd = fd,
ip = addr,
}
connection[fd] = c
skynet.send(watchdog, "lua", "socket", "open", fd, inspect(addr))
end
local function unforward(c)
if c.agent then
c.agent = nil
c.client = nil
end
end
local function close_fd(fd)
local c = connection[fd]
if c then
unforward(c)
connection[fd] = nil
end
end
function handler.disconnect(fd)
close_fd(fd)
skynet.send(watchdog, "lua", "socket", "close", fd)
end
function handler.error(fd, msg)
close_fd(fd)
skynet.send(watchdog, "lua", "socket", "error", fd, msg)
end
function handler.warning(fd, size)
skynet.send(watchdog, "lua", "socket", "warning", fd, size)
end
local CMD = {}
function CMD.forward(source, fd, client, address)
local c = assert(connection[fd])
unforward(c)
c.client = client or 0
c.agent = address or source
gateserver.openclient(fd)
end
function CMD.accept(source, fd)
local c = assert(connection[fd])
unforward(c)
gateserver.openclient(fd)
end
function CMD.kick(source, fd)
gateserver.closeclient(fd)
end
function handler.command(cmd, source, ...)
local f = assert(CMD[cmd])
return f(source, ...)
end
gateserver.start(handler)
关键数据结构
lua
local connection = {} -- fd -> connection : { fd, client, agent, ip, mode }
-- fd: 文件描述符
-- client: 客户端地址
-- agent: 代理服务地址
-- ip: 客户端IP
消息转发逻辑
其通过skynet.register_protocol注册了"client"类型协议,核心转发逻辑如下:
lua
function handler.message(fd, msg, sz)local c = connection[fd]local agent = c.agent
if agent then-- 已分配agent,直接转发消息
skynet.redirect(agent, c.client, "client", fd, msg, sz)else-- 未分配agent,发给watchdog处理
skynet.send(watchdog, "lua", "socket", "data", fd, skynet.tostring(msg, sz))
skynet.trash(msg,sz) -- 释放内存endend
连接管理命令
lua
local CMD = {}
function CMD.forward(source, fd, client, address)
-- 将连接转发给指定agent
local c = assert(connection[fd])
unforward(c)
c.client = client or 0
c.agent = address or source
gateserver.openclient(fd) -- 开始接收数据
end
function CMD.kick(source, fd)
-- 踢掉客户端
gateserver.closeclient(fd)
end
gateserver.lua
这是最底层的网关服务器实现,封装了Skynet的socket操作,提供TCP连接管理的基础框架,其内部注册了"socket"类型的协议,通过gate将gate中的"socket"类型调用到gateserver中的对于指令
lua
local skynet = require "skynet"
local netpack = require "skynet.netpack"
local socketdriver = require "skynet.socketdriver"
local gateserver = {}
local socket -- listen socket
local queue -- message queue
local maxclient -- max client
local client_number = 0
local CMD = setmetatable({}, { __gc = function() netpack.clear(queue) end })
local nodelay = false
local connection = {}
-- true : connected
-- nil : closed
-- false : close read
function gateserver.openclient(fd)
if connection[fd] then
socketdriver.start(fd)
end
end
function gateserver.closeclient(fd)
local c = connection[fd]
if c ~= nil then
connection[fd] = nil
socketdriver.close(fd)
end
end
function gateserver.start(handler)
assert(handler.message)
assert(handler.connect)
local listen_context = {}
function CMD.open( source, conf )
assert(not socket)
local address = conf.address or "0.0.0.0"
local port = assert(conf.port)
local backlog = assert(conf.backlog)
maxclient = conf.maxclient or 1024
nodelay = conf.nodelay
skynet.error(string.format("Listen on %s:%d", address, port))
socket = socketdriver.listen(address, port, backlog)
listen_context.co = coroutine.running()
listen_context.fd = socket
skynet.wait(listen_context.co)
conf.address = listen_context.addr
conf.port = listen_context.port
listen_context = nil
socketdriver.start(socket)
if handler.open then
return handler.open(source, conf)
end
end
function CMD.close()
assert(socket)
socketdriver.close(socket)
end
local MSG = {}
local function dispatch_msg(fd, msg, sz)
if connection[fd] then
handler.message(fd, msg, sz)
else
skynet.error(string.format("Drop message from fd (%d) : %s", fd, netpack.tostring(msg,sz)))
end
end
MSG.data = dispatch_msg
local function dispatch_queue()
local fd, msg, sz = netpack.pop(queue)
if fd then
-- may dispatch even the handler.message blocked
-- If the handler.message never block, the queue should be empty, so only fork once and then exit.
skynet.fork(dispatch_queue)
dispatch_msg(fd, msg, sz)
for fd, msg, sz in netpack.pop, queue do
dispatch_msg(fd, msg, sz)
end
end
end
MSG.more = dispatch_queue
function MSG.open(fd, msg)
client_number = client_number + 1
if client_number >= maxclient then
socketdriver.shutdown(fd)
return
end
if nodelay then
socketdriver.nodelay(fd)
end
connection[fd] = true
handler.connect(fd, msg)
end
function MSG.close(fd)
if fd ~= socket then
client_number = client_number - 1
if connection[fd] then
connection[fd] = false -- close read
end
if handler.disconnect then
handler.disconnect(fd)
end
else
socket = nil
end
end
function MSG.error(fd, msg)
if fd == socket then
skynet.error("gateserver accept error:",msg)
else
socketdriver.shutdown(fd)
if handler.error then
handler.error(fd, msg)
end
end
end
function MSG.warning(fd, size)
if handler.warning then
handler.warning(fd, size)
end
end
function MSG.init(id, addr, port)
if listen_context then
local co = listen_context.co
if co then
assert(id == listen_context.fd)
listen_context.addr = addr
listen_context.port = port
skynet.wakeup(co)
listen_context.co = nil
end
end
end
skynet.register_protocol {
name = "socket",
id = skynet.PTYPE_SOCKET, -- PTYPE_SOCKET = 6
unpack = function ( msg, sz )
return netpack.filter( queue, msg, sz)
end,
dispatch = function (_, _, q, type, ...)
queue = q
if type then
MSG[type](...)
end
end
}
local function init()
skynet.dispatch("lua", function (_, address, cmd, ...)
local f = CMD[cmd]
if f then
skynet.ret(skynet.pack(f(address, ...)))
else
skynet.ret(skynet.pack(handler.command(cmd, address, ...)))
end
end)
end
if handler.embed then
init()
else
skynet.start(init)
end
end
return gateserver
关键代码分析
lua
-- 核心数据结构
local connection = {} -- 连接状态管理:true=已连接, nil=已关闭, false=关闭读
local queue -- 消息队列
local maxclient -- 最大客户端数
-- 主要函数
function gateserver.openclient(fd)
-- 开启客户端连接的数据接收
if connection[fd] then
socketdriver.start(fd)
end
end
function gateserver.closeclient(fd)
-- 关闭客户端连接
local c = connection[fd]
if c ~= nil then
connection[fd] = nil
socketdriver.close(fd)
end
end
消息处理机制
主要分2类,一类是处理socket类型消息,一类是lua类型消息
lua
-- socket消息类型处理
local MSG = {}
MSG.data = function(fd, msg, sz) -- 数据到达
if connection[fd] then
handler.message(fd, msg, sz) -- 调用上层handler
end
end
MSG.open = function(fd, msg) -- 新连接
client_number = client_number + 1
if client_number >= maxclient then
socketdriver.shutdown(fd) -- 超过最大连接数,拒绝
return
end
connection[fd] = true
handler.connect(fd, msg) -- 通知上层
end
MSG.close = function(fd) -- 连接关闭
if fd ~= socket then
client_number = client_number - 1
if connection[fd] then
connection[fd] = false -- 标记为关闭读
end
if handler.disconnect then
handler.disconnect(fd) -- 通知上层
end
else
socket = nil -- 监听socket关闭
end
end
gateway.lua
业务级别网关服务实例,增加了业务逻辑,如用户认证、超时检查等,启动网关服务时每个节点只需要启动一个就行,因为网络线程只有一个,启动后可以调用"lua"类型消息open,开启网关服务
lua
local skynet = require "skynet"
local ignoreret = skynet.ignoreret
local log = require "log"
local liblogin
local libcenter
local libclientagent
local libpublish
local gateserver = require "gateserver"
local inspect = inspect
local tonumber, os_date = tonumber, os.date
local self_addr = skynet.self()
local EACH = 50
local connection = {} -- fd -> { fd , ip, uid(登录后有)game(登录后有)key(登录后有)}
local to_check = {} -- 该table会复用,所以需要使用begin to end的方式遍历
local minute = tonumber(os_date("%M"))
skynet.register_protocol {
name = "client",
id = skynet.PTYPE_CLIENT,
}
local handler = {}
function handler.open(source, conf)
log.info("start listen port: %d", conf.port)
end
local function split(input, delimiter)
input = tostring(input)
delimiter = tostring(delimiter)
if (delimiter=='') then return false end
local pos,arr = 0, {}
-- for each divider found
for st,sp in function() return string.find(input, delimiter, pos, true) end do
table.insert(arr, string.sub(input, pos, st - 1))
pos = sp + 1
end
table.insert(arr, string.sub(input, pos))
return arr
end
function handler.connect(fd, addr)
DEBUG("New client from: ", addr, " fd: ", fd)
local ips = split(addr, ":")
local c = {
fd = fd,
ip = ips[1],
uid = nil,
agent = nil,
lastop_ts = minute,
}
connection[fd] = c
gateserver.openclient(fd)
end
function handler.message(fd, msg, sz)
ignoreret() -- session is fd, don't call skynet.ret
local c = connection[fd]
local uid = c.uid
if uid then
--fd为session,特殊用法
c.lastop_ts = minute
skynet.redirect(c.agent, self_addr, "client", uid, msg, sz)
else
local login = liblogin.fetch_login()
--fd为session,特殊用法
skynet.redirect(login, self_addr, "client", fd, msg, sz)
end
end
local CMD = {}
--true/false
function CMD.register(source, data)
log.info("Begin CMD.register %d", source)
local c = connection[data.fd]
if not c then
return false
end
log.info("ok CMD.register %d", source)
c.uid = data.uid
c.agent = data.agent
c.key = data.key
libpublish.register(data.fd, data.uid)
return true
end
function CMD.get_connection(source, fd)
log.info("Begin CMD.get_connection %d", source)
local c = connection[fd]
if not c then
return false
end
log.info("ok CMD.get_connection %d,fd=%d", source,fd)
return c
end
local function close_agent(fd)
local c = connection[fd]
DEBUG("gate server close_agent:", inspect(c))
if c then
connection[fd] = nil
if c.uid then
libcenter.logout(c.uid, c.key)
libclientagent.kick(c.uid, fd)
end
libpublish.unregister(fd)
gateserver.closeclient(fd)
end
return true
end
--true/false
function CMD.kick(source, fd)
DEBUG("cmd.kick fd:", fd)
return close_agent(fd)
end
--true/false
function CMD.kick_all(source)
DEBUG("cmd.kick_all")
for fd, info in pairs(connection) do
pcall(close_agent, fd)
end
return true
end
function handler.disconnect(fd)
DEBUG("handler.disconnect fd:", fd)
return close_agent(fd)
end
function handler.error(fd, msg)
DEBUG("handler.error:", msg)
handler.disconnect(fd)
end
function handler.warning(fd, size)
DEBUG("handler.warning fd:", fd, " size:", size)
end
function handler.command(cmd, source, ...)
DEBUG("gate server handler command:", cmd)
local f = assert(CMD[cmd])
return f(source, ...)
end
gateserver.start(handler)
local function do_check()
local idx = 0
for fd, c in pairs(connection) do
idx = idx + 1
to_check[idx] = fd
end
local fd, c = nil, nil
for i=1, idx do
fd = to_check[i]
c = connection[fd]
if c then
if (minute - c.lastop_ts) > 1 then
close_agent(fd)
end
end
if i % EACH == 0 then
skynet.yield()
end
end
end
local function start_init()
liblogin = require "liblogin"
libcenter = require "libcenter"
libclientagent = require "libclientagent"
libpublish = require "libpublish"
end
skynet.init(function ()
skynet.timeout(10, start_init)
skynet.fork(function()
while true do
skynet.sleep(3000)
minute = tonumber(os_date("%M"))
end
end)
skynet.fork(function()
while true do
skynet.sleep(6000)
minute = tonumber(os_date("%M")) -- check前也更新下当前minute
pcall(do_check)
end
end)
end)
增加的连接管理
lua
local connection = {}
-- fd -> { fd, ip, uid, agent, key, lastop_ts }
-- 增加了用户ID、最后操作时间等业务字段
local to_check = {} -- 用于超时检查的临时表
local minute = tonumber(os_date("%M")) -- 当前分钟数
智能消息路由
通过登录时绑定的fd和agent,使用skynet.redirect对用户进行消息的转发,因为使用skynet.redirect派发"client"类型消息,所以需要skynet.register_protocol注册client协议
lua
function handler.message(fd, msg, sz)
local c = connection[fd]
local uid = c.uid
if uid then
-- 已登录用户:消息转发到对应agent
c.lastop_ts = minute
skynet.redirect(c.agent, self_addr, "client", uid, msg, sz)
else
-- 未登录用户:消息转发到登录服务
local login = liblogin.fetch_login()
skynet.redirect(login, self_addr, "client", fd, msg, sz)
end
end
用户注册机制
例如可以在用户login时调用register绑定agent和fd
lua
function CMD.register(source, data)
local c = connection[data.fd]
if not c then
return false
end
-- 注册用户信息
c.uid = data.uid
c.agent = data.agent
c.key = data.key
libpublish.register(data.fd, data.uid) -- 发布注册事件
return true
end
超时检查机制
lua
local function do_check()
local idx = 0
-- 收集所有连接
for fd, c in pairs(connection) do
idx = idx + 1
to_check[idx] = fd
end
-- 检查超时连接
for i=1, idx do
local fd = to_check[i]
local c = connection[fd]
if c and (minute - c.lastop_ts) > 1 then
close_agent(fd) -- 关闭超时连接
end
if i % EACH == 0 then
skynet.yield() -- 每50个连接让出CPU,避免阻塞
end
end
end
调用链路分析
启动流程
lua
gateserver.start(handler)
→ CMD.open()
→ socketdriver.listen()
→ handler.open()
新连接到达
lua
socket消息 → MSG.open()
→ handler.connect()
→ 记录connection信息
→ gateserver.openclient()开始接收数据
消息处理流程
lua
客户端发送数据 → socket消息 → MSG.data()
→ handler.message()
→ 根据是否登录路由到agent或login服务
用户登录后转发
lua
login服务 → CMD.register()
→ 更新connection信息
→ 后续消息直接转发到agent
连接关闭
lua
socket关闭 → MSG.close()
→ handler.disconnect()
→ close_agent()清理资源
可以把这三个文件想象成一个公司的前台接待系统:
gateserver.lua - 大楼门禁系统
- 功能:管理谁可以进大楼,基本的进出控制
- 职责:开门、关门、统计人数、防止超员
gate.lua - 前台接待员
- 功能:登记访客信息,决定把访客引导到哪里
- 职责:记录访客信息、分配接待部门、传递消息
gateway.lua - 智能接待系统
- 功能:在基础接待上增加了业务逻辑
- 职责:验证身份、记录最后活动时间、自动清理长时间不活动的访客
完整工作流程:
- 访客到达(连接建立)→ 门禁系统记录 → 前台登记
- 访客说话(消息到达)→ 前台根据情况:
- 未认证:转给认证部门(login服务)
- 已认证:直接转给对应负责人(agent服务)
- 认证成功 → 前台更新记录,后续直接转接
- 访客离开(连接断开)→ 前台清理记录,通知相关部门