基于skynet框架业务中的gateway实现分析

基于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 - 智能接待系统

  • 功能:在基础接待上增加了业务逻辑
  • 职责:验证身份、记录最后活动时间、自动清理长时间不活动的访客

完整工作流程:

  1. 访客到达(连接建立)→ 门禁系统记录 → 前台登记
  2. 访客说话(消息到达)→ 前台根据情况:
  • 未认证:转给认证部门(login服务)
  • 已认证:直接转给对应负责人(agent服务)
  1. 认证成功 → 前台更新记录,后续直接转接
  2. 访客离开(连接断开)→ 前台清理记录,通知相关部门
相关推荐
问道飞鱼4 小时前
【服务器知识】HTTP 请求头信息及其用途详细说明
运维·服务器·nginx·http·http头信息
weixin_436525074 小时前
linux-RabbitMQ创建虚拟主机、用户、分配权限、标签
linux·运维·服务器·rabbitmq
Leo655358 小时前
JDK8 的排序、分组求和,转换为Map
java·开发语言
磨十三9 小时前
C++ 标准库排序算法 std::sort 使用详解
开发语言·c++·排序算法
云心雨禅10 小时前
WordPress提速指南:Memcached+Super Static Cache+CDN缓存网站内容
linux·服务器·数据库·缓存·memcached
两只程序猿10 小时前
数据可视化 | Violin Plot小提琴图Python实现 数据分布密度可视化科研图表
开发语言·python·信息可视化
Pluchon10 小时前
硅基计划4.0 算法 字符串
java·数据结构·学习·算法
折翅鵬10 小时前
Android 程序员如何系统学习 MQTT
android·学习
野生技术架构师10 小时前
1000 道 Java 架构师岗面试题
java·开发语言