projectdiscovery/ratelimit 源码剖析:面向扫描器的“整段补充”式突发限流器
projectdiscovery/ratelimit 源码剖析面向扫描器的“整段补充”式突发限流器【免费下载链接】scan4allOfficial repository vuls Scan: 15000PoCs; 23 kinds of application password crack; 7000Web fingerprints; 146 protocols and 90000 rules Port scanning; Fuzz, HW, awesome BugBounty( ͡° ͜ʖ ͡°)...项目地址: https://gitcode.com/GitHub_Trending/sca/scan4all导读本文以 vendor/github.com/projectdiscovery/ratelimit/README.md 为骨架完整拆解这个 Go 限流库的核心设计——它允许在指定时间窗口内一次性突发全部额度窗口结束后整段回填令牌而非逐令牌平滑补充。读完本文你将掌握它的 API 用法、底层并发实现原理、与golang.org/x/time/rate的差异以及它在 scan4all 仓库中作为间接依赖被 vendor 引入的形态可直接迁移用于自己的扫描器或爬虫限速场景。一、这个库解决什么问题projectdiscovery/ratelimit是一个 Go 语言的限流实现其定位非常明确允许在指定时间窗口内突发执行一批请求。它被设计为面向“扫描器scanner”这类工作负载——在目标站点设定的最大速率限制内以最快速度把配额用完然后整体暂停等下一个时间窗口到来再继续全速处理。这一点与经典的平滑限流思路截然不同。对于一次需要发出大量探测请求的安全扫描来说逐请求均匀限速反而会拉长整个扫描的耗时而“突发 整体等待”的方式可以在尊重目标速率上限的前提下把每个窗口内的吞吐压满。在 scan4all 仓库中该库以 vendor 形式存在于 vendor/github.com/projectdiscovery/ratelimit/包含ratelimit.go、keyratelimit.go、README.md、LICENSE并在 go.mod 中声明为github.com/projectdiscovery/ratelimit v0.0.9 // indirect属于随 projectdiscovery 生态一并引入的间接依赖。二、与 golang.org/x/time/rate 的核心差异README 用专门一节阐述了它与 Go 标准生态中最常用的golang.org/x/time/rate#Limiter的区别这是理解本库设计哲学的关键golang.org/x/time/rate实现的是经典令牌桶token bucket算法。它允许桶中预存一批突发令牌但回填refill是逐单位进行的——按照指定的速率比值一次补充一个令牌请求会随着回填节奏被平滑地降速处理。projectdiscovery/ratelimit是令牌桶的一个变体。突发能力与经典令牌桶一致一次可取完桶内全部令牌但回填不是逐令牌进行而是在定义的时间窗口结束时一次性整体回填。README 中给出了这一设计对扫描器的直接意义This allows scanners to respect maximum defined rate limits, pause until the allowed interval hits, and then process again at maximum speed. The original library slowed down requests according to the refill ratio.即扫描器先以最大速度消费完窗口内额度然后阻塞等待直到下一个窗口到达再以最大速度继续——目标端看到的速率始终被限制在配置的窗口上限之内而扫描器自身的空闲等待被集中化不浪费在逐请求的节流上。三、快速上手README 中的完整示例README 提供了一段可直接运行的最小示例演示了ratelimit.New与Take的最基本用法以下代码原样继承自 README.mdpackage main import ( context fmt time github.com/projectdiscovery/ratelimit ) func main() { // create a rate limiter by passing context, max tasks/requests , time interval limiter : ratelimit.New(context.Background(), 5, time.Duration(10*time.Second)) save : time.Now() for i : 0; i 10; i { // run limiter.Take() method before each task limiter.Take() fmt.Printf(Task %v completed after %v\n, i, time.Since(save)) } /* Output: Task 0 completed after 4.083µs Task 1 completed after 111.416µs Task 2 completed after 118µs Task 3 completed after 121.083µs Task 4 completed after 124.583µs Task 5 completed after 10.001356375s Task 6 completed after 10.001524791s Task 7 completed after 10.001537583s Task 8 completed after 10.001542708s Task 9 completed after 10.001548666s */ }示例行为解读ratelimit.New(context.Background(), 5, time.Duration(10*time.Second))创建一个限流器5为窗口内最大任务/请求数10s为时间窗口。每个任务执行前调用一次limiter.Take()获取一个令牌。输出清晰地展示了“突发”特征前 5 个任务在约 125µs 内全部完成一次性消耗完 5 个令牌第 6 个任务开始前阻塞了约 10 秒等待下一个窗口的整体回填随后第 610 个任务再次以微秒级间隔瞬间完成。这个运行结果就是该库“整段回填”行为的最直观证据等待集中在窗口边界而窗口内则全程全速。四、源码剖析Limiter 的并发实现在 vendor/github.com/projectdiscovery/ratelimit/ratelimit.go 中核心数据结构定义如下// Limiter allows a burst of request during the defined duration type Limiter struct { maxCount uint32 count atomic.Uint32 ticker *time.Ticker tokens chan struct{} ctx context.Context // internal cancelFunc context.CancelFunc }各字段的职责可以这样理解字段类型作用maxCountuint32每个时间窗口内允许突发执行的令牌总数即窗口额度countatomic.Uint32当前桶内可用令牌数使用原子操作保证并发安全ticker*time.Ticker以duration为周期触发“整体回填”的定时器tokenschan struct{}令牌通道Take()通过接收通道元素来获取令牌ctx/cancelFunccontext外部上下文与内部取消函数用于优雅停止限流器后台协程 run回填与发放逻辑New会启动一个后台 goroutine 执行run这是整个算法的引擎ratelimit.gofunc (limiter *Limiter) run(ctx context.Context) { defer close(limiter.tokens) for { if limiter.count.Load() 0 { -limiter.ticker.C limiter.count.Store(limiter.maxCount) } select { case -ctx.Done(): // Internal Context limiter.ticker.Stop() return case -limiter.ctx.Done(): limiter.ticker.Stop() return case limiter.tokens - struct{}{}: limiter.count.Add(minusOne) case -limiter.ticker.C: limiter.count.Store(limiter.maxCount) } } }关键逻辑可以拆成三点额度耗尽时整段等待当count降到 0协程先阻塞在-limiter.ticker.C等到下一个窗口边界然后把count一次性重置为maxCount——这就是 README 所说的“refill happens entirely at the defined ratio”。令牌发放与计数联动select中尝试向tokens通道发送一个空结构体代表放入一个令牌同时count通过atomic.Add(minusOne)递减minusOne被定义为^uint32(0)即无符号的-1。双 context 退出机制无论是外部传入的ctx还是内部cancelFunc派生的internalctx被取消都会停止 ticker 并退出协程最后通过defer close(limiter.tokens)关闭令牌通道。公开 API 一览Take()ratelimit.go-limiter.tokens从令牌通道取出一个令牌取不到时阻塞是典型的同步限流点。CanTake()ratelimit.goreturn limiter.count.Load() 0非阻塞地探测当前是否还有令牌适合与超时/轮询逻辑配合。GetLimit()ratelimit.go返回每个窗口的额度maxCount。Stop()ratelimit.go调用内部cancelFunc()停止限流器并释放 ticker。New 与 NewUnlimitedfunc New(ctx context.Context, max uint, duration time.Duration) *Limiter { internalctx, cancel : context.WithCancel(context.TODO()) limiter : Limiter{ maxCount: uint32(max), ticker: time.NewTicker(duration), tokens: make(chan struct{}), ctx: ctx, cancelFunc: cancel, } limiter.count.Store(uint32(max)) go limiter.run(internalctx) return limiter } func NewUnlimited(ctx context.Context) *Limiter { // ... limiter : Limiter{ maxCount: math.MaxUint32, ticker: time.NewTicker(time.Millisecond), tokens: make(chan struct{}), ctx: ctx, cancelFunc: cancel, } limiter.count.Store(math.MaxUint32) go limiter.run(internalctx) return limiter }注意几个实现细节源码可证New初始化时count即被设为满额max因此限流器创建后立即可突发使用无需等待第一个窗口。NewUnlimited用math.MaxUint32近似“无限令牌”并把 ticker 周期压到time.Millisecond即使窗口耗尽也会在毫秒级完成回填从行为上等价于不限速。每个限流器内部都独立派生了internalctx外部 ctx 取消与Stop()都能触发同一套退出清理流程。另外在源码中可以看到一段被注释掉的SleepandResetratelimit.go注释说明它原本用于“自适应限流Adaptive Ratelimiting”且受限于实现无法处理多次调用目前并未启用——说明作者曾为动态调整窗口额度保留过扩展思路。五、按 key 隔离的 MultiLimiter在扫描、爬虫等场景中常常需要对不同目标如不同 host分别限流。keyratelimit.go 提供了基于 key 的多限流器封装MultiLimiter。Options 与校验type Options struct { Key string // Unique Identifier IsUnlimited bool MaxCount uint Duration time.Duration }Validate()keyratelimit.go强制约束非 unlimited 模式下Key不能为空、MaxCount不能为 0、Duration必须非零否则返回带MultiLimiter标签的错误。核心方法方法行为NewMultiLimiter(ctx, opts)创建MultiLimiter并添加首个限流桶Add(opts)按Key新增一个桶内部用sync.Map.LoadOrStore去重key 已存在时返回ErrKeyAlreadyExistsTake(key)对指定 key 的桶取令牌key 不存在时返回ErrKeyMissingCanTake(key)非阻塞探测指定 key 是否有令牌GetLimit(key)查询指定 key 的窗口额度AddAndTake(opts)若 key 已存在直接取令牌否则先Add再Take适合“惰性初始化”式用法Stop(keys...)停止指定 key 的限流器不传任何 key 时遍历sync.Map停止全部MultiLimiter内部用sync.Map维护map[string]*Limiter因此天然支持并发地对不同 key 执行Take/AddAndTake非常适合“每个目标 host 一套独立限流窗口”的多租户扫描场景。六、在 scan4all 中的存在形态与工程定位间接依赖scan4all 的 go.mod 中声明github.com/projectdiscovery/ratelimit v0.0.9 // indirectgo.sum 中固定了v0.0.9的哈希。它作为 projectdiscovery 生态组件被引入以 vendor 目录形式随仓库分发保证构建可复现。直接使用情况从当前源码检索结果看scan4all 自己的 HTTP 探测模块 pkg/httpx/runner/runner.go 与端口扫描模块 pkg/naabu/v2/pkg/runner/runner.go 使用的限流实现是go.uber.org/ratelimit另一个基于漏桶/令牌桶思路的第三方库其默认限速配置可见 pkg/httpx/runner/options.go默认RateLimit 150。因此本仓库中projectdiscovery/ratelimit目前属于随依赖链引入、可在后续模块中直接复用的能力。可直接复用的价值由于其 API 极简NewTake/MultiLimiter按 key 隔离任何扫描、指纹识别、爆破等模块都可以在构造请求前插入limiter.Take()将“突发窗口 整体等待”的节奏应用到自己的流量上且无需引入额外的外部依赖。七、使用建议与注意事项定位匹配场景当业务要求“窗口内最大请求数 尽可能快的吞吐”时本库的突发语义是最优解若你的场景需要平滑匀速、避免瞬时洪峰例如控制数据库连接池或第三方 API 配额则应考虑golang.org/x/time/rate这类逐令牌回填的实现。Take 会阻塞Take()在没有令牌时无限期阻塞生产代码应配合 context 超时或CanTake()轮询避免窗口配置过大时 goroutine 长时间挂起。记得 Stop每个New/NewMultiLimiter都会启动后台 ticker goroutine用完应调用Stop()MultiLimiter可传 key 精确停止防止资源泄漏。合理选择窗口粒度窗口duration与额度maxCount共同决定了目标端看到的速率上限maxCount / duration。扫描场景中建议根据目标服务的能力文档或实际探测结果动态调整必要时可结合GetLimit()与CanTake()做自适应探测。测试驱动理解README 中的示例输出就是最好的“行为契约”——前 5 个任务微秒级完成、后 5 个任务整体平移约 10 秒这一输出可通过运行该示例在任何 Go 环境直接复现验证。小结projectdiscovery/ratelimit以不到 200 行的核心代码ratelimit.go keyratelimit.go实现了一个极具工程针对性的“突发式”限流器令牌窗口内全速突发、窗口边界整段回填并辅以按 key 隔离的MultiLimiter与NewUnlimited快捷构造。它用atomic计数 通道 ticker 三个原语就完成了并发安全的核心逻辑是理解 Go 并发限流、或在扫描器/爬虫中控制出站流量速率的优秀参考实现。【免费下载链接】scan4allOfficial repository vuls Scan: 15000PoCs; 23 kinds of application password crack; 7000Web fingerprints; 146 protocols and 90000 rules Port scanning; Fuzz, HW, awesome BugBounty( ͡° ͜ʖ ͡°)...项目地址: https://gitcode.com/GitHub_Trending/sca/scan4all创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →