杰哥乱弹琴之发送队列管理
队列/发送队列是MUD客户端/机器人的重要功能之一。一般客户端都会提供类似于speedwalk的定节奏发送,实现基本的队列和发送限流。
因为speedwalk在追求高性能/立即反应的情况下性能较差,所以我做了另做了一套基于漏桶算法的限流器。
由于是跳过客户端完全由脚本控制的发送管理,所以除了正常的限流外,能完全定制大部分细节,所以可以可以实现新人比较喜欢的#wait,#t+,#t-等类zmud指令,以及可以当作普通队列进行单布发送或重发等功能。
使用库之前先要进行安装。
目前这个库支持GBK编码的mushclient和utf8编码的mudlet
具体安装说明见
https://www.pkuxkx.com/forum/thread-49188-1-1.html
此帖的2楼 简单说一下漏桶算法和令牌算法。
其实这两个算法说是算法,实际上是一个 “高可行性的实施方案”罢了。
可以参考
https://www.cnblogs.com/kiko2014551511/p/16869723.html
总的来说,令牌算法是每隔一定时间有一定的新额度,更适用于防止被请求方打爆
漏桶算法就是有个桶,保证指令以恒定的速度流出去,没留出去的都存在桶里,更适用于防止打爆被请求方。 然后是先上代码和明确概念。
我把我的限流器起名叫metronome,家里有学琴的娃应该都见过,就是能调整间隔,然后以固定节奏打拍子,让娃跟着节奏弹奏的东西。
所以,总体来说,这个节拍器主要是两个参数。
第一个beats,节拍,就是过去一定时间里最多能发送多少指令。
另一个是tick,节奏,就是节拍失效的时间。
然后看创建代码
function M.Metronome:new()
local m = {
_beats = 0,
_tick = 0,
_timer = M.DefaultTimer,
_queue = list:new(),
_sent = list:new(),
_paused = false,
_resumeNext = false,
_sender = self.DefaultSender,
_decoder = M.DefaultDecoder,
_converter = M.DefaultConverter,
_pipe = nil,
_last = {},
params = {}
}
setmetatable(m, self)
return m
end
核心就是那个_sent,代表的是发送历史,也是控制的核心。
_sent会在每次插入或者固定时间进行检查,如果超过节奏,就把对应的发送记录清理掉。然后再和beats对比,如果比beats小,那就说明还能发送指令。
一个很简单容易理解的机制。
不听更新和维护_sent的发送记录,_sent比beats大,就继续等着,比beats小,就能发送,直到_sent的长度大于等于beats为止。
作为一个自定义的限流器,节拍器一开始的目的是为了解决怎么尽快的发出指令,以及怎么确保同时发出指令。
由于mud是基于心跳的。
所以大部分工作的目的是为了
怎么顶着心跳发送的上限发送指令,不受惩罚
这个很简单,用心跳一半的周期发送限制一半的指令,这样可以避免连续两次发送超过心跳上限(爆发式发送也不会超过一个心跳的限制数量)
为了避免网络延迟,可以把周期调的比心跳的一半略大一丢丢,指令数比限制的一半略小一丢丢。
毕竟我一直玩的是会雷劈的MUD,需要在被雷P的边缘作死。
同时,还要确保有必要时,指令必须同时发送
很简单,kill和kill后的指令必须同时发出,不然不是被npc先手白打一个回合了么?
因此。节拍器最核心的指令就是
metronome:send({cmd1,cmd2,cmd3,cmd3},grouped)里面的grouped是是否按组发送。
按组发送的话,节拍器会判断当前tick剩下的空间(space),也就是_sent-beats是否足够。不够就下拍子发送。
当然,为了防止死循环,如果当前拍子如果没发送过任何其他指令,哪怕指令组超长也会强制发出。
以上都是背景和原始设计意图,在北侠用不用关心这个核心功能,北侠的性能完全撑不起我的节拍器全力输出,要不是我还做了个散热器限制输出,top cmd肯定分分钟被打爆。
下面是正题。
当我们有了_sent这个大杀器之后,我们能对队列进行非常细致的调整。
比如,我们如果用当前时间,把_sent填满,那就能强制节拍器休息,直到当前节拍过去
Metronome:full()
Metronome:fullTick()这就是这两个函数的用法了。
那我们拓展一下,如果把未来的时间戳把_sent填满呢?
那不就是让整个节拍器停止N秒,然后继续发送?
不就是Zmud的#wait功能吗?
Metronome:wait(offset)这不就一下子好用了么?
要实现这个功能,我们可以让push方法不仅接收字符串指令,还能接受函数,在函数里给节拍器:wait就行了,这个非常简单。
下一步,我们怎么实现#wait指令和:wait函数的转换呢?
这里我们肯定需要一个将"#wait"的指令,转换成function() Metronome:wait(xxx) end的函数,我这里管他叫解码器(decoder)。
除了#wait,用这个方法很明显我们还能加入#t+ #t- 开关触发组的功能,#pause #resume 的暂停/继续控制,甚至#print的打印方法。
这是我的默认解码器
M._commands = {}
M.register = function(command, handler)
M._commands = handler
end
M.decoder = function(metronome, data)
runtime.HC.eventBus:raiseEvent('core.metronome.sent',metronome)
if (#data > 0 and string.sub(data, 1, 1) == '#') then
local cmd, sep, param = string.match(data, "^#([^ ]+)(%s*)(.-)$")
if cmd ~= nil then
if M._commands ~= nil then
return M._commands(metronome, param)
end
end
end
return data
end
M.register('wait', function(metronome, param)
return function(metronome)
metronome:wait(param / 1000)
end
end)
M.register('pause', function(metronome, param)
return function(metronome)
metronome:pause()
end
end)
M.register('resume', function(metronome, param)
return function(metronome)
metronome:resume()
end
end)
M.register('print', function(metronome, param)
return function(metronome)
print(param)
end
end)
M.register('t+', function(metronome, param)
return function(metronome)
runtime.world:enableTriggers(param)
end
end)
M.register('t-', function(metronome, param)
return function(metronome)
runtime.world:disableTriggers(param)
end
end)
很简单易懂
还提供了一个register方法,能很方便的添加自己的新指令。
使用了这个,我们就能直接发送"n;w;w;#wait 3000;n;e;"的指令了。
实现了#wait,我们就要进入下一个课题了。
节拍器的转发/重定向。
我一直说zmud的#wait不够好用,因为zmud只有一个主队列,你一个队列#wait了其他的发送都卡住了。
而我的这个节拍器明显可以创建多个。
那么我们完全可以用一个主节拍器(默认会安装到Hclua.HC.sender)管理发送和限流。
给不同的功能,比如遍历,任务,触发 给到不同的子节拍器。
甚至我们可以给不同的节拍器设置不同的发送限制,来简单实现 普通指令发送 和 房间移动指令的分别限流
也就是可以做到普通指令每秒发送20个的情况下,移动指令只能1秒4个,甚至4秒1个(比如华山巡山等必须在指定房间待满一定时间)的功能
主要这样
mysend1.withPipe(Hclua.HC.sender).withBeats(2).withTick(1)
mysend2.withPipe(Hclua.HC.sender).withBeats(1).withTick(4)我们就能同时拥有三个不同控制流程的队列了,甚至我们还能创建一个队列暂停着手动发送,这不久实现简单遍历了么?
好,让我们发挥北侠的光荣传统
脑袋一拍,说干就干。
除了常规很容易实现的pause和resume之外,我还实现了一个resumeNext方法
Metronome:resumeNext()简单粗暴的设置了一个单步发送的标志位_resumeNext
在展厅状态下,发送一个指令,再把_resumeNext设为false,实现了一步一发送的功能。
当然,既然想拿来做队列设置简单遍历,还需要做重发功能。
每次发送成功的正常指令,我都放再了一个_last变量里,还提供了一个resend指令,插入重新发送。
这样做简单遍历就很简单了。
判断到了下一个房间,比如看到出口或者你自定义的response了
Metronome:resumeNext()
如果busy了,或者门没开需要重试,那么
Metronome:resend()
简单粗暴又直接。
当然,我个人极不推荐任何用任何形式的队列实现的遍历,值是给个临时解决方案。
好了,下一个问题来了。
如果我的遍历队列,一个移动指令需要对应多个指令怎么办?
比如我移动的路径可能是
open gate;n又比如,我希望每个指令之后自动发送一个response R:walknext,做触发判断是否有我找的npc,怎么办?
很简单,我加入了一个转换器(converter),功能是在计算完指令限制后,将一组原生的指令,转化成一组新的指令。
这个我可以简单的 将 open gate&&n 拆分,并加入resposne,
open gate
n
response R:walkok
还不影响主队列
到此为止,很明显,我们加上合适的触发(response 和 需要重发),就能做一个简单遍历找npc的功能了。
页:
[1]
2