基于 IP 地址限制 Go 中的 HTTP 请求速率

2025-06-04

基于 IP 地址限制 Go 中的 HTTP 请求速率

如果您正在运行 HTTP 服务器,并且想要限制对端点的请求速率,则可以使用维护良好的工具,例如github.com/didip/tollbooth。但是,如果您正在构建一些非常简单的东西,那么自己实现它并不难。

已经有一个实验性的 Go 包x/time/rate,我们可以使用它。

在本教程中,我们将创建一个简单的中间件,用于根据用户的 IP 地址进行速率限制。

纯 HTTP 服务器

我们先来构建一个简单的 HTTP 服务器,它有一个非常简单的端点。但它可能是一个负载很大的端点,所以我们想在那里添加速率限制。

package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", okHandler)

    if err := http.ListenAndServe(":8888", mux); err != nil {
        log.Fatalf("unable to start server: %s", err.Error())
    }
}

func okHandler(w http.ResponseWriter, r *http.Request) {
    // Some very expensive database call
    w.Write([]byte("alles gut"))
}
Enter fullscreen mode Exit fullscreen mode

我们main.go启动服务器:8888并拥有一个端点/

golang.org/x/time/rate

我们将使用x/time/rateGo 包,该包提供了一个令牌桶速率限制器算法。rate #Limiter控制允许事件发生的频率。它实现了一个大小为 的“令牌桶” b,初始状态为满,并以每秒 的速率重新填充r令牌。通俗地说,在任何足够长的时间间隔内,限制器将速率限制为每秒 r 个令牌,最大突发大小为 b 个事件。

由于我们想要为每个 IP 地址实现速率限制器,因此我们还需要维护一个限制器映射。

package main

import (
    "sync"

    "golang.org/x/time/rate"
)

// IPRateLimiter .
type IPRateLimiter struct {
    ips map[string]*rate.Limiter
    mu  *sync.RWMutex
    r   rate.Limit
    b   int
}

// NewIPRateLimiter .
func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter {
    i := &IPRateLimiter{
        ips: make(map[string]*rate.Limiter),
        mu:  &sync.RWMutex{},
        r:   r,
        b:   b,
    }

    return i
}

// AddIP creates a new rate limiter and adds it to the ips map,
// using the IP address as the key
func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter {
    i.mu.Lock()
    defer i.mu.Unlock()

    limiter := rate.NewLimiter(i.r, i.b)

    i.ips[ip] = limiter

    return limiter
}

// GetLimiter returns the rate limiter for the provided IP address if it exists.
// Otherwise calls AddIP to add IP address to the map
func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter {
    i.mu.Lock()
    limiter, exists := i.ips[ip]

    if !exists {
        i.mu.Unlock()
        return i.AddIP(ip)
    }

    i.mu.Unlock()

    return limiter
}
Enter fullscreen mode Exit fullscreen mode

NewIPRateLimiter创建一个 IP 限制器实例,HTTP 服务器将必须调用GetLimiter此实例来获取指定 IP 的限制器(从映射中或生成一个新的)。

中间件

让我们升级我们的 HTTP 服务器并将中间件添加到所有端点,因此如果 IP 已达到限制,它将响应 429 请求过多,否则,它将继续处理请求。

limitMiddleware函数中,每当中间件收到 HTTP 请求时,我们都会调用全局限制器的Allow()方法。如果令牌桶中没有剩余令牌,Allow()则返回 false,并向用户发送 429 Too Many Requests 响应。否则,调用该方法Allow()只会消耗令牌桶中的一个令牌,并将控制权交给链中的下一个处理程序。

package main

import (
    "log"
    "net/http"
)

var limiter = NewIPRateLimiter(1, 5)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", okHandler)

    if err := http.ListenAndServe(":8888", limitMiddleware(mux)); err != nil {
        log.Fatalf("unable to start server: %s", err.Error())
    }
}

func limitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        limiter := limiter.GetLimiter(r.RemoteAddr)
        if !limiter.Allow() {
            http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func okHandler(w http.ResponseWriter, r *http.Request) {
    // Some very expensive database call
    w.Write([]byte("alles gut"))
}
Enter fullscreen mode Exit fullscreen mode

构建并运行

go get golang.org/x/time/rate
go build -o server .
./server
Enter fullscreen mode Exit fullscreen mode

测试

我喜欢使用一个非常好的工具来进行 HTTP 负载测试,称为vegeta(也是用 Go 编写的)。

brew install vegeta
Enter fullscreen mode Exit fullscreen mode

我们需要创建一个简单的配置文件,说明我们想要产生什么请求。

GET http://localhost:8888/
Enter fullscreen mode Exit fullscreen mode

然后以每单位时间 100 个请求的速度运行攻击 10 秒。

vegeta attack -duration=10s -rate=100 -targets=vegeta.conf | vegeta report
Enter fullscreen mode Exit fullscreen mode

结果你会看到有些请求返回 200,但大多数返回 429。

文章来源:https://dev.to/der_gopher/rate-limiting-http-requests-in-go-based-on-ip-address-542g
PREV
CSS 是一种编程语言吗?CSS 和 HTML 都是编程语言吗?
NEXT
10 个学习 JavaScript 的网站 [免费] 你好