北大侠客行MUD论坛

 找回密码
 注册
搜索
热搜: 新手 wiki 升级
查看: 1758|回复: 11

杰哥乱弹琴之发送队列管理

[复制链接]
发表于 2024-5-28 14:54:18 | 显示全部楼层 |阅读模式
队列/发送队列是MUD客户端/机器人的重要功能之一。

一般客户端都会提供类似于speedwalk的定节奏发送,实现基本的队列和发送限流。

因为speedwalk在追求高性能/立即反应的情况下性能较差,所以我做了另做了一套基于漏桶算法的限流器。

由于是跳过客户端完全由脚本控制的发送管理,所以除了正常的限流外,能完全定制大部分细节,所以可以可以实现新人比较喜欢的#wait,#t+,#t-等类zmud指令,以及可以当作普通队列进行单布发送或重发等功能。
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 14:54:53 | 显示全部楼层

使用库之前先要进行安装。

目前这个库支持GBK编码的mushclient和utf8编码的mudlet

具体安装说明见

https://www.pkuxkx.net/forum/thread-49188-1-1.html

此帖的2楼
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 14:58:00 | 显示全部楼层
简单说一下漏桶算法和令牌算法。

其实这两个算法说是算法,实际上是一个 “高可行性的实施方案”罢了。

可以参考

https://www.cnblogs.com/kiko2014551511/p/16869723.html

总的来说,令牌算法是每隔一定时间有一定的新额度,更适用于防止被请求方打爆

漏桶算法就是有个桶,保证指令以恒定的速度流出去,没留出去的都存在桶里,更适用于防止打爆被请求方。
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
回复 支持 1 反对 0

使用道具 举报

 楼主| 发表于 2024-5-28 15:05:11 | 显示全部楼层
然后是先上代码和明确概念。

我把我的限流器起名叫metronome,家里有学琴的娃应该都见过,就是能调整间隔,然后以固定节奏打拍子,让娃跟着节奏弹奏的东西。

所以,总体来说,这个节拍器主要是两个参数。

第一个beats,节拍,就是过去一定时间里最多能发送多少指令。

另一个是tick,节奏,就是节拍失效的时间。

然后看创建代码

  1.     function M.Metronome:new()
  2.         local m = {
  3.             _beats = 0,
  4.             _tick = 0,
  5.             _timer = M.DefaultTimer,
  6.             _queue = list:new(),
  7.             _sent = list:new(),
  8.             _paused = false,
  9.             _resumeNext = false,
  10.             _sender = self.DefaultSender,
  11.             _decoder = M.DefaultDecoder,
  12.             _converter = M.DefaultConverter,
  13.             _pipe = nil,
  14.             _last = {},
  15.             params = {}
  16.         }
  17.         setmetatable(m, self)
  18.         return m
  19.     end
复制代码
核心就是那个_sent,代表的是发送历史,也是控制的核心。

_sent会在每次插入或者固定时间进行检查,如果超过节奏,就把对应的发送记录清理掉。然后再和beats对比,如果比beats小,那就说明还能发送指令。

一个很简单容易理解的机制。

不听更新和维护_sent的发送记录,_sent比beats大,就继续等着,比beats小,就能发送,直到_sent的长度大于等于beats为止。


北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 15:15:17 | 显示全部楼层
作为一个自定义的限流器,节拍器一开始的目的是为了解决怎么尽快的发出指令,以及怎么确保同时发出指令。

由于mud是基于心跳的。

所以大部分工作的目的是为了

怎么顶着心跳发送的上限发送指令,不受惩罚

这个很简单,用心跳一半的周期发送限制一半的指令,这样可以避免连续两次发送超过心跳上限(爆发式发送也不会超过一个心跳的限制数量)

为了避免网络延迟,可以把周期调的比心跳的一半略大一丢丢,指令数比限制的一半略小一丢丢。

毕竟我一直玩的是会雷劈的MUD,需要在被雷P的边缘作死。

同时,还要确保有必要时,指令必须同时发送

很简单,kill和kill后的指令必须同时发出,不然不是被npc先手白打一个回合了么?

因此。节拍器最核心的指令就是

  1. metronome:send({cmd1,cmd2,cmd3,cmd3},grouped)
复制代码
里面的grouped是是否按组发送。

按组发送的话,节拍器会判断当前tick剩下的空间(space),也就是_sent-beats是否足够。不够就下拍子发送。

当然,为了防止死循环,如果当前拍子如果没发送过任何其他指令,哪怕指令组超长也会强制发出。

以上都是背景和原始设计意图,在北侠用不用关心这个核心功能,北侠的性能完全撑不起我的节拍器全力输出,要不是我还做了个散热器限制输出,top cmd肯定分分钟被打爆。
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 15:22:36 | 显示全部楼层
下面是正题。

当我们有了_sent这个大杀器之后,我们能对队列进行非常细致的调整。

比如,我们如果用当前时间,把_sent填满,那就能强制节拍器休息,直到当前节拍过去

  1. Metronome:full()
  2. Metronome:fullTick()
复制代码
这就是这两个函数的用法了。

那我们拓展一下,如果把未来的时间戳把_sent填满呢?

那不就是让整个节拍器停止N秒,然后继续发送?

不就是Zmud的#wait功能吗?

  1. Metronome:wait(offset)
复制代码
这不就一下子好用了么?

要实现这个功能,我们可以让push方法不仅接收字符串指令,还能接受函数,在函数里给节拍器:wait就行了,这个非常简单。

下一步,我们怎么实现#wait指令和:wait函数的转换呢?
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 15:27:07 | 显示全部楼层
这里我们肯定需要一个将"#wait"的指令,转换成function() Metronome:wait(xxx) end的函数,我这里管他叫解码器(decoder)。

除了#wait,用这个方法很明显我们还能加入#t+ #t- 开关触发组的功能,#pause #resume 的暂停/继续控制,甚至#print的打印方法。

这是我的默认解码器

  1.     M._commands = {}
  2.     M.register = function(command, handler)
  3.         M._commands[command] = handler
  4.     end
  5.     M.decoder = function(metronome, data)
  6.         runtime.HC.eventBus:raiseEvent('core.metronome.sent',metronome)
  7.         if (#data > 0 and string.sub(data, 1, 1) == '#') then
  8.             local cmd, sep, param = string.match(data, "^#([^ ]+)(%s*)(.-)$")
  9.             if cmd ~= nil then
  10.                 if M._commands[cmd] ~= nil then
  11.                     return M._commands[cmd](metronome, param)
  12.                 end
  13.             end
  14.         end
  15.         return data
  16.     end
  17.     M.register('wait', function(metronome, param)
  18.         return function(metronome)
  19.             metronome:wait(param / 1000)
  20.         end
  21.     end)
  22.     M.register('pause', function(metronome, param)
  23.         return function(metronome)
  24.             metronome:pause()
  25.         end
  26.     end)
  27.     M.register('resume', function(metronome, param)
  28.         return function(metronome)
  29.             metronome:resume()
  30.         end
  31.     end)
  32.     M.register('print', function(metronome, param)
  33.         return function(metronome)
  34.             print(param)
  35.         end
  36.     end)
  37.     M.register('t+', function(metronome, param)
  38.         return function(metronome)
  39.             runtime.world:enableTriggers(param)
  40.         end
  41.     end)
  42.     M.register('t-', function(metronome, param)
  43.         return function(metronome)
  44.             runtime.world:disableTriggers(param)
  45.         end
  46.     end)

复制代码
很简单易懂

还提供了一个register方法,能很方便的添加自己的新指令。

使用了这个,我们就能直接发送"n;w;w;#wait 3000;n;e;"的指令了。
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 15:34:20 | 显示全部楼层
实现了#wait,我们就要进入下一个课题了。

节拍器的转发/重定向。

我一直说zmud的#wait不够好用,因为zmud只有一个主队列,你一个队列#wait了其他的发送都卡住了。

而我的这个节拍器明显可以创建多个。

那么我们完全可以用一个主节拍器(默认会安装到Hclua.HC.sender)管理发送和限流。

给不同的功能,比如遍历,任务,触发 给到不同的子节拍器。

甚至我们可以给不同的节拍器设置不同的发送限制,来简单实现 普通指令发送 和 房间移动指令的分别限流

也就是可以做到普通指令每秒发送20个的情况下,移动指令只能1秒4个,甚至4秒1个(比如华山巡山等必须在指定房间待满一定时间)的功能

主要这样

  1. mysend1.withPipe(Hclua.HC.sender).withBeats(2).withTick(1)
  2. mysend2.withPipe(Hclua.HC.sender).withBeats(1).withTick(4)
复制代码
我们就能同时拥有三个不同控制流程的队列了,甚至我们还能创建一个队列暂停着手动发送,这不久实现简单遍历了么?
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 15:43:48 | 显示全部楼层
好,让我们发挥北侠的光荣传统

脑袋一拍,说干就干。

除了常规很容易实现的pause和resume之外,我还实现了一个resumeNext方法

  1. Metronome:resumeNext()
复制代码
简单粗暴的设置了一个单步发送的标志位_resumeNext

在展厅状态下,发送一个指令,再把_resumeNext设为false,实现了一步一发送的功能。

当然,既然想拿来做队列设置简单遍历,还需要做重发功能。

每次发送成功的正常指令,我都放再了一个_last变量里,还提供了一个resend指令,插入重新发送。

这样做简单遍历就很简单了。

判断到了下一个房间,比如看到出口或者你自定义的response了
  1. Metronome:resumeNext()
复制代码
如果busy了,或者门没开需要重试,那么
  1. Metronome:resend()
复制代码
简单粗暴又直接。

当然,我个人极不推荐任何用任何形式的队列实现的遍历,值是给个临时解决方案。
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
 楼主| 发表于 2024-5-28 15:48:59 | 显示全部楼层
好了,下一个问题来了。

如果我的遍历队列,一个移动指令需要对应多个指令怎么办?

比如我移动的路径可能是

  1. open gate;n
复制代码
又比如,我希望每个指令之后自动发送一个response R:walknext,做触发判断是否有我找的npc,怎么办?

很简单,我加入了一个转换器(converter),功能是在计算完指令限制后,将一组原生的指令,转化成一组新的指令。

这个我可以简单的 将 open gate&&n 拆分,并加入resposne,
  1. open gate
  2. n
  3. response R:walkok
复制代码


还不影响主队列

到此为止,很明显,我们加上合适的触发(response 和 需要重发),就能做一个简单遍历找npc的功能了。
北大侠客行Mud(pkuxkx.net),最好的中文Mud游戏!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|小黑屋|北大侠客行MUD ( 京ICP备16065414号-1 )

GMT+8, 2024-9-21 11:26 AM , Processed in 0.015549 second(s), 16 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表