« MMORPG 的同步设计 | 返回首页 | Direct3D12 的接口设计 bug »

日程表服务

skynet 的用户中,问的比较多的一个问题是,为什么我改了系统时间对 skynet 却没有生效?继续追问发现,有这个需求的人大多是想实现一个日程表,到某个特定时间触发特定的任务,修改系统时间是为了测试。

不得不说,通过修改系统时间来测试是个直接、却很糟糕的主意。skynet 的定时器也不依赖系统时间驱动,修改系统时间自然也不会生效。

日程服务是个普遍的需求。在国内网游里,你要不做什么节日任务、每周副本,基本不可能上线。这篇 blog 就谈谈这类需求应该在 skynet 中如何实现。

最好的方法是实现一个单独的服务,用来控制日程。这样、如果你有测试的需求,也可以通过预留的接口让这个服务模拟时间快进,快速触发定时任务了。

通常,日程任务不需要特别高的时间精度,精确到分钟就够了。我们一般不会精确到秒来设定任务开启的时间。日程设置也多半和具体日期、星期几有关。常见的需求类似这样:每周六晚上 8 点开启;每个月的第二个周四中午 12 点;6 月 1 日儿童节活动;等等。

如果要设计一个这样的日程表服务,其实只需要一个订阅接口,类似 skynet timer 那样单次触发的就够了。触发接口就是传入一个时间点,可以描述上面例子中描述的日程。我们在具体活动实现的服务中用 skynet.call 这个日程安排服务,当 skynet.call 返回时,就是日程时间触发点。例如,我们需要每周 6 晚上 8 点固定开启一个活动,可以用一个 while 循环:

while true do
  skynet.call(schedule_service, "lua", { wday = 7 , hour = 20 } )
  -- 执行活动
end

这个日程表服务每次接到订阅请求,就按照参数计算出下一个符合要求的触发时间点,然后用 skynet.sleep 等到触发时间即可。

如果出于调试需要改动当前时间,直接通知这个服务,调整其内部时间和真实时间的时间差,wakeup 所有正在等待的请求,重新计算等待时间就好了。


这样一个日程表服务并不复杂,我花了一点时间随手实现了一个,供大家参考。它应该有很大的改进空间,明白了其设计之后,应该不难完善它:

local skynet = require "skynet"
local service = require "skynet.service"

local schedule = {}
local service_addr

-- { month=, day=, wday=, hour= , min= }
function schedule.submit(ti)
    return skynet.call(service_addr, "lua", ti)
end

function schedule.changetime(ti)
    local tmp = {}
    for k,v in pairs(ti) do
        tmp[k] = v
    end
    tmp.changetime = true
    return skynet.call(service_addr, "lua", tmp)
end

skynet.init(function()
    local schedule_service = function()
-- schedule service

local skynet = require "skynet"

local task = { session = 0, difftime = 0 }

local function next_time(now, ti)
    local nt = {
        year = now.year ,
        month = now.month ,
        day = now.day,
        hour = ti.hour or 0,
        min = ti.min or 0,
        sec = ti.sec,
    }
    if ti.wday then
        -- set week
        assert(ti.day == nil and ti.month == nil)
        nt.day = nt.day + ti.wday - now.wday
        local t = os.time(nt)
        if t < now.time then
            nt.day = nt.day + 7
        end
    else
        -- set day, no week day
        if ti.day then
            nt.day = ti.day
        end
        if ti.month then
            nt.month = ti.month
        end
        local t = os.time(nt)
        if t < now.time then
            if ti.month then
                nt.year = nt.year + 1   -- next year
            else
                nt.month = nt.month + 1 -- next month
            end
        end
    end

    return os.time(nt)
end

local function changetime(ti)
    local ct = math.floor(skynet.time())
    local current = os.date("*t", ct)
    current.time = ct
    if not ti.hour then
        ti.hour = current.hour
    end
    if not ti.min then
        ti.min = current.min
    end
    ti.sec = current.sec
    local nt = next_time(current, ti)
    skynet.error(string.format("Change time to %s", os.date(nil, nt)))
    task.difftime = os.difftime(nt,ct)
    for k,v in pairs(task) do
        if type(v) == "table" then
            skynet.wakeup(v.co)
        end
    end
    skynet.ret()
end

local function submit(_, addr, ti)
    if ti.changetime then
        return changetime(ti)
    end
    local session = task.session + 1
    task.session = session
    repeat
        local ct = math.floor(skynet.time()) + task.difftime
        local current = os.date("*t", ct)
        current.time = ct
        local nt = next_time(current, ti)
        task[session] = { time = nt, co = coroutine.running(), address = addr }
        local diff = os.difftime(nt , ct)
        print("sleep", diff)
    until skynet.sleep(diff * 100) ~= "BREAK"
    task[session] = nil
    skynet.ret()
end

skynet.start(function()
    skynet.dispatch("lua", submit)
    skynet.info_func(function()
        local info = {}
        for k, v in pairs(task) do
            if type(v) == "table" then
                table.insert( info, {
                    time = os.date(nil, v.time),
                    address = skynet.address(v.address),
                })
            end
            return info
        end
    end)
end)

-- end of schedule service
    end

    service_addr = service.new("schedule", schedule_service)
end)

return schedule

用 schedule.submit 可以申请一个时间点,到时后会返回。使用的时候只需要用 skynet.fork 开一个独立线程,循环调用 schedule.submit 即可。

schedule.changetime 支持修改当前时间,供调试用。

Comments

可以使用faketime
你的页面终于换成tls了,但还不是h2,不知是否开启?如果h2使用了openssl,要升级版本,否则不会打开alpn,无法与chrome协商成h2.
不错
不错
从08年玩大话西游开始就听说过云风。到13年高中毕业,17年参加工作。作为一个C++服务器方向的程序员,希望能跟您多交流交流。
做通用的游戏服务端解决方案。这个更有商业价值啊。
这个觉得skynet 应该集成更多这类游戏相关的功能。做完全通用的服务框架,实际上是没有任何价值的。因为现在的语言已经提供了这类服务框架
打算把这个服务加入框架吗?
醍醐灌顶

Post a comment

非这个主题相关的留言请到:留言本